493 lines
17 KiB
JavaScript
493 lines
17 KiB
JavaScript
import { AlertFilled } from "@ant-design/icons";
|
|
import { useLazyQuery, useMutation } from "@apollo/client/react";
|
|
import { Button, Divider, Dropdown, Form, Input, Popover, Select, Space } from "antd";
|
|
import parsePhoneNumber from "libphonenumber-js";
|
|
import queryString from "query-string";
|
|
import { useState } from "react";
|
|
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";
|
|
|
|
const mapStateToProps = createStructuredSelector({
|
|
bodyshop: selectBodyshop
|
|
});
|
|
|
|
const mapDispatchToProps = (dispatch) => ({
|
|
setScheduleContext: (context) => dispatch(setModalContext({ context: context, modal: "schedule" })),
|
|
openChatByPhone: (phone) => dispatch(openChatByPhone(phone)),
|
|
setMessage: (text) => dispatch(setMessage(text)),
|
|
insertAuditTrail: ({ jobid, operation }) => dispatch(insertAuditTrail({ jobid, operation }))
|
|
});
|
|
|
|
export function ScheduleEventComponent({
|
|
bodyshop,
|
|
setMessage,
|
|
openChatByPhone,
|
|
event,
|
|
refetch,
|
|
handleCancel,
|
|
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, {
|
|
onCompleted: (data) => {
|
|
if (data?.jobs_by_pk) {
|
|
const totalHours =
|
|
(data.jobs_by_pk.labhrs?.aggregate?.sum?.mod_lb_hrs || 0) +
|
|
(data.jobs_by_pk.larhrs?.aggregate?.sum?.mod_lb_hrs || 0);
|
|
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
|
|
? data.jobs_by_pk.scheduled_completion
|
|
: totalHours && bodyshop.ss_configuration.nobusinessdays
|
|
? dayjs().businessDaysAdd(totalHours / (bodyshop.target_touchtime || 1), "day")
|
|
: dayjs().add(totalHours / (bodyshop.target_touchtime || 1), "day"),
|
|
scheduled_delivery: data.jobs_by_pk.scheduled_delivery
|
|
});
|
|
}
|
|
},
|
|
fetchPolicy: "network-only"
|
|
});
|
|
|
|
const blockContent = (
|
|
<Space orientation="vertical" wrap>
|
|
<Input
|
|
value={title}
|
|
onChange={(e) => setTitle(e.currentTarget.value)}
|
|
onBlur={async () => {
|
|
await updateAppointment({
|
|
variables: {
|
|
appid: event.id,
|
|
app: {
|
|
title: title
|
|
}
|
|
},
|
|
optimisticResponse: {
|
|
update_appointments: {
|
|
__typename: "appointments_mutation_response",
|
|
returning: [
|
|
{
|
|
...event,
|
|
title: title,
|
|
__typename: "appointments"
|
|
}
|
|
]
|
|
}
|
|
}
|
|
});
|
|
}}
|
|
/>
|
|
<Button onClick={() => handleCancel({ id: event.id })} disabled={event.arrived}>
|
|
{t("appointments.actions.unblock")}
|
|
</Button>
|
|
</Space>
|
|
);
|
|
|
|
const handleConvert = async (values) => {
|
|
if (!event.job?.id) {
|
|
notification.error({
|
|
title: t("appointments.errors.nojob")
|
|
});
|
|
return;
|
|
}
|
|
|
|
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({
|
|
title: 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 ? (
|
|
<Space>
|
|
<strong>{event.title}</strong>
|
|
<ScheduleEventColor event={event} />
|
|
</Space>
|
|
) : (
|
|
<Space>
|
|
<strong>
|
|
<OwnerNameDisplay ownerObject={event.job} />
|
|
</strong>
|
|
<span style={{ margin: 4 }}>
|
|
{`${(event.job && event.job.v_model_yr) || ""} ${
|
|
(event.job && event.job.v_make_desc) || ""
|
|
} ${(event.job && event.job.v_model_desc) || ""}`}
|
|
</span>
|
|
<ScheduleEventColor event={event} />
|
|
</Space>
|
|
)}
|
|
{event.job ? (
|
|
<div>
|
|
<DataLabel label={t("jobs.fields.ro_number")}>{(event.job && event.job.ro_number) || ""}</DataLabel>
|
|
<DataLabel label={t("jobs.fields.clm_total")}>
|
|
<CurrencyFormatter>{(event.job && event.job.clm_total) || ""}</CurrencyFormatter>
|
|
</DataLabel>
|
|
<DataLabel hideIfNull label={t("jobs.fields.ins_co_nm")}>
|
|
{(event.job && event.job.ins_co_nm) || ""}
|
|
</DataLabel>
|
|
<DataLabel hideIfNull label={t("jobs.fields.clm_no")}>
|
|
{(event.job && event.job.clm_no) || ""}
|
|
</DataLabel>
|
|
<DataLabel label={t("jobs.fields.ownr_ea")}>{(event.job && event.job.ownr_ea) || ""}</DataLabel>
|
|
<DataLabel label={t("jobs.fields.ownr_ph1")}>
|
|
<ChatOpenButton phone={event?.job?.ownr_ph1} type={event?.job?.ownr_ph1_ty} jobid={event.job.id} />
|
|
</DataLabel>
|
|
<DataLabel label={t("jobs.fields.ownr_ph2")}>
|
|
<ChatOpenButton phone={event?.job?.ownr_ph2} type={event?.job?.ownr_ph2_ty} jobid={event.job.id} />
|
|
</DataLabel>
|
|
<DataLabel hideIfNull label={t("jobs.fields.loss_of_use")}>
|
|
{(event.job && event.job.loss_of_use) || ""}
|
|
</DataLabel>
|
|
<DataLabel label={t("jobs.fields.alt_transport")}>
|
|
{(event.job && event.job.alt_transport) || ""}
|
|
<ScheduleAtChange job={event && event.job} />
|
|
</DataLabel>
|
|
<DataLabel
|
|
label={t("jobs.fields.comment")}
|
|
styles={{ value: { overflow: "hidden", textOverflow: "ellipsis" } }}
|
|
>
|
|
<ProductionListColumnComment record={event && event.job} />
|
|
</DataLabel>
|
|
<ScheduleEventNote event={event} />
|
|
</div>
|
|
) : (
|
|
<div>{event.note || ""}</div>
|
|
)}
|
|
<Divider />
|
|
<Space wrap>
|
|
{event.job ? (
|
|
<Link to={`/manage/jobs/${event.job && event.job.id}`}>
|
|
<Button>{t("appointments.actions.viewjob")}</Button>
|
|
</Link>
|
|
) : null}
|
|
{event.job ? (
|
|
<Button
|
|
onClick={() => {
|
|
history({
|
|
search: queryString.stringify({
|
|
...searchParams,
|
|
selected: event.job.id
|
|
})
|
|
});
|
|
}}
|
|
>
|
|
{t("appointments.actions.preview")}
|
|
</Button>
|
|
) : null}
|
|
{event.job ? (
|
|
<Dropdown
|
|
menu={{
|
|
items: [
|
|
{
|
|
key: "email",
|
|
label: t("general.labels.email"),
|
|
disabled: event.arrived,
|
|
onClick: () => {
|
|
const Template = TemplateList("job").appointment_reminder;
|
|
GenerateDocument(
|
|
{
|
|
name: Template.key,
|
|
variables: { id: event.job.id }
|
|
},
|
|
{
|
|
to: event.job && event.job.ownr_ea,
|
|
subject: Template.subject
|
|
},
|
|
"e",
|
|
event.job && event.job.id,
|
|
notification
|
|
);
|
|
}
|
|
},
|
|
{
|
|
key: "sms",
|
|
label: t("general.labels.sms"),
|
|
disabled: event.arrived || !bodyshop.messagingservicesid,
|
|
onClick: () => {
|
|
const p = parsePhoneNumber(event.job.ownr_ph1, "CA");
|
|
if (p && p.isValid()) {
|
|
openChatByPhone({
|
|
phone_num: p.formatInternational(),
|
|
jobid: event.job.id,
|
|
socket
|
|
});
|
|
setMessage(
|
|
t("appointments.labels.reminder", {
|
|
shopname: bodyshop.shopname,
|
|
date: dayjs(event.start).format("MM/DD/YYYY"),
|
|
time: dayjs(event.start).format("HH:mm a")
|
|
})
|
|
);
|
|
setOpen(false);
|
|
} else {
|
|
notification.error({
|
|
title: t("messaging.error.invalidphone")
|
|
});
|
|
}
|
|
}
|
|
}
|
|
]
|
|
}}
|
|
>
|
|
<Button>{t("appointments.actions.sendreminder")}</Button>
|
|
</Dropdown>
|
|
) : null}
|
|
{event.arrived ? (
|
|
<Button
|
|
// onClick={() => handleCancel(event.id)}
|
|
disabled={event.arrived}
|
|
>
|
|
{t("appointments.actions.cancel")}
|
|
</Button>
|
|
) : (
|
|
<Popover
|
|
trigger="click"
|
|
disabled={event.arrived}
|
|
content={
|
|
<Form
|
|
layout="vertical"
|
|
onFinish={({ lost_sale_reason }) => {
|
|
handleCancel({ id: event.id, lost_sale_reason });
|
|
}}
|
|
>
|
|
<Form.Item
|
|
name="lost_sale_reason"
|
|
label={t("jobs.fields.lost_sale_reason")}
|
|
rules={[
|
|
{
|
|
required: true
|
|
//message: t("general.validation.required"),
|
|
}
|
|
]}
|
|
>
|
|
<Select
|
|
options={bodyshop.md_lost_sale_reasons.map((lsr) => ({
|
|
label: lsr,
|
|
value: lsr
|
|
}))}
|
|
/>
|
|
</Form.Item>
|
|
<Button htmlType="submit">{t("appointments.actions.cancel")}</Button>
|
|
</Form>
|
|
}
|
|
>
|
|
<Button
|
|
// onClick={() => handleCancel(event.id)}
|
|
disabled={event.arrived}
|
|
>
|
|
{t("appointments.actions.cancel")}
|
|
</Button>
|
|
</Popover>
|
|
)}
|
|
{event.isintake ? (
|
|
<Button
|
|
disabled={event.arrived}
|
|
onClick={() => {
|
|
setOpen(false);
|
|
setScheduleContext({
|
|
actions: { refetch: refetch },
|
|
context: {
|
|
jobId: event.job.id,
|
|
job: event.job,
|
|
previousEvent: event.id,
|
|
color: event.color,
|
|
alt_transport: event.job && event.job.alt_transport,
|
|
note: event.note,
|
|
scheduled_in: event.job && event.job.scheduled_in,
|
|
scheduled_completion: event.job && event.job.scheduled_completion
|
|
}
|
|
});
|
|
}}
|
|
>
|
|
{t("appointments.actions.reschedule")}
|
|
</Button>
|
|
) : (
|
|
<ScheduleManualEvent event={event} />
|
|
)}
|
|
{event.job &&
|
|
(HasFeatureAccess({ featureName: "checklist", bodyshop }) ? (
|
|
<Link
|
|
to={{
|
|
pathname: `/manage/jobs/${event.job.id}/intake`,
|
|
search: `?appointmentId=${event.id}`
|
|
}}
|
|
>
|
|
<Button disabled={event.arrived}>{t("appointments.actions.intake")}</Button>
|
|
</Link>
|
|
) : (
|
|
<Popover
|
|
content={popMenu}
|
|
open={popOverVisible}
|
|
onOpenChange={setPopOverVisible}
|
|
onClick={(e) => {
|
|
if (event.job?.id) {
|
|
e.stopPropagation();
|
|
getJobDetails({ variables: { id: event.job.id } });
|
|
}
|
|
}}
|
|
getPopupContainer={(trigger) => trigger.parentNode}
|
|
trigger="click"
|
|
>
|
|
<Button disabled={event.arrived}>{t("jobs.actions.intake_quick")}</Button>
|
|
</Popover>
|
|
))}
|
|
</Space>
|
|
</div>
|
|
);
|
|
|
|
// Adjust event color for dark mode if needed
|
|
const getEventBackground = () => {
|
|
if (event?.block) {
|
|
return "var(--event-block-bg)"; // Use a specific color for dark mode
|
|
}
|
|
const baseColor = event.color && event.color.hex ? event.color.hex : event.color || "var(--event-bg-fallback)";
|
|
// Optionally adjust color for dark mode (e.g., lighten if too dark)
|
|
return baseColor;
|
|
};
|
|
|
|
const RegularEvent =
|
|
event.isintake && event.job ? (
|
|
<Space
|
|
wrap
|
|
size="small"
|
|
style={{
|
|
backgroundColor: getEventBackground()
|
|
}}
|
|
>
|
|
{event.note && <AlertFilled className="production-alert" />}
|
|
<strong>{`${event.job.ro_number || t("general.labels.na")}`}</strong>
|
|
<OwnerNameDisplay ownerObject={event.job} />
|
|
{`${event.job.v_model_yr || ""} ${event.job.v_make_desc || ""} ${event.job.v_model_desc || ""}`}
|
|
{`(${event.job.labhrs?.aggregate?.sum?.mod_lb_hrs || "0"} / ${
|
|
event.job.larhrs?.aggregate?.sum?.mod_lb_hrs || "0"
|
|
})`}
|
|
{event.job.alt_transport && <div style={{ margin: ".1rem" }}>{event.job.alt_transport}</div>}
|
|
{event.job.comment && `C: ${event.job.comment}`}
|
|
</Space>
|
|
) : (
|
|
<div
|
|
style={{
|
|
height: "100%",
|
|
width: "100%",
|
|
backgroundColor: getEventBackground()
|
|
}}
|
|
>
|
|
<strong>{`${event.title || ""}`}</strong>
|
|
</div>
|
|
);
|
|
|
|
return (
|
|
<Popover
|
|
open={open}
|
|
onOpenChange={(vis) => !event.vacation && setOpen(vis)}
|
|
trigger="click"
|
|
content={event.block ? blockContent : popoverContent}
|
|
style={{
|
|
height: "100%",
|
|
width: "100%",
|
|
backgroundColor: getEventBackground()
|
|
}}
|
|
>
|
|
{RegularEvent}
|
|
</Popover>
|
|
);
|
|
}
|
|
|
|
export default connect(mapStateToProps, mapDispatchToProps)(ScheduleEventComponent);
|