IO-347 Manual appointments.

This commit is contained in:
Patrick Fic
2022-01-06 10:13:49 -08:00
parent fbd9fce230
commit ade92d86aa
9 changed files with 288 additions and 106 deletions

View File

@@ -1,4 +1,4 @@
<babeledit_project version="1.2" be_version="2.7.1"> <babeledit_project be_version="2.7.1" version="1.2">
<!-- <!--
BabelEdit project file BabelEdit project file
@@ -500,6 +500,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>end</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node> <concept_node>
<name>note</name> <name>note</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -521,6 +542,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>start</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node> <concept_node>
<name>time</name> <name>time</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -673,6 +715,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>dataconsistency</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node> <concept_node>
<name>history</name> <name>history</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -715,6 +778,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>manualevent</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node> <concept_node>
<name>noarrivingjobs</name> <name>noarrivingjobs</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>

View File

@@ -27,6 +27,7 @@ import { GenerateDocument } from "../../utils/RenderTemplate";
import { TemplateList } from "../../utils/TemplateConstants"; import { TemplateList } from "../../utils/TemplateConstants";
import ChatOpenButton from "../chat-open-button/chat-open-button.component"; import ChatOpenButton from "../chat-open-button/chat-open-button.component";
import DataLabel from "../data-label/data-label.component"; import DataLabel from "../data-label/data-label.component";
import ScheduleManualEvent from "../schedule-manual-event/schedule-manual-event.component";
import ScheduleAtChange from "./job-at-change.component"; import ScheduleAtChange from "./job-at-change.component";
import ScheduleEventColor from "./schedule-event.color.component"; import ScheduleEventColor from "./schedule-event.color.component";
import ScheduleEventNote from "./schedule-event.note.component"; import ScheduleEventNote from "./schedule-event.note.component";
@@ -66,7 +67,10 @@ export function ScheduleEventComponent({
const popoverContent = ( const popoverContent = (
<div style={{ maxWidth: "40vw" }}> <div style={{ maxWidth: "40vw" }}>
{!event.isintake ? ( {!event.isintake ? (
<strong>{event.title}</strong> <Space>
<strong>{event.title}</strong>
<ScheduleEventColor event={event} />
</Space>
) : ( ) : (
<Space> <Space>
<strong>{`${(event.job && event.job.ownr_fn) || ""} ${ <strong>{`${(event.job && event.job.ownr_fn) || ""} ${
@@ -118,7 +122,9 @@ export function ScheduleEventComponent({
</DataLabel> </DataLabel>
<ScheduleEventNote event={event} /> <ScheduleEventNote event={event} />
</div> </div>
) : null} ) : (
<div>{event.note || ""}</div>
)}
<Divider /> <Divider />
<Space wrap> <Space wrap>
{event.job ? ( {event.job ? (
@@ -140,84 +146,89 @@ export function ScheduleEventComponent({
{t("appointments.actions.preview")} {t("appointments.actions.preview")}
</Button> </Button>
) : null} ) : null}
{event.job ? (
<Dropdown <Dropdown
overlay={ overlay={
<Menu> <Menu>
<Menu.Item <Menu.Item
onClick={() => { onClick={() => {
const Template = TemplateList("job").appointment_reminder; const Template = TemplateList("job").appointment_reminder;
GenerateDocument( GenerateDocument(
{ {
name: Template.key, name: Template.key,
variables: { id: event.job.id }, variables: { id: event.job.id },
}, },
{ {
to: event.job && event.job.ownr_ea, to: event.job && event.job.ownr_ea,
subject: Template.subject, subject: Template.subject,
}, },
"e", "e",
event.job && event.job.id event.job && event.job.id
);
}}
disabled={event.arrived}
>
{t("general.labels.email")}
</Menu.Item>
<Menu.Item
onClick={() => {
const p = parsePhoneNumber(event.job.ownr_ph1, "CA");
if (p && p.isValid()) {
openChatByPhone({
phone_num: p.formatInternational(),
jobid: event.job.id,
});
setMessage(
t("appointments.labels.reminder", {
shopname: bodyshop.shopname,
date: moment(event.start).format("MM/DD/YYYY"),
time: moment(event.start).format("HH:mm a"),
})
); );
setVisible(false); }}
} else { disabled={event.arrived}
notification["error"]({ >
message: t("messaging.error.invalidphone"), {t("general.labels.email")}
}); </Menu.Item>
} <Menu.Item
}} onClick={() => {
disabled={event.arrived || !bodyshop.messagingservicesid} const p = parsePhoneNumber(event.job.ownr_ph1, "CA");
> if (p && p.isValid()) {
{t("general.labels.sms")} openChatByPhone({
</Menu.Item> phone_num: p.formatInternational(),
</Menu> jobid: event.job.id,
} });
> setMessage(
<Button>{t("appointments.actions.sendreminder")}</Button> t("appointments.labels.reminder", {
</Dropdown> shopname: bodyshop.shopname,
date: moment(event.start).format("MM/DD/YYYY"),
time: moment(event.start).format("HH:mm a"),
})
);
setVisible(false);
} else {
notification["error"]({
message: t("messaging.error.invalidphone"),
});
}
}}
disabled={event.arrived || !bodyshop.messagingservicesid}
>
{t("general.labels.sms")}
</Menu.Item>
</Menu>
}
>
<Button>{t("appointments.actions.sendreminder")}</Button>
</Dropdown>
) : null}
<Button onClick={() => handleCancel(event.id)} disabled={event.arrived}> <Button onClick={() => handleCancel(event.id)} disabled={event.arrived}>
{t("appointments.actions.cancel")} {t("appointments.actions.cancel")}
</Button> </Button>
<Button {event.isintake ? (
disabled={event.arrived} <Button
onClick={() => { disabled={event.arrived}
setVisible(false); onClick={() => {
setScheduleContext({ setVisible(false);
actions: { refetch: refetch }, setScheduleContext({
context: { actions: { refetch: refetch },
jobId: event.job.id, context: {
job: event.job, jobId: event.job.id,
previousEvent: event.id, job: event.job,
color: event.color, previousEvent: event.id,
alt_transport: event.job && event.job.alt_transport, color: event.color,
note: event.note, alt_transport: event.job && event.job.alt_transport,
}, note: event.note,
}); },
}} });
> }}
{t("appointments.actions.reschedule")} >
</Button> {t("appointments.actions.reschedule")}
</Button>
) : (
<ScheduleManualEvent event={event} />
)}
{event.isintake ? ( {event.isintake ? (
<Link <Link
to={{ to={{
@@ -235,7 +246,13 @@ export function ScheduleEventComponent({
); );
const RegularEvent = event.isintake ? ( const RegularEvent = event.isintake ? (
<div style={{ display: "flex", flexWrap: "wrap" }}> <div
style={{
display: "flex",
flexWrap: "wrap",
height: "100%",
}}
>
<Space> <Space>
{event.note && <AlertFilled className="production-alert" />} {event.note && <AlertFilled className="production-alert" />}
<strong>{`${event.job.ro_number || t("general.labels.na")}`}</strong> <strong>{`${event.job.ro_number || t("general.labels.na")}`}</strong>
@@ -262,7 +279,7 @@ export function ScheduleEventComponent({
)} )}
</div> </div>
) : ( ) : (
<div> <div style={{ height: "100%", width: "100%" }}>
<strong>{`${event.title || ""}`}</strong> <strong>{`${event.title || ""}`}</strong>
</div> </div>
); );

View File

@@ -12,6 +12,7 @@ import "./schedule-calendar.styles.scss";
import JobDetailCards from "../job-detail-cards/job-detail-cards.component"; import JobDetailCards from "../job-detail-cards/job-detail-cards.component";
import { selectProblemJobs } from "../../redux/application/application.selectors"; import { selectProblemJobs } from "../../redux/application/application.selectors";
import { Alert } from "antd"; import { Alert } from "antd";
import { useTranslation } from "react-i18next";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
@@ -30,7 +31,7 @@ export function ScheduleCalendarWrapperComponent({
}) { }) {
const search = queryString.parse(useLocation().search); const search = queryString.parse(useLocation().search);
const history = useHistory(); const history = useHistory();
const { t } = useTranslation();
const handleEventPropStyles = (event, start, end, isSelected) => { const handleEventPropStyles = (event, start, end, isSelected) => {
return { return {
...(event.color ...(event.color
@@ -57,7 +58,10 @@ export function ScheduleCalendarWrapperComponent({
<Alert <Alert
key={problem.id} key={problem.id}
type="error" type="error"
message={`${problem.ro_number} has a data consistency issue. It has been exlcuded for scheduling purposes. CODE: ${problem.code}`} message={t("appointments.labels.dataconsistency", {
ro_number: problem.ro_number,
code: problem.code,
})}
/> />
))} ))}

View File

@@ -3,7 +3,7 @@ import { Button, Card, Col, PageHeader, Row, Space } from "antd";
import React from "react"; import React from "react";
import ScheduleCalendarWrapperComponent from "../schedule-calendar-wrapper/scheduler-calendar-wrapper.component"; import ScheduleCalendarWrapperComponent from "../schedule-calendar-wrapper/scheduler-calendar-wrapper.component";
import ScheduleModal from "../schedule-job-modal/schedule-job-modal.container"; import ScheduleModal from "../schedule-job-modal/schedule-job-modal.container";
//import ScheduleManualEvent from "../schedule-manual-event/schedule-manual-event.component"; import ScheduleManualEvent from "../schedule-manual-event/schedule-manual-event.component";
import ScheduleProductionList from "../schedule-production-list/schedule-production-list.component"; import ScheduleProductionList from "../schedule-production-list/schedule-production-list.component";
import ScheduleVerifyIntegrity from "../schedule-verify-integrity/schedule-verify-integrity.component"; import ScheduleVerifyIntegrity from "../schedule-verify-integrity/schedule-verify-integrity.component";
@@ -25,9 +25,8 @@ export default function ScheduleCalendarComponent({ data, refetch }) {
<SyncOutlined /> <SyncOutlined />
</Button> </Button>
<ScheduleProductionList /> <ScheduleProductionList />
{
// <ScheduleManualEvent /> <ScheduleManualEvent />
}
</Space> </Space>
} }
/> />

View File

@@ -1,17 +1,32 @@
import { useMutation } from "@apollo/client"; import { useMutation } from "@apollo/client";
import { Button, Card, Form, Input, Popover, Space } from "antd"; import { Button, Card, Form, Input, Popover, Select, Space } from "antd";
import moment from "moment";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { logImEXEvent } from "../../firebase/firebase.utils"; import { logImEXEvent } from "../../firebase/firebase.utils";
import { import {
INSERT_APPOINTMENT, INSERT_MANUAL_APPT,
UPDATE_APPOINTMENT, UPDATE_APPOINTMENT,
} from "../../graphql/appointments.queries"; } from "../../graphql/appointments.queries";
import { selectBodyshop } from "../../redux/user/user.selectors";
import FormDateTimePickerComponent from "../form-date-time-picker/form-date-time-picker.component"; import FormDateTimePickerComponent from "../form-date-time-picker/form-date-time-picker.component";
export default function ScheduleManualEvent({ event }) { const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(ScheduleManualEvent);
export function ScheduleManualEvent({ bodyshop, event }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [insertAppointment] = useMutation(INSERT_APPOINTMENT); const [insertAppointment] = useMutation(INSERT_MANUAL_APPT);
const [updateAppointment] = useMutation(UPDATE_APPOINTMENT); const [updateAppointment] = useMutation(UPDATE_APPOINTMENT);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [form] = Form.useForm(); const [form] = Form.useForm();
@@ -22,26 +37,27 @@ export default function ScheduleManualEvent({ event }) {
useEffect(() => { useEffect(() => {
if (visibility && event) { if (visibility && event) {
form.setFieldsValue({ event }); form.setFieldsValue(event);
} }
}, [visibility, form, event]); }, [visibility, form, event]);
useEffect(() => {
// if (entryData && entryData.scoreboard && entryData.scoreboard[0]) {
// console.log("Setting FOrm");
// // form.setFieldsValue(entryData.scoreboard[0]);
// }
}, [form]);
const handleFinish = async (values) => { const handleFinish = async (values) => {
logImEXEvent("job_close_add_to_scoreboard"); logImEXEvent("schedule_manual_event");
setLoading(true); setLoading(true);
try { try {
if (event && event.id) { if (event && event.id) {
updateAppointment(); updateAppointment({
variables: { appid: event.id, app: values },
refetchQueries: ["QUERY_ALL_ACTIVE_APPOINTMENTS"],
});
} else { } else {
insertAppointment(); insertAppointment({
variables: {
apt: { ...values, isintake: false, bodyshopid: bodyshop.id },
},
refetchQueries: ["QUERY_ALL_ACTIVE_APPOINTMENTS"],
});
} }
} catch (error) { } catch (error) {
console.log(error); console.log(error);
@@ -56,8 +72,8 @@ export default function ScheduleManualEvent({ event }) {
<div> <div>
<Form form={form} layout="vertical" onFinish={handleFinish}> <Form form={form} layout="vertical" onFinish={handleFinish}>
<Form.Item <Form.Item
label={t("schedule.fields.note")} label={t("appointments.fields.title")}
name="note" name="title"
rules={[ rules={[
{ {
required: true, required: true,
@@ -67,8 +83,11 @@ export default function ScheduleManualEvent({ event }) {
> >
<Input /> <Input />
</Form.Item> </Form.Item>
<Form.Item label={t("appointments.fields.note")} name="note">
<Input />
</Form.Item>
<Form.Item <Form.Item
label={t("schedule.fields.start")} label={t("appointments.fields.start")}
name="start" name="start"
rules={[ rules={[
{ {
@@ -80,17 +99,42 @@ export default function ScheduleManualEvent({ event }) {
<FormDateTimePickerComponent /> <FormDateTimePickerComponent />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label={t("schedule.fields.end")} label={t("appointments.fields.end")}
name="end" name="end"
rules={[ rules={[
{ {
required: true, required: true,
//message: t("general.validation.required"), //message: t("general.validation.required"),
}, },
({ getFieldValue }) => ({
async validator(rule, value) {
if (value) {
const { start } = form.getFieldsValue();
if (moment(start).isAfter(moment(value))) {
return Promise.reject(
t("employees.labels.endmustbeafterstart")
);
} else {
return Promise.resolve();
}
} else {
return Promise.resolve();
}
},
}),
]} ]}
> >
<FormDateTimePickerComponent /> <FormDateTimePickerComponent />
</Form.Item> </Form.Item>
<Form.Item label={t("appointments.fields.color")} name="color">
<Select>
{bodyshop.appt_colors.map((col, idx) => (
<Select.Option key={idx} value={col.color.hex}>
{col.label}
</Select.Option>
))}
</Select>
</Form.Item>
<Space wrap> <Space wrap>
<Button type="primary" htmlType="submit"> <Button type="primary" htmlType="submit">
@@ -112,7 +156,9 @@ export default function ScheduleManualEvent({ event }) {
return ( return (
<Popover content={overlay} visible={visibility}> <Popover content={overlay} visible={visibility}>
<Button loading={loading} onClick={handleClick}> <Button loading={loading} onClick={handleClick}>
{t("schedule.labels.manualevent")} {event
? t("appointments.actions.reschedule")
: t("appointments.labels.manualevent")}
</Button> </Button>
</Popover> </Popover>
); );

View File

@@ -91,6 +91,26 @@ export const INSERT_APPOINTMENT_BLOCK = gql`
} }
`; `;
export const INSERT_MANUAL_APPT = gql`
mutation INSERT_MANUAL_APPT($apt: appointments_insert_input!) {
insert_appointments_one(object: $apt) {
start
id
end
arrived
title
isintake
block
color
note
canceled
job {
id
}
}
}
`;
export const INSERT_APPOINTMENT = gql` export const INSERT_APPOINTMENT = gql`
mutation INSERT_APPOINTMENT( mutation INSERT_APPOINTMENT(
$app: [appointments_insert_input!]! $app: [appointments_insert_input!]!

View File

@@ -37,7 +37,9 @@
"fields": { "fields": {
"alt_transport": "Alt. Trans.", "alt_transport": "Alt. Trans.",
"color": "Appointment Color", "color": "Appointment Color",
"note": "Appt. Note", "end": "End",
"note": "Note",
"start": "Start",
"time": "Appointment Time", "time": "Appointment Time",
"title": "Title" "title": "Title"
}, },
@@ -47,8 +49,10 @@
"blocked": "Blocked", "blocked": "Blocked",
"cancelledappointment": "Canceled appointment for: ", "cancelledappointment": "Canceled appointment for: ",
"completingjobs": "Completing Jobs", "completingjobs": "Completing Jobs",
"dataconsistency": "{{ro_number}} has a data consistency issue. It has been excluded for scheduling purposes. CODE: {{code}}.",
"history": "History", "history": "History",
"inproduction": "Jobs In Production", "inproduction": "Jobs In Production",
"manualevent": "Add Manual Appointment",
"noarrivingjobs": "No jobs are arriving.", "noarrivingjobs": "No jobs are arriving.",
"nocompletingjobs": "No jobs scheduled for completion.", "nocompletingjobs": "No jobs scheduled for completion.",
"nodateselected": "No date has been selected.", "nodateselected": "No date has been selected.",
@@ -864,7 +868,7 @@
}, },
"labels": { "labels": {
"actions": "Actions", "actions": "Actions",
"endmustbeafterstart": "End date must be after start date.Ï", "endmustbeafterstart": "End date must be after start date.",
"flat_rate": "Flat Rate", "flat_rate": "Flat Rate",
"name": "Name", "name": "Name",
"rate_type": "Rate Type", "rate_type": "Rate Type",

View File

@@ -37,7 +37,9 @@
"fields": { "fields": {
"alt_transport": "", "alt_transport": "",
"color": "", "color": "",
"end": "",
"note": "", "note": "",
"start": "",
"time": "", "time": "",
"title": "Título" "title": "Título"
}, },
@@ -47,8 +49,10 @@
"blocked": "", "blocked": "",
"cancelledappointment": "Cita cancelada para:", "cancelledappointment": "Cita cancelada para:",
"completingjobs": "", "completingjobs": "",
"dataconsistency": "",
"history": "", "history": "",
"inproduction": "", "inproduction": "",
"manualevent": "",
"noarrivingjobs": "", "noarrivingjobs": "",
"nocompletingjobs": "", "nocompletingjobs": "",
"nodateselected": "No se ha seleccionado ninguna fecha.", "nodateselected": "No se ha seleccionado ninguna fecha.",

View File

@@ -37,7 +37,9 @@
"fields": { "fields": {
"alt_transport": "", "alt_transport": "",
"color": "", "color": "",
"end": "",
"note": "", "note": "",
"start": "",
"time": "", "time": "",
"title": "Titre" "title": "Titre"
}, },
@@ -47,8 +49,10 @@
"blocked": "", "blocked": "",
"cancelledappointment": "Rendez-vous annulé pour:", "cancelledappointment": "Rendez-vous annulé pour:",
"completingjobs": "", "completingjobs": "",
"dataconsistency": "",
"history": "", "history": "",
"inproduction": "", "inproduction": "",
"manualevent": "",
"noarrivingjobs": "", "noarrivingjobs": "",
"nocompletingjobs": "", "nocompletingjobs": "",
"nodateselected": "Aucune date n'a été sélectionnée.", "nodateselected": "Aucune date n'a été sélectionnée.",