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 { Card, Divider, Drawer, Grid } from "antd";
|
||||
import queryString from "query-string";
|
||||
import React from "react";
|
||||
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 AlertComponent from "../alert/alert.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 axios from "axios";
|
||||
import _ from "lodash";
|
||||
@@ -15,6 +15,7 @@ import { alphaSort, statusSort } from "../../utils/sorters";
|
||||
import useLocalStorage from "../../utils/useLocalStorage";
|
||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||
import JobPartsQueueCount from "../job-parts-queue-count/job-parts-queue-count.component";
|
||||
import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
@@ -192,6 +193,15 @@ export function SimplifiedPartsJobsListComponent({ bodyshop, refetch, loading, j
|
||||
<Card
|
||||
extra={
|
||||
<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 && (
|
||||
<>
|
||||
<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(
|
||||
() => 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 ShopVendorPageContainer = lazy(() => import("../shop-vendor/shop-vendor.page.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>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/parts-settings"
|
||||
element={
|
||||
<Suspense fallback={<Spin />}>
|
||||
<PartsSettingsPage />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</Suspense>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user