feature/IO-3255-simplified-parts-management - Add Shop / Vendor Configuration
This commit is contained in:
@@ -1,9 +1,8 @@
|
|||||||
import { useQuery } from "@apollo/client";
|
import { useQuery } from "@apollo/client";
|
||||||
import { Card, Divider, Drawer, Grid } from "antd";
|
import { Card, Divider, Drawer, Grid } from "antd";
|
||||||
import queryString from "query-string";
|
import queryString from "query-string";
|
||||||
import React from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Link, useNavigate, useLocation } from "react-router-dom";
|
import { Link, useLocation, useNavigate } from "react-router-dom";
|
||||||
import { QUERY_PARTS_QUEUE_CARD_DETAILS } from "../../graphql/jobs.queries";
|
import { QUERY_PARTS_QUEUE_CARD_DETAILS } from "../../graphql/jobs.queries";
|
||||||
import AlertComponent from "../alert/alert.component";
|
import AlertComponent from "../alert/alert.component";
|
||||||
import JobsDetailHeader from "../jobs-detail-header/jobs-detail-header.component";
|
import JobsDetailHeader from "../jobs-detail-header/jobs-detail-header.component";
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
import { Form, Input, InputNumber, Select } from "antd";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
import PhoneFormItem, { PhoneItemFormatterValidation } from "../form-items-formatted/phone-form-item.component";
|
||||||
|
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
|
const timeZonesList = Intl.supportedValuesOf("timeZone");
|
||||||
|
|
||||||
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
bodyshop: selectBodyshop
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = () => ({});
|
||||||
|
|
||||||
|
export function PartsBusinessInfoComponent({ form }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<LayoutFormRow header={t("bodyshop.labels.businessinformation")} id="businessinformation">
|
||||||
|
<Form.Item
|
||||||
|
label={t("bodyshop.fields.shopname")}
|
||||||
|
name="shopname"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("bodyshop.fields.address1")}
|
||||||
|
name="address1"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={t("bodyshop.fields.address2")} name="address2">
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("bodyshop.fields.city")}
|
||||||
|
name="city"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("bodyshop.fields.state")}
|
||||||
|
name="state"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={t("bodyshop.fields.zip_post")} name="zip_post">
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={t("bodyshop.fields.country")} name="country">
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={t("bodyshop.fields.email")} name="email">
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("bodyshop.fields.phone")}
|
||||||
|
name="phone"
|
||||||
|
rules={[({ getFieldValue }) => PhoneItemFormatterValidation(getFieldValue, "phone")]}
|
||||||
|
>
|
||||||
|
<PhoneFormItem />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={t("bodyshop.fields.website")} name="website">
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("bodyshop.fields.timezone")}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
name="timezone"
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
showSearch
|
||||||
|
options={timeZonesList.map((z) => {
|
||||||
|
return { label: z, value: z };
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={t("bodyshop.fields.insurance_vendor_id")} name="insurance_vendor_id">
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={t("bodyshop.fields.logo_img_path")} name={["logo_img_path", "src"]}>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={t("bodyshop.fields.logo_img_path_height")} name={["logo_img_path", "height"]}>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={t("bodyshop.fields.logo_img_path_width")} name={["logo_img_path", "width"]}>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={t("bodyshop.fields.logo_img_header_margin")} name={["logo_img_path", "headerMargin"]}>
|
||||||
|
<InputNumber min={0} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={t("bodyshop.fields.logo_img_footer_margin")} name={["logo_img_path", "footerMargin"]}>
|
||||||
|
<InputNumber min={0} />
|
||||||
|
</Form.Item>
|
||||||
|
</LayoutFormRow>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(PartsBusinessInfoComponent);
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { DeleteFilled } from "@ant-design/icons";
|
||||||
|
import { Button, Form, Input, Select, Space } from "antd";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
|
||||||
|
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||||
|
|
||||||
|
export default function PartsEmailPresetsComponent({ form }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<LayoutFormRow grow header={t("bodyshop.labels.md_to_emails")} id="md_to_emails">
|
||||||
|
<Form.List name={["md_to_emails"]}>
|
||||||
|
{(fields, { add, remove, move }) => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{fields.map((field, index) => (
|
||||||
|
<Form.Item key={field.key}>
|
||||||
|
<LayoutFormRow noDivider>
|
||||||
|
<Form.Item label={t("general.labels.label")} key={`${index}label`} name={[field.name, "label"]}>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("bodyshop.labels.md_to_emails_emails")}
|
||||||
|
key={`${index}emails`}
|
||||||
|
name={[field.name, "emails"]}
|
||||||
|
>
|
||||||
|
<Select mode="tags" tokenSeparators={[",", ";"]} />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Space>
|
||||||
|
<DeleteFilled
|
||||||
|
onClick={() => {
|
||||||
|
remove(field.name);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<FormListMoveArrows move={move} index={index} total={fields.length} />
|
||||||
|
</Space>
|
||||||
|
</LayoutFormRow>
|
||||||
|
</Form.Item>
|
||||||
|
))}
|
||||||
|
<Form.Item>
|
||||||
|
<Button
|
||||||
|
type="dashed"
|
||||||
|
onClick={() => {
|
||||||
|
add();
|
||||||
|
}}
|
||||||
|
style={{ width: "100%" }}
|
||||||
|
>
|
||||||
|
{t("general.actions.add")}
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Form.List>
|
||||||
|
</LayoutFormRow>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import { DeleteFilled } from "@ant-design/icons";
|
||||||
|
import { Button, Form, Input, Space } from "antd";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
|
||||||
|
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||||
|
|
||||||
|
export default function PartsLocationsComponent({ form }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<LayoutFormRow grow header={t("bodyshop.labels.partslocations")} id="partslocations">
|
||||||
|
<Form.List name={["md_parts_locations"]}>
|
||||||
|
{(fields, { add, remove, move }) => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{fields.map((field, index) => (
|
||||||
|
<Form.Item key={field.key}>
|
||||||
|
<LayoutFormRow noDivider>
|
||||||
|
<Form.Item
|
||||||
|
className="imex-flex-row__margin"
|
||||||
|
label={t("bodyshop.fields.partslocation")}
|
||||||
|
key={`${index}`}
|
||||||
|
name={[field.name]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Space wrap>
|
||||||
|
<DeleteFilled
|
||||||
|
className="imex-flex-row__margin"
|
||||||
|
onClick={() => {
|
||||||
|
remove(field.name);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<FormListMoveArrows move={move} index={index} total={fields.length} />
|
||||||
|
</Space>
|
||||||
|
</LayoutFormRow>
|
||||||
|
</Form.Item>
|
||||||
|
))}
|
||||||
|
<Form.Item>
|
||||||
|
<Button
|
||||||
|
type="dashed"
|
||||||
|
onClick={() => {
|
||||||
|
add();
|
||||||
|
}}
|
||||||
|
style={{ width: "100%" }}
|
||||||
|
>
|
||||||
|
{t("bodyshop.actions.addpartslocation")}
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Form.List>
|
||||||
|
</LayoutFormRow>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import { DeleteFilled } from "@ant-design/icons";
|
||||||
|
import { Button, Form, Input, Space } from "antd";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
|
||||||
|
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||||
|
|
||||||
|
export default function PartsOrderCommentsComponent({ form }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<LayoutFormRow grow header={t("bodyshop.fields.md_parts_order_comment")} id="md_parts_order_comment">
|
||||||
|
<Form.List name={["md_parts_order_comment"]}>
|
||||||
|
{(fields, { add, remove, move }) => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{fields.map((field, index) => (
|
||||||
|
<Form.Item key={field.key}>
|
||||||
|
<LayoutFormRow noDivider>
|
||||||
|
<Form.Item
|
||||||
|
label={t("general.labels.label")}
|
||||||
|
key={`${index}label`}
|
||||||
|
name={[field.name, "label"]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("parts_orders.fields.comments")}
|
||||||
|
key={`${index}comment`}
|
||||||
|
name={[field.name, "comment"]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input.TextArea autoSize />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Space wrap>
|
||||||
|
<DeleteFilled
|
||||||
|
onClick={() => {
|
||||||
|
remove(field.name);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<FormListMoveArrows move={move} index={index} total={fields.length} />
|
||||||
|
</Space>
|
||||||
|
</LayoutFormRow>
|
||||||
|
</Form.Item>
|
||||||
|
))}
|
||||||
|
<Form.Item>
|
||||||
|
<Button
|
||||||
|
type="dashed"
|
||||||
|
onClick={() => {
|
||||||
|
add();
|
||||||
|
}}
|
||||||
|
style={{ width: "100%" }}
|
||||||
|
>
|
||||||
|
{t("general.actions.add")}
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Form.List>
|
||||||
|
</LayoutFormRow>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { Button, Card, Divider, Form, Input, Space } from "antd";
|
||||||
|
import { DeleteOutlined, PlusOutlined } from "@ant-design/icons";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
const { TextArea } = Input;
|
||||||
|
|
||||||
|
export default function PartsOrdersCommentsComponent({ form }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card title={t("bodyshop.labels.parts_orders_comments")}>
|
||||||
|
<Form.List name="parts_orders_comments">
|
||||||
|
{(fields, { add, remove }) => (
|
||||||
|
<>
|
||||||
|
{fields.map(({ key, name, ...restField }) => (
|
||||||
|
<Space key={key} style={{ display: "flex", marginBottom: 8 }} align="baseline">
|
||||||
|
<Form.Item
|
||||||
|
{...restField}
|
||||||
|
name={[name, "comment_type"]}
|
||||||
|
label={t("bodyshop.labels.comment_type")}
|
||||||
|
rules={[{ required: true, message: t("bodyshop.errors.comment_type_required") }]}
|
||||||
|
>
|
||||||
|
<Input placeholder={t("bodyshop.placeholders.comment_type")} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
{...restField}
|
||||||
|
name={[name, "default_comment"]}
|
||||||
|
label={t("bodyshop.labels.default_comment")}
|
||||||
|
rules={[{ required: true, message: t("bodyshop.errors.default_comment_required") }]}
|
||||||
|
>
|
||||||
|
<TextArea
|
||||||
|
placeholder={t("bodyshop.placeholders.default_comment")}
|
||||||
|
rows={3}
|
||||||
|
maxLength={500}
|
||||||
|
showCount
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
{...restField}
|
||||||
|
name={[name, "is_active"]}
|
||||||
|
label={t("bodyshop.labels.is_active")}
|
||||||
|
valuePropName="checked"
|
||||||
|
initialValue={true}
|
||||||
|
>
|
||||||
|
<Input type="checkbox" />
|
||||||
|
</Form.Item>
|
||||||
|
<Button type="text" danger icon={<DeleteOutlined />} onClick={() => remove(name)} />
|
||||||
|
</Space>
|
||||||
|
))}
|
||||||
|
<Divider />
|
||||||
|
<Form.Item>
|
||||||
|
<Button type="dashed" onClick={() => add()} block icon={<PlusOutlined />}>
|
||||||
|
{t("bodyshop.actions.add_parts_comment")}
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Form.List>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import { Button, Card, Divider, Form, Input, Select, Space } from "antd";
|
||||||
|
import { DeleteOutlined, PlusOutlined } from "@ant-design/icons";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
const { Option } = Select;
|
||||||
|
|
||||||
|
export default function PartsShopInfoEmailPresets({ form }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const emailTypes = [
|
||||||
|
{ value: "parts_order", label: t("bodyshop.labels.parts_order") },
|
||||||
|
{ value: "parts_receipt", label: t("bodyshop.labels.parts_receipt") },
|
||||||
|
{ value: "parts_notification", label: t("bodyshop.labels.parts_notification") }
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card title={t("bodyshop.labels.preset_to_emails")}>
|
||||||
|
<Form.List name="email_presets">
|
||||||
|
{(fields, { add, remove }) => (
|
||||||
|
<>
|
||||||
|
{fields.map(({ key, name, ...restField }) => (
|
||||||
|
<Space key={key} style={{ display: "flex", marginBottom: 8 }} align="baseline">
|
||||||
|
<Form.Item
|
||||||
|
{...restField}
|
||||||
|
name={[name, "email_type"]}
|
||||||
|
label={t("bodyshop.labels.email_type")}
|
||||||
|
rules={[{ required: true, message: t("bodyshop.errors.email_type_required") }]}
|
||||||
|
>
|
||||||
|
<Select placeholder={t("bodyshop.placeholders.select_email_type")}>
|
||||||
|
{emailTypes.map((type) => (
|
||||||
|
<Option key={type.value} value={type.value}>
|
||||||
|
{type.label}
|
||||||
|
</Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
{...restField}
|
||||||
|
name={[name, "to_emails"]}
|
||||||
|
label={t("bodyshop.labels.to_emails")}
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: t("bodyshop.errors.to_emails_required") },
|
||||||
|
{ type: "email", message: t("bodyshop.errors.invalid_email") }
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input placeholder={t("bodyshop.placeholders.to_emails")} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
{...restField}
|
||||||
|
name={[name, "cc_emails"]}
|
||||||
|
label={t("bodyshop.labels.cc_emails")}
|
||||||
|
rules={[{ type: "email", message: t("bodyshop.errors.invalid_email") }]}
|
||||||
|
>
|
||||||
|
<Input placeholder={t("bodyshop.placeholders.cc_emails")} />
|
||||||
|
</Form.Item>
|
||||||
|
<Button type="text" danger icon={<DeleteOutlined />} onClick={() => remove(name)} />
|
||||||
|
</Space>
|
||||||
|
))}
|
||||||
|
<Divider />
|
||||||
|
<Form.Item>
|
||||||
|
<Button type="dashed" onClick={() => add()} block icon={<PlusOutlined />}>
|
||||||
|
{t("bodyshop.actions.add_email_preset")}
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Form.List>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import { Button, Card, Tabs } from "antd";
|
||||||
|
import queryString from "query-string";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { useLocation, useNavigate } from "react-router-dom";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
import ShopInfoGeneral from "../shop-info/shop-info.general.component";
|
||||||
|
import ShopInfoResponsibilityCenterComponent from "../shop-info/shop-info.responsibilitycenters.component";
|
||||||
|
import ShopInfoOrderStatusComponent from "../shop-info/shop-info.orderstatus.component";
|
||||||
|
import ShopInfoNotificationsAutoadd from "../shop-info/shop-info.notifications-autoadd.component";
|
||||||
|
|
||||||
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
bodyshop: selectBodyshop
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = () => ({});
|
||||||
|
|
||||||
|
export function PartsShopInfoComponent({ bodyshop, form, saveLoading }) {
|
||||||
|
const { scenarioNotificationsOn } = useSocket();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const history = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
|
const search = queryString.parse(location.search);
|
||||||
|
|
||||||
|
// Only include the 4 specific sections requested for parts settings
|
||||||
|
const tabItems = [
|
||||||
|
{
|
||||||
|
key: "general",
|
||||||
|
label: t("bodyshop.labels.shopinfo"), // Business Information
|
||||||
|
children: <ShopInfoGeneral form={form} />,
|
||||||
|
id: "tab-parts-general"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "responsibilityCenters",
|
||||||
|
label: t("bodyshop.labels.responsibilitycenters.title"), // Parts Locations
|
||||||
|
children: <ShopInfoResponsibilityCenterComponent form={form} />,
|
||||||
|
id: "tab-parts-responsibilitycenters"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "orderStatus",
|
||||||
|
label: t("bodyshop.labels.orderstatuses"), // Parts Orders Comments
|
||||||
|
children: <ShopInfoOrderStatusComponent form={form} />,
|
||||||
|
id: "tab-parts-orderstatus"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// Only add notifications tab if scenario notifications are enabled (Preset To Emails)
|
||||||
|
if (scenarioNotificationsOn) {
|
||||||
|
tabItems.push({
|
||||||
|
key: "notifications_autoadd",
|
||||||
|
label: t("bodyshop.labels.notifications.followers"), // Preset To Emails
|
||||||
|
children: <ShopInfoNotificationsAutoadd form={form} bodyshop={bodyshop} />,
|
||||||
|
id: "tab-parts-notifications"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
extra={
|
||||||
|
<Button type="primary" loading={saveLoading} onClick={() => form.submit()} id="parts-shop-info-save-button">
|
||||||
|
{t("general.actions.save")}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Tabs
|
||||||
|
defaultActiveKey={search.subtab || "general"}
|
||||||
|
onChange={(key) =>
|
||||||
|
history({
|
||||||
|
search: `?tab=${search.tab}&subtab=${key}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
items={tabItems}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(PartsShopInfoComponent);
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { Button, Card, Divider, Form, Input, Space } from "antd";
|
||||||
|
import { DeleteOutlined, PlusOutlined } from "@ant-design/icons";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
export default function PartsShopInfoLocations({ form }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card title={t("bodyshop.labels.parts_locations")}>
|
||||||
|
<Form.List name="parts_locations">
|
||||||
|
{(fields, { add, remove }) => (
|
||||||
|
<>
|
||||||
|
{fields.map(({ key, name, ...restField }) => (
|
||||||
|
<Space key={key} style={{ display: "flex", marginBottom: 8 }} align="baseline">
|
||||||
|
<Form.Item
|
||||||
|
{...restField}
|
||||||
|
name={[name, "location_name"]}
|
||||||
|
label={t("bodyshop.labels.location_name")}
|
||||||
|
rules={[{ required: true, message: t("bodyshop.errors.location_name_required") }]}
|
||||||
|
>
|
||||||
|
<Input placeholder={t("bodyshop.placeholders.location_name")} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
{...restField}
|
||||||
|
name={[name, "location_code"]}
|
||||||
|
label={t("bodyshop.labels.location_code")}
|
||||||
|
rules={[{ required: true, message: t("bodyshop.errors.location_code_required") }]}
|
||||||
|
>
|
||||||
|
<Input placeholder={t("bodyshop.placeholders.location_code")} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item {...restField} name={[name, "description"]} label={t("bodyshop.labels.description")}>
|
||||||
|
<Input placeholder={t("bodyshop.placeholders.location_description")} />
|
||||||
|
</Form.Item>
|
||||||
|
<Button type="text" danger icon={<DeleteOutlined />} onClick={() => remove(name)} />
|
||||||
|
</Space>
|
||||||
|
))}
|
||||||
|
<Divider />
|
||||||
|
<Form.Item>
|
||||||
|
<Button type="dashed" onClick={() => add()} block icon={<PlusOutlined />}>
|
||||||
|
{t("bodyshop.actions.add_location")}
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Form.List>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import { Button, Card, Tabs } from "antd";
|
||||||
|
import queryString from "query-string";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { useLocation, useNavigate } from "react-router-dom";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
import ShopInfoGeneral from "../shop-info/shop-info.general.component";
|
||||||
|
import ShopInfoResponsibilityCenterComponent from "../shop-info/shop-info.responsibilitycenters.component";
|
||||||
|
import ShopInfoOrderStatusComponent from "../shop-info/shop-info.orderstatus.component";
|
||||||
|
import ShopInfoNotificationsAutoadd from "../shop-info/shop-info.notifications-autoadd.component";
|
||||||
|
|
||||||
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
bodyshop: selectBodyshop
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = () => ({});
|
||||||
|
|
||||||
|
export function PartsShopInfoComponent({ bodyshop, form, saveLoading }) {
|
||||||
|
const { scenarioNotificationsOn } = useSocket();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const history = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
|
const search = queryString.parse(location.search);
|
||||||
|
|
||||||
|
const tabItems = [
|
||||||
|
{
|
||||||
|
key: "general",
|
||||||
|
label: t("bodyshop.labels.shopinfo"),
|
||||||
|
children: <ShopInfoGeneral form={form} />,
|
||||||
|
id: "tab-parts-general"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "responsibilityCenters",
|
||||||
|
label: t("bodyshop.labels.responsibilitycenters.title"),
|
||||||
|
children: <ShopInfoResponsibilityCenterComponent form={form} />,
|
||||||
|
id: "tab-parts-responsibilitycenters"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "orderStatus",
|
||||||
|
label: t("bodyshop.labels.orderstatuses"),
|
||||||
|
children: <ShopInfoOrderStatusComponent form={form} />,
|
||||||
|
id: "tab-parts-orderstatus"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// Only add notifications tab if scenario notifications are enabled (same condition as full shop settings)
|
||||||
|
if (scenarioNotificationsOn) {
|
||||||
|
tabItems.push({
|
||||||
|
key: "notifications_autoadd",
|
||||||
|
label: t("bodyshop.labels.notifications.followers"),
|
||||||
|
children: <ShopInfoNotificationsAutoadd form={form} bodyshop={bodyshop} />,
|
||||||
|
id: "tab-parts-notifications"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
extra={
|
||||||
|
<Button type="primary" loading={saveLoading} onClick={() => form.submit()} id="parts-shop-info-save-button">
|
||||||
|
{t("general.actions.save")}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Tabs
|
||||||
|
defaultActiveKey={search.subtab || "general"}
|
||||||
|
onChange={(key) =>
|
||||||
|
history({
|
||||||
|
search: `?tab=${search.tab}&subtab=${key}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
items={tabItems}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(PartsShopInfoComponent);
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import { useMutation, useQuery } from "@apollo/client";
|
||||||
|
import { Form } from "antd";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||||
|
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
|
import { QUERY_BODYSHOP, UPDATE_SHOP } from "../../graphql/bodyshop.queries";
|
||||||
|
import dayjs from "../../utils/day";
|
||||||
|
import AlertComponent from "../alert/alert.component";
|
||||||
|
import FormsFieldChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
|
||||||
|
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
||||||
|
import PartsShopManagementComponent from "./parts-shop-management.component";
|
||||||
|
|
||||||
|
export default function PartsShopInfoContainer() {
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [saveLoading, setSaveLoading] = useState(false);
|
||||||
|
const [updateBodyshop] = useMutation(UPDATE_SHOP);
|
||||||
|
const { loading, error, data, refetch } = useQuery(QUERY_BODYSHOP, {
|
||||||
|
fetchPolicy: "network-only",
|
||||||
|
nextFetchPolicy: "network-only"
|
||||||
|
});
|
||||||
|
const notification = useNotification();
|
||||||
|
|
||||||
|
const handleFinish = (values) => {
|
||||||
|
setSaveLoading(true);
|
||||||
|
logImEXEvent("parts_shop_update");
|
||||||
|
|
||||||
|
updateBodyshop({
|
||||||
|
variables: { id: data.bodyshops[0].id, shop: values }
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
notification["success"]({ message: t("bodyshop.successes.save") });
|
||||||
|
refetch().then(() => form.resetFields());
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
notification["error"]({
|
||||||
|
message: t("bodyshop.errors.saving", { message: error })
|
||||||
|
});
|
||||||
|
});
|
||||||
|
setSaveLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data) form.resetFields();
|
||||||
|
}, [form, data]);
|
||||||
|
|
||||||
|
if (error) return <AlertComponent message={error.message} type="error" />;
|
||||||
|
if (loading) return <LoadingSpinner />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form
|
||||||
|
form={form}
|
||||||
|
layout="vertical"
|
||||||
|
autoComplete="new-password"
|
||||||
|
onFinish={handleFinish}
|
||||||
|
initialValues={
|
||||||
|
data
|
||||||
|
? {
|
||||||
|
...data.bodyshops[0],
|
||||||
|
schedule_start_time: dayjs(data.bodyshops[0].schedule_start_time),
|
||||||
|
schedule_end_time: dayjs(data.bodyshops[0].schedule_end_time)
|
||||||
|
}
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FormsFieldChanged form={form} />
|
||||||
|
<PartsShopManagementComponent form={form} saveLoading={saveLoading} />
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import { Button, Card, Divider, Space } from "antd";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
import PartsBusinessInfoComponent from "./parts-business-info.component";
|
||||||
|
import PartsLocationsComponent from "./parts-locations.component";
|
||||||
|
import PartsOrderCommentsComponent from "./parts-order-comments.component";
|
||||||
|
import PartsEmailPresetsComponent from "./parts-email-presets.component";
|
||||||
|
|
||||||
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
bodyshop: selectBodyshop
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = () => ({});
|
||||||
|
|
||||||
|
export function PartsShopManagementComponent({ bodyshop, form, saveLoading }) {
|
||||||
|
const { scenarioNotificationsOn } = useSocket();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
extra={
|
||||||
|
<Button type="primary" loading={saveLoading} onClick={() => form.submit()} id="parts-shop-save-button">
|
||||||
|
{t("general.actions.save")}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Space direction="vertical" size="large" style={{ width: "100%" }}>
|
||||||
|
{/* Business Information Section - Limited to basic shop info only */}
|
||||||
|
<PartsBusinessInfoComponent form={form} />
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
{/* Parts Locations Section */}
|
||||||
|
<PartsLocationsComponent form={form} />
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
{/* Parts Orders Comments Section */}
|
||||||
|
<PartsOrderCommentsComponent form={form} />
|
||||||
|
|
||||||
|
{/* Preset To Emails Section - only show if notifications are enabled */}
|
||||||
|
{scenarioNotificationsOn && (
|
||||||
|
<>
|
||||||
|
<Divider />
|
||||||
|
<PartsEmailPresetsComponent form={form} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(PartsShopManagementComponent);
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { SyncOutlined } from "@ant-design/icons";
|
import { SettingOutlined, SyncOutlined } from "@ant-design/icons";
|
||||||
import { Button, Card, Input, Space, Table, Typography } from "antd";
|
import { Button, Card, Input, Space, Table, Typography } from "antd";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
@@ -15,6 +15,7 @@ import { alphaSort, statusSort } from "../../utils/sorters";
|
|||||||
import useLocalStorage from "../../utils/useLocalStorage";
|
import useLocalStorage from "../../utils/useLocalStorage";
|
||||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||||
import JobPartsQueueCount from "../job-parts-queue-count/job-parts-queue-count.component";
|
import JobPartsQueueCount from "../job-parts-queue-count/job-parts-queue-count.component";
|
||||||
|
import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
//currentUser: selectCurrentUser
|
//currentUser: selectCurrentUser
|
||||||
@@ -192,6 +193,15 @@ export function SimplifiedPartsJobsListComponent({ bodyshop, refetch, loading, j
|
|||||||
<Card
|
<Card
|
||||||
extra={
|
extra={
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
|
<RbacWrapper action="shop:config">
|
||||||
|
<Button
|
||||||
|
icon={<SettingOutlined />}
|
||||||
|
onClick={() => history("/parts/parts-settings")}
|
||||||
|
title={t("general.labels.settings")}
|
||||||
|
>
|
||||||
|
{t("general.labels.settings")}
|
||||||
|
</Button>
|
||||||
|
</RbacWrapper>
|
||||||
{search.search && (
|
{search.search && (
|
||||||
<>
|
<>
|
||||||
<Typography.Title level={4}>
|
<Typography.Title level={4}>
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import { Tabs } from "antd";
|
||||||
|
import queryString from "query-string";
|
||||||
|
import React, { useEffect } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { useLocation, useNavigate } from "react-router-dom";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component";
|
||||||
|
import ShopVendorPageComponent from "../shop-vendor/shop-vendor.page.component";
|
||||||
|
import { setBreadcrumbs, setSelectedHeader } from "../../redux/application/application.actions";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||||
|
import PartsShopInfoContainer from "../../components/parts-shop-info/parts-shop-info.container";
|
||||||
|
|
||||||
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
bodyshop: selectBodyshop
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
setSelectedHeader: (key) => dispatch(setSelectedHeader(key)),
|
||||||
|
setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs))
|
||||||
|
});
|
||||||
|
|
||||||
|
export function PartsSettingsPage({ bodyshop, setSelectedHeader, setBreadcrumbs }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const history = useNavigate();
|
||||||
|
const search = queryString.parse(useLocation().search);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.title = t("titles.parts_settings", {
|
||||||
|
app: InstanceRenderManager({
|
||||||
|
imex: "$t(titles.imexonline)",
|
||||||
|
rome: "$t(titles.romeonline)"
|
||||||
|
})
|
||||||
|
});
|
||||||
|
setSelectedHeader("parts-settings");
|
||||||
|
setBreadcrumbs([
|
||||||
|
{
|
||||||
|
link: "/parts",
|
||||||
|
label: t("titles.bc.parts")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
link: "/parts/settings",
|
||||||
|
label: t("titles.bc.parts_settings")
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
}, [t, setSelectedHeader, setBreadcrumbs]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!search.tab) history({ search: "?tab=shop" });
|
||||||
|
}, [history, search]);
|
||||||
|
|
||||||
|
const items = [
|
||||||
|
{
|
||||||
|
key: "shop",
|
||||||
|
label: t("bodyshop.labels.shop_management"),
|
||||||
|
children: (
|
||||||
|
<RbacWrapper action="shop:config">
|
||||||
|
<PartsShopInfoContainer />
|
||||||
|
</RbacWrapper>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "vendors",
|
||||||
|
label: t("bodyshop.labels.vendor_management"),
|
||||||
|
children: (
|
||||||
|
<RbacWrapper action="shop:vendors">
|
||||||
|
<ShopVendorPageComponent />
|
||||||
|
</RbacWrapper>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return <Tabs activeKey={search.tab} onChange={(key) => history({ search: `?tab=${key}` })} items={items} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(PartsSettingsPage);
|
||||||
@@ -22,6 +22,7 @@ const SimplifiedPartsJobsPage = lazy(() => import("../simplified-parts-jobs/simp
|
|||||||
const SimplifiedPartsJobsDetailPage = lazy(
|
const SimplifiedPartsJobsDetailPage = lazy(
|
||||||
() => import("../simplified-parts-jobs-detail/simplified-parts-jobs-detail.container.jsx")
|
() => import("../simplified-parts-jobs-detail/simplified-parts-jobs-detail.container.jsx")
|
||||||
);
|
);
|
||||||
|
const PartsSettingsPage = lazy(() => import("../parts-settings/parts-settings.page.component.jsx"));
|
||||||
const ShopPage = lazy(() => import("../shop/shop.page.component.jsx"));
|
const ShopPage = lazy(() => import("../shop/shop.page.component.jsx"));
|
||||||
const ShopVendorPageContainer = lazy(() => import("../shop-vendor/shop-vendor.page.container.jsx"));
|
const ShopVendorPageContainer = lazy(() => import("../shop-vendor/shop-vendor.page.container.jsx"));
|
||||||
const EmailOverlayContainer = lazy(() => import("../../components/email-overlay/email-overlay.container.jsx"));
|
const EmailOverlayContainer = lazy(() => import("../../components/email-overlay/email-overlay.container.jsx"));
|
||||||
@@ -183,6 +184,14 @@ export function SimplifiedPartsPage({ conflict, bodyshop, alerts, setAlerts }) {
|
|||||||
</Suspense>
|
</Suspense>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path="/parts-settings"
|
||||||
|
element={
|
||||||
|
<Suspense fallback={<Spin />}>
|
||||||
|
<PartsSettingsPage />
|
||||||
|
</Suspense>
|
||||||
|
}
|
||||||
|
/>
|
||||||
</Routes>
|
</Routes>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user