feature/IO-3255-simplified-parts-management - Add Shop / Vendor Configuration

This commit is contained in:
Dave Richer
2025-08-05 16:04:07 -04:00
parent 8a2dfae487
commit 0ed41de956
15 changed files with 889 additions and 3 deletions

View File

@@ -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";

View File

@@ -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);

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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);

View File

@@ -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>
);
}

View File

@@ -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);

View File

@@ -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>
);
}

View File

@@ -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);

View File

@@ -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}>

View File

@@ -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);

View File

@@ -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>
);