Compare commits
15 Commits
feature/IO
...
feature/IO
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b0dcd3618e | ||
|
|
e015d3574a | ||
|
|
60140902d4 | ||
|
|
84f41b2c11 | ||
|
|
282fa787a9 | ||
|
|
e26eb17d09 | ||
|
|
fbea9fde27 | ||
|
|
ce7cf6bdbe | ||
|
|
a6f809b20a | ||
|
|
6b1b393804 | ||
|
|
9eb77964db | ||
|
|
0a68d2791d | ||
|
|
11928d9a7e | ||
|
|
5237b1d535 | ||
|
|
3691d32aaa |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -128,3 +128,5 @@ vitest-coverage/
|
||||
*.vitest.log
|
||||
test-output.txt
|
||||
server/job/test/fixtures
|
||||
|
||||
.github
|
||||
|
||||
@@ -142,11 +142,10 @@ export function App({ bodyshop, checkUserSession, currentUser, online, setOnline
|
||||
>
|
||||
<ProductFruitsWrapper
|
||||
currentUser={currentUser}
|
||||
workspaceCode={InstanceRenderMgr({
|
||||
imex: null,
|
||||
rome: "9BkbEseqNqxw8jUH"
|
||||
})}
|
||||
bodyshop={bodyshop}
|
||||
workspaceCode={bodyshop?.tours_enabled ? "9BkbEseqNqxw8jUH" : ""}
|
||||
/>
|
||||
|
||||
<NotificationProvider>
|
||||
<Routes>
|
||||
<Route
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
import React from "react";
|
||||
import { ProductFruits } from "react-product-fruits";
|
||||
import PropTypes from "prop-types";
|
||||
import { ProductFruits } from "react-product-fruits";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
const ProductFruitsWrapper = React.memo(({ currentUser, bodyshop, workspaceCode }) => {
|
||||
const featureProps = bodyshop?.features
|
||||
? Object.entries(bodyshop.features).reduce((acc, [key, value]) => {
|
||||
acc[key] = value === true || (typeof value === "string" && dayjs(value).isAfter(dayjs()));
|
||||
return acc;
|
||||
}, {})
|
||||
: {};
|
||||
|
||||
const ProductFruitsWrapper = React.memo(({ currentUser, workspaceCode }) => {
|
||||
return (
|
||||
workspaceCode &&
|
||||
currentUser?.authorized === true &&
|
||||
@@ -14,7 +22,8 @@ const ProductFruitsWrapper = React.memo(({ currentUser, workspaceCode }) => {
|
||||
language="en"
|
||||
user={{
|
||||
email: currentUser.email,
|
||||
username: currentUser.email
|
||||
username: currentUser.email,
|
||||
props: featureProps
|
||||
}}
|
||||
/>
|
||||
)
|
||||
@@ -28,5 +37,6 @@ ProductFruitsWrapper.propTypes = {
|
||||
authorized: PropTypes.bool,
|
||||
email: PropTypes.string
|
||||
}),
|
||||
workspaceCode: PropTypes.string
|
||||
workspaceCode: PropTypes.string,
|
||||
bodyshop: PropTypes.object
|
||||
};
|
||||
|
||||
@@ -20,6 +20,7 @@ function FeatureWrapper({
|
||||
children,
|
||||
upsellComponent,
|
||||
bypass,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
...restProps
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
@@ -78,7 +79,11 @@ export function HasFeatureAccess({ featureName, bodyshop, bypass, debug = false
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return bodyshop?.features?.allAccess || dayjs(bodyshop?.features[featureName]).isAfter(dayjs());
|
||||
return (
|
||||
bodyshop?.features?.allAccess ||
|
||||
bodyshop?.features?.[featureName] ||
|
||||
dayjs(bodyshop?.features[featureName]).isAfter(dayjs())
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, null)(FeatureWrapper);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { AlertFilled } from "@ant-design/icons";
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { useLazyQuery, useMutation } from "@apollo/client";
|
||||
import { Button, Divider, Dropdown, Form, Input, Popover, Select, Space } from "antd";
|
||||
import parsePhoneNumber from "libphonenumber-js";
|
||||
import queryString from "query-string";
|
||||
@@ -8,24 +8,30 @@ import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { Link, useLocation, useNavigate } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||
import { UPDATE_APPOINTMENT } from "../../graphql/appointments.queries";
|
||||
import { GET_JOB_BY_PK_QUICK_INTAKE, JOB_PRODUCTION_TOGGLE } from "../../graphql/jobs.queries";
|
||||
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||
import { openChatByPhone, setMessage } from "../../redux/messaging/messaging.actions";
|
||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import { DateTimeFormatterFunction } from "../../utils/DateFormatter";
|
||||
import dayjs from "../../utils/day";
|
||||
import { GenerateDocument } from "../../utils/RenderTemplate";
|
||||
import { TemplateList } from "../../utils/TemplateConstants";
|
||||
import ChatOpenButton from "../chat-open-button/chat-open-button.component";
|
||||
import DataLabel from "../data-label/data-label.component";
|
||||
import FormDateTimePickerComponent from "../form-date-time-picker/form-date-time-picker.component";
|
||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||
import ProductionListColumnComment from "../production-list-columns/production-list-columns.comment.component";
|
||||
import ScheduleManualEvent from "../schedule-manual-event/schedule-manual-event.component";
|
||||
import { HasFeatureAccess } from "./../feature-wrapper/feature-wrapper.component";
|
||||
import ScheduleAtChange from "./job-at-change.component";
|
||||
import ScheduleEventColor from "./schedule-event.color.component";
|
||||
import ScheduleEventNote from "./schedule-event.note.component";
|
||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
@@ -33,7 +39,8 @@ const mapStateToProps = createStructuredSelector({
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setScheduleContext: (context) => dispatch(setModalContext({ context: context, modal: "schedule" })),
|
||||
openChatByPhone: (phone) => dispatch(openChatByPhone(phone)),
|
||||
setMessage: (text) => dispatch(setMessage(text))
|
||||
setMessage: (text) => dispatch(setMessage(text)),
|
||||
insertAuditTrail: ({ jobid, operation }) => dispatch(insertAuditTrail({ jobid, operation }))
|
||||
});
|
||||
|
||||
export function ScheduleEventComponent({
|
||||
@@ -43,16 +50,36 @@ export function ScheduleEventComponent({
|
||||
event,
|
||||
refetch,
|
||||
handleCancel,
|
||||
setScheduleContext
|
||||
setScheduleContext,
|
||||
insertAuditTrail
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = useState(false);
|
||||
const history = useNavigate();
|
||||
const searchParams = queryString.parse(useLocation().search);
|
||||
const [updateAppointment] = useMutation(UPDATE_APPOINTMENT);
|
||||
const [mutationUpdateJob] = useMutation(JOB_PRODUCTION_TOGGLE);
|
||||
const [title, setTitle] = useState(event.title);
|
||||
const { socket } = useSocket();
|
||||
const notification = useNotification();
|
||||
const [form] = Form.useForm();
|
||||
const [popOverVisible, setPopOverVisible] = useState(false);
|
||||
|
||||
const [getJobDetails] = useLazyQuery(GET_JOB_BY_PK_QUICK_INTAKE, {
|
||||
variables: { id: event.job.id },
|
||||
onCompleted: (data) => {
|
||||
if (data?.jobs_by_pk) {
|
||||
form.setFieldsValue({
|
||||
actual_in: data.jobs_by_pk.actual_in ? data.jobs_by_pk.actual_in : dayjs(),
|
||||
scheduled_completion: data.jobs_by_pk.scheduled_completion,
|
||||
actual_completion: data.jobs_by_pk.actual_completion,
|
||||
scheduled_delivery: data.jobs_by_pk.scheduled_delivery,
|
||||
actual_delivery: data.jobs_by_pk.actual_delivery
|
||||
});
|
||||
}
|
||||
},
|
||||
fetchPolicy: "network-only"
|
||||
});
|
||||
|
||||
const blockContent = (
|
||||
<Space direction="vertical" wrap>
|
||||
@@ -89,6 +116,74 @@ export function ScheduleEventComponent({
|
||||
</Space>
|
||||
);
|
||||
|
||||
const handleConvert = async (values) => {
|
||||
const res = await mutationUpdateJob({
|
||||
variables: {
|
||||
jobId: event.job.id,
|
||||
job: {
|
||||
...values,
|
||||
status: bodyshop.md_ro_statuses.default_arrived,
|
||||
inproduction: true
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!res.errors) {
|
||||
notification["success"]({
|
||||
message: t("jobs.successes.converted")
|
||||
});
|
||||
insertAuditTrail({
|
||||
jobid: event.job.id,
|
||||
operation: AuditTrailMapping.jobintake(
|
||||
res.data.update_jobs.returning[0].status,
|
||||
DateTimeFormatterFunction(values.scheduled_completion)
|
||||
)
|
||||
});
|
||||
setPopOverVisible(false);
|
||||
refetch();
|
||||
}
|
||||
};
|
||||
|
||||
const popMenu = (
|
||||
<div onClick={(e) => e.stopPropagation()}>
|
||||
<Form layout="vertical" form={form} onFinish={handleConvert}>
|
||||
<Form.Item
|
||||
name={["actual_in"]}
|
||||
label={t("jobs.fields.actual_in")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<FormDateTimePickerComponent disabled={event.ro_number} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["scheduled_completion"]}
|
||||
label={t("jobs.fields.scheduled_completion")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<FormDateTimePickerComponent disabled={event.ro_number} />
|
||||
</Form.Item>
|
||||
<Form.Item name={["scheduled_delivery"]} label={t("jobs.fields.scheduled_delivery")}>
|
||||
<FormDateTimePickerComponent disabled={event.ro_number} />
|
||||
</Form.Item>
|
||||
|
||||
<Space wrap>
|
||||
<Button type="primary" onClick={() => form.submit()}>
|
||||
{t("general.actions.save")}
|
||||
</Button>
|
||||
</Space>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
|
||||
const popoverContent = (
|
||||
<div style={{ maxWidth: "40vw" }}>
|
||||
{!event.isintake ? (
|
||||
@@ -294,7 +389,7 @@ export function ScheduleEventComponent({
|
||||
) : (
|
||||
<ScheduleManualEvent event={event} />
|
||||
)}
|
||||
{event.isintake ? (
|
||||
{event.isintake && HasFeatureAccess({ featureName: "checklist", bodyshop }) ? (
|
||||
<Link
|
||||
to={{
|
||||
pathname: `/manage/jobs/${event.job && event.job.id}/intake`,
|
||||
@@ -303,7 +398,21 @@ export function ScheduleEventComponent({
|
||||
>
|
||||
<Button disabled={event.arrived}>{t("appointments.actions.intake")}</Button>
|
||||
</Link>
|
||||
) : null}
|
||||
) : (
|
||||
<Popover //open={open}
|
||||
content={popMenu}
|
||||
open={popOverVisible}
|
||||
onOpenChange={setPopOverVisible}
|
||||
onClick={(e) => {
|
||||
getJobDetails();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
getPopupContainer={(trigger) => trigger.parentNode}
|
||||
trigger="click"
|
||||
>
|
||||
<Button disabled={event.arrived}>{t("jobs.actions.intake_quick")}</Button>
|
||||
</Popover>
|
||||
)}
|
||||
</Space>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -57,6 +57,7 @@ export const QUERY_BODYSHOP = gql`
|
||||
logo_img_path
|
||||
md_ro_statuses
|
||||
md_order_statuses
|
||||
tours_enabled
|
||||
md_functionality_toggles
|
||||
shopname
|
||||
state
|
||||
@@ -186,6 +187,7 @@ export const UPDATE_SHOP = gql`
|
||||
phone
|
||||
federal_tax_id
|
||||
id
|
||||
tours_enabled
|
||||
insurance_vendor_id
|
||||
logo_img_path
|
||||
md_ro_statuses
|
||||
|
||||
@@ -78,6 +78,9 @@ export const QUERY_PARTS_ORDER_OEC = gql`
|
||||
}
|
||||
ro_number
|
||||
clm_no
|
||||
cieca_stl
|
||||
cieca_ttl
|
||||
cieca_pfl
|
||||
asgn_no
|
||||
asgn_date
|
||||
state_tax_rate
|
||||
@@ -164,6 +167,7 @@ export const QUERY_PARTS_ORDER_OEC = gql`
|
||||
loss_desc
|
||||
loss_of_use
|
||||
loss_type
|
||||
materials
|
||||
ownr_addr1
|
||||
ownr_addr2
|
||||
ownr_city
|
||||
|
||||
@@ -15,8 +15,8 @@ const AuditTrailMapping = {
|
||||
jobchecklist: (type, inproduction, status) =>
|
||||
i18n.t("audit_trail.messages.jobchecklist", { type, inproduction, status }),
|
||||
jobconverted: (ro_number) => i18n.t("audit_trail.messages.jobconverted", { ro_number }),
|
||||
jobintake: (status, email, scheduled_completion) =>
|
||||
i18n.t("audit_trail.messages.jobintake", { status, email, scheduled_completion }),
|
||||
jobintake: (status, scheduled_completion) =>
|
||||
i18n.t("audit_trail.messages.jobintake", { status, scheduled_completion }),
|
||||
jobdelivery: (status, email, actual_completion) =>
|
||||
i18n.t("audit_trail.messages.jobdelivery", { status, email, actual_completion }),
|
||||
jobexported: () => i18n.t("audit_trail.messages.jobexported"),
|
||||
|
||||
@@ -965,6 +965,7 @@
|
||||
- insurance_vendor_id
|
||||
- intakechecklist
|
||||
- intellipay_config
|
||||
- intellipay_merchant_id
|
||||
- jc_hourly_rates
|
||||
- jobsizelimit
|
||||
- last_name_first
|
||||
@@ -1023,6 +1024,7 @@
|
||||
- template_header
|
||||
- textid
|
||||
- timezone
|
||||
- tours_enabled
|
||||
- tt_allow_post_to_invoiced
|
||||
- tt_enforce_hours_for_tech_console
|
||||
- updated_at
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
-- Could not auto-generate a down migration.
|
||||
-- Please write an appropriate down migration for the SQL below:
|
||||
-- alter table "public"."bodyshops" add column "tours_enabled" boolean
|
||||
-- not null default 'true';
|
||||
@@ -0,0 +1,2 @@
|
||||
alter table "public"."bodyshops" add column "tours_enabled" boolean
|
||||
not null default 'true';
|
||||
Reference in New Issue
Block a user