Added job intake process for BOD-114

This commit is contained in:
Patrick Fic
2020-05-13 00:38:10 -07:00
parent cf8e0a918c
commit c470e0ba18
39 changed files with 2508 additions and 37 deletions

View File

@@ -3,7 +3,7 @@ import axios from "axios";
import React, { useState } from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { EmailSettings } from "../../utils/constants";
import { EmailSettings } from "../../utils/TemplateConstants";
import {
endLoading,
startLoading,

View File

@@ -10,7 +10,7 @@ import {
selectEmailVisible,
} from "../../redux/email/email.selectors.js";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { EmailSettings } from "../../utils/constants";
import { EmailSettings } from "../../utils/TemplateConstants";
import RenderTemplate from "../../utils/RenderTemplate";
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
import EmailOverlayComponent from "./email-overlay.component";
@@ -20,9 +20,11 @@ const mapStateToProps = createStructuredSelector({
emailConfig: selectEmailConfig,
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
toggleEmailOverlayVisible: () => dispatch(toggleEmailOverlayVisible()),
});
export function EmailOverlayContainer({
emailConfig,
modalVisible,

View File

@@ -0,0 +1,22 @@
import React from "react";
import { Form, Checkbox } from "antd";
import { useTranslation } from "react-i18next";
export default function JobIntakeFormCheckboxComponent({ formItem }) {
const { name, label, required } = formItem;
const { t } = useTranslation();
return (
<Form.Item
name={name}
label={label}
valuePropName='checked'
rules={[
{
required: required,
message: t("general.validation.required"),
},
]}>
<Checkbox />
</Form.Item>
);
}

View File

@@ -0,0 +1,97 @@
import React from "react";
import { Form, Button, Switch, DatePicker, notification } from "antd";
import CheckboxFormItem from "../job-intake-form-checkbox/job-itnake-form-checkbox.component";
import { useTranslation } from "react-i18next";
import { useMutation } from "@apollo/react-hooks";
import { UPDATE_JOB } from "../../../../graphql/jobs.queries";
import { useParams } from "react-router-dom";
import { selectBodyshop } from "../../../../redux/user/user.selectors";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { useHistory } from "react-router-dom";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export function JobIntakeForm({ formItems, bodyshop }) {
const { t } = useTranslation();
const [intakeJob] = useMutation(UPDATE_JOB);
const { jobId } = useParams();
const history = useHistory();
const handleFinish = async (values) => {
console.log("values", values);
const result = await intakeJob({
variables: {
jobId: jobId,
job: {
inproduction: values.addToProduction || false,
status: bodyshop.md_ro_statuses.default_arrived || "Arrived*",
actual_in: new Date(),
scheduled_completion: values.scheduledCompletion,
intakechecklist: values,
scheduled_delivery: values.scheduledDelivery,
},
},
});
if (!!!result.errors) {
notification["success"]({ message: t("intake.successes.intake") });
history.push(`/manage/jobs/${jobId}`);
} else {
notification["error"]({
message: t("intake.errors.intake", {
error: JSON.stringify(result.errors),
}),
});
}
console.log("handleFinish -> result", result);
};
const [form] = Form.useForm();
return (
<Form
form={form}
onFinish={handleFinish}
initialValues={{ addToProduction: true }}>
{t("intake.labels.checklist")}
{formItems.map((f, idx) => {
switch (f.type) {
case "checkbox":
return <CheckboxFormItem key={idx} formItem={f} />;
default:
return <div key={idx}>Error</div>;
}
})}
<Form.Item
name='addToProduction'
valuePropName='checked'
label={t("intake.labels.addtoproduction")}>
<Switch />
</Form.Item>
<Form.Item
name='scheduledCompletion'
label={t("jobs.fields.scheduled_completion")}
rules={[
{
required: true,
message: t("general.validation.required"),
},
]}>
<DatePicker />
</Form.Item>
<Form.Item
name='scheduledDelivery'
label={t("jobs.fields.scheduled_delivery")}>
<DatePicker />
</Form.Item>
<Button htmlType='submit'>{t("general.actions.submit")}</Button>
</Form>
);
}
export default connect(mapStateToProps, mapDispatchToProps)(JobIntakeForm);

View File

@@ -0,0 +1,10 @@
import React from "react";
import { PrinterFilled } from "@ant-design/icons";
export default function JobIntakeTemplateItem({ templateKey, renderTemplate }) {
return (
<div>
{templateKey}
<PrinterFilled onClick={() => renderTemplate(templateKey)} />
</div>
);
}

View File

@@ -0,0 +1,58 @@
import React from "react";
import JobIntakeTemplateItem from "../job-intake-template-item/job-intake-template-item.component";
import { useParams } from "react-router-dom";
import RenderTemplate, {
displayTemplateInWindow,
} from "../../../../utils/RenderTemplate";
import { Button } from "antd";
import { selectBodyshop } from "../../../../redux/user/user.selectors";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { useTranslation } from "react-i18next";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export function JobIntakeTemplateList({ bodyshop, templates }) {
const { jobId } = useParams();
const { t } = useTranslation();
const renderTemplate = async (templateKey) => {
const html = await RenderTemplate(
{
name: templateKey,
variables: { id: jobId },
},
bodyshop
);
displayTemplateInWindow(html);
};
const renderAllTemplates = () => {
templates.forEach((template) => renderTemplate(template));
};
return (
<div>
{t("intake.labels.printpack")}
<Button onClick={renderAllTemplates}>
{t("intake.actions.printall")}
</Button>
{templates.map((template) => (
<JobIntakeTemplateItem
key={template}
templateKey={template}
renderTemplate={renderTemplate}
/>
))}
</div>
);
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(JobIntakeTemplateList);

View File

@@ -0,0 +1,13 @@
import React from "react";
import JobIntakeTemplateList from "./components/job-intake-template-list/job-intake-template-list.component";
import JobIntakeForm from "./components/job-intake-form/job-intake-form.component";
export default function JobIntakeComponent({ intakeChecklistConfig }) {
const { form, templates } = intakeChecklistConfig;
return (
<div>
<JobIntakeTemplateList templates={templates} />
<JobIntakeForm formItems={form} />
</div>
);
}

View File

@@ -5,7 +5,9 @@ import { createStructuredSelector } from "reselect";
import { setEmailOptions } from "../../redux/email/email.actions";
import { selectPrintCenter } from "../../redux/modals/modals.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import RenderTemplate from "../../utils/RenderTemplate";
import RenderTemplate, {
displayTemplateInWindow,
} from "../../utils/RenderTemplate";
const mapStateToProps = createStructuredSelector({
printCenterModal: selectPrintCenter,
bodyshop: selectBodyshop,
@@ -28,12 +30,9 @@ export function PrintCenterItemComponent({
name: item.key,
variables: { id: id },
},
bodyshop
);
var newWin = window.open();
newWin.document.write(html);
displayTemplateInWindow(html);
};
if (disabled) return <li className='print-center-item'>{item.title} </li>;

View File

@@ -9,7 +9,7 @@ import { useHistory, useLocation } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { INSERT_TEMPLATE } from "../../graphql/templates.queries";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { TemplateList } from "../../utils/constants";
import { TemplateList } from "../../utils/TemplateConstants";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser

View File

@@ -7,7 +7,7 @@ import Skeleton from "../loading-skeleton/loading-skeleton.component";
import { useTranslation } from "react-i18next";
import { useHistory, useLocation } from "react-router-dom";
import queryString from "query-string";
import { TemplateList } from "../../utils/constants";
import { TemplateList } from "../../utils/TemplateConstants";
import ShopTemplateAdd from "../shop-template-add/shop-template-add.component";
import ShopTemplateDeleteComponent from "../shop-template-delete/shop-template-delete.component";

View File

@@ -38,12 +38,8 @@ let messaging;
try {
messaging = firebase.messaging();
// Project Settings => Cloud Messaging => Web Push certificates
// messaging.usePublicVapidKey(process.env.REACT_APP_FIREBASE_PUBLIC_VAPID_KEY);
messaging.usePublicVapidKey(process.env.REACT_APP_FIREBASE_PUBLIC_VAPID_KEY);
console.log(
"Firebase Messaging initialized successfully.",
process.env.REACT_APP_FIREBASE_PUBLIC_VAPID_KEY
);
console.log("Firebase Messaging initialized successfully.");
} catch {
console.log("Firebase Messaging is likely unsupported.");
}

View File

@@ -88,3 +88,11 @@ export const UPDATE_SHOP = gql`
}
}
`;
export const QUERY_INTAKE_CHECKLIST = gql`
query QUERY_INTAKE_CHECKLIST($shopId: uuid!) {
bodyshops_by_pk(id: $shopId) {
id
intakechecklist
}
}
`;

View File

@@ -1,18 +1,13 @@
import { Col, Row } from "antd";
import React, { useEffect } from "react";
import InvoicesByVendorList from "../../components/invoices-by-vendor-list/invoices-by-vendor-list.component";
import VendorsList from "../../components/invoices-vendors-list/invoices-vendors-list.component";
import InvoiceDetailEditContainer from "../../components/invoice-detail-edit/invoice-detail-edit.container";
import { setBreadcrumbs } from "../../redux/application/application.actions";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import InvoiceDetailEditContainer from "../../components/invoice-detail-edit/invoice-detail-edit.container";
import InvoicesByVendorList from "../../components/invoices-by-vendor-list/invoices-by-vendor-list.component";
import VendorsList from "../../components/invoices-vendors-list/invoices-vendors-list.component";
import { setBreadcrumbs } from "../../redux/application/application.actions";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)),
});
@@ -41,7 +36,4 @@ export function InvoicesPageContainer({ setBreadcrumbs }) {
</Row>
);
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(InvoicesPageContainer);
export default connect(null, mapDispatchToProps)(InvoicesPageContainer);

View File

@@ -0,0 +1,90 @@
import React, { useEffect } from "react";
import { useQuery } from "@apollo/react-hooks";
import { QUERY_INTAKE_CHECKLIST } from "../../graphql/bodyshop.queries";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { useParams } from "react-router-dom";
import AlertComponent from "../../components/alert/alert.component";
import LoadingSpinner from "../../components/loading-spinner/loading-spinner.component";
import { useTranslation } from "react-i18next";
import { setBreadcrumbs } from "../../redux/application/application.actions";
import JobIntakeComponent from "../../components/job-intake/job-intake.component";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)),
});
export function JobsIntakeContainer({ bodyshop, setBreadcrumbs }) {
const { t } = useTranslation();
const { loading, error, data } = useQuery(QUERY_INTAKE_CHECKLIST, {
variables: { shopId: bodyshop.id },
});
const { jobId } = useParams();
useEffect(() => {
document.title = t("titles.jobs-intake");
setBreadcrumbs([
{ link: "/manage/jobs", label: t("titles.bc.jobs") },
{
link: `/manage/jobs/${jobId}`,
label: t("titles.bc.jobs-detail", { number: "TODO" }),
},
{
link: `/manage/jobs/${jobId}/intake`,
label: t("titles.bc.jobs-intake"),
},
]);
}, [t, setBreadcrumbs, jobId]);
if (loading) return <LoadingSpinner />;
if (error) return <AlertComponent message={error.message} type='error' />;
// if (data && !!!data.bodyshops_by_pk.intakechecklist)
// return (
// <AlertComponent message={t("intake.errors.nochecklist")} type='error' />
// );
return (
<div>
<JobIntakeComponent intakeChecklistConfig={testConfig} />
</div>
);
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(JobsIntakeContainer);
const testConfig = {
form: [
{
name: "item1",
label: "Checklist Item 1",
required: true,
type: "checkbox",
},
{
name: "item2",
label: "Checklist Item 2",
required: true,
type: "checkbox",
},
{
name: "item3",
label: "Checklist Item 3",
required: true,
type: "checkbox",
},
{
name: "item4",
label: "Checklist Item 4",
required: true,
type: "checkbox",
},
],
templates: ["estimate_detail", "estimate_detail2"],
};

View File

@@ -85,6 +85,9 @@ const ProductionBoardPage = lazy(() =>
const ShopTemplates = lazy(() =>
import("../shop-templates/shop-templates.container")
);
const JobIntake = lazy(() =>
import("../jobs-intake/jobs-intake.page.container")
);
const { Header, Content, Footer } = Layout;
export default function Manage({ match }) {
@@ -116,14 +119,19 @@ export default function Manage({ match }) {
<PrintCenterModalContainer />
<Route exact path={`${match.path}`} component={ManageRootPage} />
<Route exact path={`${match.path}/jobs`} component={JobsPage} />
<Switch>
<Route
exact
path={`${match.path}/jobs/:jobId/intake`}
component={JobIntake}
/>
<Route
exact
path={`${match.path}/jobs/new`}
component={JobsCreateContainerPage}
/>
<Route
exact
path={`${match.path}/jobs/:jobId`}
component={JobsDetailPage}
/>
@@ -188,7 +196,6 @@ export default function Manage({ match }) {
path={`${match.path}/vehicles/:vehId`}
component={VehiclesDetailContainer}
/>
<Route
exact
path={`${match.path}/invoices`}
@@ -199,7 +206,6 @@ export default function Manage({ match }) {
path={`${match.path}/invoices/:invoiceId`}
component={InvoiceDetailPage}
/>
<Route
exact
path={`${match.path}/owners`}

View File

@@ -304,7 +304,8 @@
"edit": "Edit",
"reset": "Reset to original.",
"save": "Save",
"saveandnew": "Save and New"
"saveandnew": "Save and New",
"submit": "Submit"
},
"labels": {
"actions": "Actions",
@@ -335,6 +336,23 @@
"required": "This field is required. "
}
},
"intake": {
"actions": {
"printall": "Print All Documents"
},
"errors": {
"intake": "Error during job intake. {{error}}",
"nochecklist": "No checklist has been configured for your shop. "
},
"labels": {
"addtoproduction": "Add Job to Production?",
"checklist": "Job Intake Checklist",
"printpack": "Job Intake Print Pack"
},
"successes": {
"intake": "Job intake completed."
}
},
"invoicelines": {
"actions": {
"newline": "New Line"
@@ -857,6 +875,7 @@
"jobs": "Jobs",
"jobs-active": "Active Jobs",
"jobs-detail": "Job {{number}}",
"jobs-intake": "Intake",
"jobs-new": "Create a New Job",
"owner-detail": "{{name}}",
"owners": "Owners",
@@ -877,6 +896,7 @@
"invoices-list": "Invoices | $t(titles.app)",
"jobs": "Active Jobs | $t(titles.app)",
"jobs-create": "Create a New Job | $t(titles.app)",
"jobs-intake": "Intake | $t(titles.app)",
"jobsavailable": "Available Jobs | $t(titles.app)",
"jobsdetail": "Job {{ro_number}} | $t(titles.app)",
"jobsdocuments": "Job Documents {{ro_number}} | $t(titles.app)",

View File

@@ -304,7 +304,8 @@
"edit": "Editar",
"reset": "Restablecer a original.",
"save": "Salvar",
"saveandnew": ""
"saveandnew": "",
"submit": ""
},
"labels": {
"actions": "Comportamiento",
@@ -335,6 +336,23 @@
"required": "Este campo es requerido."
}
},
"intake": {
"actions": {
"printall": ""
},
"errors": {
"intake": "",
"nochecklist": ""
},
"labels": {
"addtoproduction": "",
"checklist": "",
"printpack": ""
},
"successes": {
"intake": ""
}
},
"invoicelines": {
"actions": {
"newline": ""
@@ -857,6 +875,7 @@
"jobs": "",
"jobs-active": "",
"jobs-detail": "",
"jobs-intake": "",
"jobs-new": "",
"owner-detail": "",
"owners": "",
@@ -877,6 +896,7 @@
"invoices-list": "",
"jobs": "Todos los trabajos | $t(titles.app)",
"jobs-create": "",
"jobs-intake": "",
"jobsavailable": "Empleos disponibles | $t(titles.app)",
"jobsdetail": "Trabajo {{ro_number}} | $t(titles.app)",
"jobsdocuments": "Documentos de trabajo {{ro_number}} | $ t (títulos.app)",

View File

@@ -304,7 +304,8 @@
"edit": "modifier",
"reset": "Rétablir l'original.",
"save": "sauvegarder",
"saveandnew": ""
"saveandnew": "",
"submit": ""
},
"labels": {
"actions": "actes",
@@ -335,6 +336,23 @@
"required": "Ce champ est requis."
}
},
"intake": {
"actions": {
"printall": ""
},
"errors": {
"intake": "",
"nochecklist": ""
},
"labels": {
"addtoproduction": "",
"checklist": "",
"printpack": ""
},
"successes": {
"intake": ""
}
},
"invoicelines": {
"actions": {
"newline": ""
@@ -857,6 +875,7 @@
"jobs": "",
"jobs-active": "",
"jobs-detail": "",
"jobs-intake": "",
"jobs-new": "",
"owner-detail": "",
"owners": "",
@@ -877,6 +896,7 @@
"invoices-list": "",
"jobs": "Tous les emplois | $t(titles.app)",
"jobs-create": "",
"jobs-intake": "",
"jobsavailable": "Emplois disponibles | $t(titles.app)",
"jobsdetail": "Travail {{ro_number}} | $t(titles.app)",
"jobsdocuments": "Documents de travail {{ro_number}} | $ t (titres.app)",

View File

@@ -39,3 +39,19 @@ export default async function RenderTemplate(templateObject, bodyshop) {
resolve(data);
});
}
export const displayTemplateInWindow = (html) => {
try {
var newWin = window.open("", "_blank", "toolbar=0,location=0,menubar=0");
newWin.document.write(html);
setTimeout(function () {
newWin.document.close();
newWin.focus();
newWin.print();
newWin.close();
}, 500);
} catch (error) {
console.log("Unable to write to new window.", error);
}
};

View File

@@ -14,9 +14,9 @@ export const TemplateList = {
description: "Parts order template including part details",
drivingId: "Parts order Id",
},
test_Template: {
title: "Test",
description: "Test - does nto exist.",
estimate_detail: {
title: "Estimate Detail Lines",
description: "Est Detail",
drivingId: "test does not exist.",
},
};