Merged in release/2022-03-04 (pull request #413)

release/2022-03-04

Approved-by: Patrick Fic
This commit is contained in:
Patrick Fic
2022-03-04 18:15:38 +00:00
18 changed files with 138 additions and 59 deletions

View File

@@ -9,7 +9,7 @@ import { DateFormatter } from "../../utils/DateFormatter";
import CourtesyCarFuelSlider from "../courtesy-car-fuel-select/courtesy-car-fuel-select.component"; import CourtesyCarFuelSlider from "../courtesy-car-fuel-select/courtesy-car-fuel-select.component";
import CourtesyCarStatus from "../courtesy-car-status-select/courtesy-car-status-select.component"; import CourtesyCarStatus from "../courtesy-car-status-select/courtesy-car-status-select.component";
import FormDatePicker from "../form-date-picker/form-date-picker.component"; import FormDatePicker from "../form-date-picker/form-date-picker.component";
import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component"; //import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
import CurrencyInput from "../form-items-formatted/currency-form-item.component"; import CurrencyInput from "../form-items-formatted/currency-form-item.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component"; import LayoutFormRow from "../layout-form-row/layout-form-row.component";
@@ -32,7 +32,7 @@ export default function CourtesyCarCreateFormComponent({ form, saveLoading }) {
} }
/> />
<FormFieldsChanged form={form} /> {/* <FormFieldsChanged form={form} /> */}
<LayoutFormRow header={t("courtesycars.labels.vehicle")}> <LayoutFormRow header={t("courtesycars.labels.vehicle")}>
<Form.Item <Form.Item
label={t("courtesycars.fields.make")} label={t("courtesycars.fields.make")}

View File

@@ -55,15 +55,17 @@ export function JobEmployeeAssignments({
0 0
} }
> >
{bodyshop.employees.map((emp) => ( {bodyshop.employees
<Select.Option .filter((emp) => emp.active)
value={emp.id} .map((emp) => (
key={emp.id} <Select.Option
name={`${emp.first_name} ${emp.last_name}`} value={emp.id}
> key={emp.id}
{`${emp.first_name} ${emp.last_name}`} name={`${emp.first_name} ${emp.last_name}`}
</Select.Option> >
))} {`${emp.first_name} ${emp.last_name}`}
</Select.Option>
))}
</Select> </Select>
</Col> </Col>
<Col span={24}> <Col span={24}>

View File

@@ -10,7 +10,7 @@ export default function JobLinesBillRefernece({ jobline }) {
{subletRequired && <WarningFilled />} {subletRequired && <WarningFilled />}
{`${(billLine.actual_price * billLine.quantity).toFixed(2)} (${ {`${(billLine.actual_price * billLine.quantity).toFixed(2)} (${
billLine.bill.vendor.name billLine.bill.vendor.name
})`} } #${billLine.bill.invoice_number})`}
</div> </div>
); );
} }

View File

@@ -124,7 +124,7 @@ export function JobNotesComponent({
messageObject={{ messageObject={{
subject: Templates.individual_job_note.subject, subject: Templates.individual_job_note.subject,
}} }}
id={record.id} id={jobId}
/> />
</Space> </Space>
), ),

View File

@@ -73,6 +73,7 @@ export function PartsOrderModalComponent({
options={vendorList} options={vendorList}
disabled={isReturn} disabled={isReturn}
preferredMake={preferredMake} preferredMake={preferredMake}
showPhone
/> />
</Form.Item> </Form.Item>
<Form.Item <Form.Item

View File

@@ -196,6 +196,11 @@ export function PartsOrderModalContainer({
(item) => item.id === values.vendorid (item) => item.id === values.vendorid
)[0]; )[0];
let vendorEmails =
matchingVendor &&
matchingVendor.email &&
matchingVendor.email.split(RegExp("[;,]"));
GenerateDocument( GenerateDocument(
{ {
name: isReturn name: isReturn
@@ -206,7 +211,7 @@ export function PartsOrderModalContainer({
}, },
}, },
{ {
to: matchingVendor ? [matchingVendor.email] : null, to: matchingVendor ? vendorEmails : null,
replyTo: bodyshop.email, replyTo: bodyshop.email,
subject: isReturn subject: isReturn
? Templates.parts_return_slip.subject ? Templates.parts_return_slip.subject

View File

@@ -22,7 +22,7 @@ export default function ProductionBoardCard(
) { ) {
const { t } = useTranslation(); const { t } = useTranslation();
let employee_body, employee_prep, employee_refinish; //employee_csr; let employee_body, employee_prep, employee_refinish, employee_csr;
if (card.employee_body) { if (card.employee_body) {
employee_body = bodyshop.employees.find((e) => e.id === card.employee_body); employee_body = bodyshop.employees.find((e) => e.id === card.employee_body);
} }
@@ -34,6 +34,9 @@ export default function ProductionBoardCard(
(e) => e.id === card.employee_refinish (e) => e.id === card.employee_refinish
); );
} }
if (card.employee_csr) {
employee_csr = bodyshop.employees.find((e) => e.id === card.employee_csr);
}
// if (card.employee_csr) { // if (card.employee_csr) {
// employee_csr = bodyshop.employees.find((e) => e.id === card.employee_csr); // employee_csr = bodyshop.employees.find((e) => e.id === card.employee_csr);
// } // }
@@ -131,11 +134,11 @@ export default function ProductionBoardCard(
)} ${employee_refinish.last_name.charAt(0)}` )} ${employee_refinish.last_name.charAt(0)}`
: "" : ""
} ${card.larhrs.aggregate.sum.mod_lb_hrs || "?"}h`}</Col> } ${card.larhrs.aggregate.sum.mod_lb_hrs || "?"}h`}</Col>
{/* <Col span={cardSettings && cardSettings.compact ? 24 : 12}>{`C: ${ <Col span={cardSettings && cardSettings.compact ? 24 : 12}>{`C: ${
employee_csr employee_csr
? `${employee_csr.first_name} ${employee_csr.last_name}` ? `${employee_csr.first_name} ${employee_csr.last_name}`
: "" : ""
}`}</Col> */} }`}</Col>
</Row> </Row>
</Col> </Col>
)} )}

View File

@@ -66,6 +66,7 @@ export const createBoardData = (AllStatuses, Jobs, filter) => {
include || include ||
j.employee_body === employeeId || j.employee_body === employeeId ||
j.employee_prep === employeeId || j.employee_prep === employeeId ||
j.employee_csr === employeeId ||
j.employee_refinish === employeeId; j.employee_refinish === employeeId;
} }
@@ -76,9 +77,8 @@ export const createBoardData = (AllStatuses, Jobs, filter) => {
Object.keys(DataGroupedByStatus).map((statusGroupKey) => { Object.keys(DataGroupedByStatus).map((statusGroupKey) => {
try { try {
boardLanes.columns.find( boardLanes.columns.find((l) => l.id === statusGroupKey).cards =
(l) => l.id === statusGroupKey sortByParentId(DataGroupedByStatus[statusGroupKey]);
).cards = sortByParentId(DataGroupedByStatus[statusGroupKey]);
} catch (error) { } catch (error) {
console.log("Error while creating board card", error); console.log("Error while creating board card", error);
} }

View File

@@ -116,15 +116,17 @@ export function ProductionListEmpAssignment({
0 0
} }
> >
{bodyshop.employees.map((emp) => ( {bodyshop.employees
<Select.Option .filter((emp) => emp.active)
value={emp.id} .map((emp) => (
key={emp.id} <Select.Option
name={`${emp.first_name} ${emp.last_name}`} value={emp.id}
> key={emp.id}
{`${emp.first_name} ${emp.last_name}`} name={`${emp.first_name} ${emp.last_name}`}
</Select.Option> >
))} {`${emp.first_name} ${emp.last_name}`}
</Select.Option>
))}
</Select> </Select>
</Col> </Col>
<Col span={24}> <Col span={24}>

View File

@@ -1,12 +1,13 @@
import { HeartOutlined } from "@ant-design/icons"; import { HeartOutlined } from "@ant-design/icons";
import { Select, Tag } from "antd"; import { Select, Space, Tag } from "antd";
import React, { forwardRef, useEffect, useState } from "react"; import React, { forwardRef, useEffect, useState } from "react";
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
const { Option } = Select; const { Option } = Select;
//To be used as a form element only. //To be used as a form element only.
const VendorSearchSelect = ( const VendorSearchSelect = (
{ value, onChange, options, onSelect, disabled, preferredMake }, { value, onChange, options, onSelect, disabled, preferredMake, showPhone },
ref ref
) => { ) => {
const [option, setOption] = useState(value); const [option, setOption] = useState(value);
@@ -35,6 +36,7 @@ const VendorSearchSelect = (
style={{ style={{
width: "100%", width: "100%",
}} }}
dropdownMatchSelectWidth={false}
onChange={setOption} onChange={setOption}
optionFilterProp="name" optionFilterProp="name"
onSelect={onSelect} onSelect={onSelect}
@@ -50,10 +52,15 @@ const VendorSearchSelect = (
> >
<div className="imex-flex-row"> <div className="imex-flex-row">
<div style={{ flex: 1 }}>{o.name}</div> <div style={{ flex: 1 }}>{o.name}</div>
<HeartOutlined /> <Space style={{ marginLeft: "1rem" }}>
{o.discount && o.discount !== 0 ? ( <HeartOutlined style={{ color: "red" }} />
<Tag color="green">{`${o.discount * 100}%`}</Tag> {o.phone && showPhone && (
) : null} <PhoneNumberFormatter>{o.phone}</PhoneNumberFormatter>
)}
{o.discount && o.discount !== 0 ? (
<Tag color="green">{`${o.discount * 100}%`}</Tag>
) : null}
</Space>
</div> </div>
</Option> </Option>
)) ))
@@ -64,9 +71,14 @@ const VendorSearchSelect = (
<div className="imex-flex-row" style={{ width: "100%" }}> <div className="imex-flex-row" style={{ width: "100%" }}>
<div style={{ flex: 1 }}>{o.name}</div> <div style={{ flex: 1 }}>{o.name}</div>
{o.discount && o.discount !== 0 ? ( <Space style={{ marginLeft: "1rem" }}>
<Tag color="green">{`${o.discount * 100}%`}</Tag> {o.phone && showPhone && (
) : null} <PhoneNumberFormatter>{o.phone}</PhoneNumberFormatter>
)}
{o.discount && o.discount !== 0 ? (
<Tag color="green">{`${o.discount * 100}%`}</Tag>
) : null}
</Space>
</div> </div>
</Option> </Option>
)) ))

View File

@@ -109,12 +109,14 @@ export default function VendorsFormComponent({
<Form.Item <Form.Item
label={t("vendors.fields.email")} label={t("vendors.fields.email")}
rules={[ rules={
{ [
type: "email", // {
message: t("general.validation.invalidemail"), // type: "email",
}, // message: t("general.validation.invalidemail"),
]} // },
]
}
name="email" name="email"
> >
<FormItemEmail email={getFieldValue("email")} /> <FormItemEmail email={getFieldValue("email")} />

View File

@@ -59,6 +59,8 @@ export const GET_LINE_TICKET_BY_PK = gql`
employeeid employeeid
memo memo
flat_rate flat_rate
clockon
clockoff
employee { employee {
id id
first_name first_name

View File

@@ -696,6 +696,7 @@ export const GET_JOB_BY_PK = gql`
joblineid joblineid
bill { bill {
id id
invoice_number
vendor { vendor {
id id
name name

View File

@@ -87,6 +87,7 @@ export const QUERY_ALL_VENDORS_FOR_ORDER = gql`
discount discount
email email
active active
phone
} }
jobs(where: { id: { _eq: $jobId } }) { jobs(where: { id: { _eq: $jobId } }) {
v_make_desc v_make_desc

View File

@@ -70,7 +70,12 @@ export function CourtesyCarCreateContainer({
return ( return (
<RbacWrapper action="courtesycar:create"> <RbacWrapper action="courtesycar:create">
<Form form={form} autoComplete="new-password" onFinish={handleFinish}> <Form
form={form}
autoComplete="new-password"
onFinish={handleFinish}
layout="vertical"
>
<CourtesyCarFormComponent form={form} saveLoading={loading} /> <CourtesyCarFormComponent form={form} saveLoading={loading} />
</Form> </Form>
</RbacWrapper> </RbacWrapper>

View File

@@ -149,6 +149,7 @@ app.post(
fb.unsubscribe fb.unsubscribe
); );
app.post("/adm/updateuser", fb.validateFirebaseIdToken, fb.updateUser); app.post("/adm/updateuser", fb.validateFirebaseIdToken, fb.updateUser);
app.post("/adm/createuser", fb.validateFirebaseIdToken, fb.createUser);
//Stripe Processing //Stripe Processing
var stripe = require("./server/stripe/payment"); var stripe = require("./server/stripe/payment");
@@ -180,7 +181,6 @@ app.post("/data/arms", data.arms);
var taskHandler = require("./server/tasks/tasks"); var taskHandler = require("./server/tasks/tasks");
app.post("/taskHandler", taskHandler.taskHandler); app.post("/taskHandler", taskHandler.taskHandler);
var ioevent = require("./server/ioevent/ioevent"); var ioevent = require("./server/ioevent/ioevent");
app.post("/ioevent", ioevent.default); app.post("/ioevent", ioevent.default);
app.post("/newlog", (req, res) => { app.post("/newlog", (req, res) => {
@@ -188,7 +188,6 @@ app.post("/newlog", (req, res) => {
logger.log(message, type, user, record, object); logger.log(message, type, user, record, object);
}); });
var cdkGetMake = require("./server/cdk/cdk-get-makes"); var cdkGetMake = require("./server/cdk/cdk-get-makes");
app.post("/cdk/getvehicles", fb.validateFirebaseIdToken, cdkGetMake.default); app.post("/cdk/getvehicles", fb.validateFirebaseIdToken, cdkGetMake.default);

View File

@@ -25,6 +25,43 @@ const adminEmail = [
"patrick@thinkimex.com", "patrick@thinkimex.com",
]; ];
exports.createUser = (req, res) => {
logger.log("admin-create-user", "WARN", req.user.email, null, {
request: req.body,
});
if (!adminEmail.includes(req.user.email)) {
logger.log(
"admin-create-user-unauthorized",
"ERROR",
req.user.email,
null,
{
request: req.body,
user: req.user,
}
);
res.sendStatus(404);
}
const { email, displayName, password } = req.body;
admin
.auth()
.createUser({ email, displayName, password })
.then((userRecord) => {
// See the UserRecord reference doc for the contents of userRecord.
logger.log("admin-update-user-success", "DEBUG", req.user.email, null, {
userRecord,
});
res.json(userRecord);
})
.catch((error) => {
logger.log("admin-update-user-error", "ERROR", req.user.email, null, {
error,
});
res.status(500).json(error);
});
};
exports.updateUser = (req, res) => { exports.updateUser = (req, res) => {
logger.log("admin-update-user", "WARN", req.user.email, null, { logger.log("admin-update-user", "WARN", req.user.email, null, {
request: req.body, request: req.body,

View File

@@ -111,10 +111,9 @@ exports.job = async (req, res) => {
} }
if ( if (
moment(item.actual_completion || item.scheduled_completion).tz(timezone).isBefore( moment(item.actual_completion || item.scheduled_completion)
moment().tz(timezone), .tz(timezone)
"day" .isBefore(moment().tz(timezone), "day")
)
) { ) {
console.log("Job should have already gone. Ignoring it.", item); console.log("Job should have already gone. Ignoring it.", item);
return; return;
@@ -128,7 +127,9 @@ exports.job = async (req, res) => {
} else { } else {
const itemDate = moment( const itemDate = moment(
item.actual_completion || item.scheduled_completion item.actual_completion || item.scheduled_completion
).tz(timezone).format("yyyy-MM-DD"); )
.tz(timezone)
.format("yyyy-MM-DD");
if (!!load[itemDate]) { if (!!load[itemDate]) {
load[itemDate].hoursOut = load[itemDate].hoursOut =
(load[itemDate].hoursOut || 0) + (load[itemDate].hoursOut || 0) +
@@ -153,14 +154,20 @@ exports.job = async (req, res) => {
const end = moment.max([ const end = moment.max([
...filteredArrJobs.map((a) => moment(a.scheduled_in).tz(timezone)), ...filteredArrJobs.map((a) => moment(a.scheduled_in).tz(timezone)),
...filteredCompJobs ...filteredCompJobs
.map((p) => moment(p.actual_completion || p.scheduled_completion).tz(timezone)) .map((p) =>
moment(p.actual_completion || p.scheduled_completion).tz(timezone)
)
.filter((p) => p.isValid() && p.isAfter(yesterday)), .filter((p) => p.isValid() && p.isAfter(yesterday)),
moment().tz(timezone).add(15, "days"), moment().tz(timezone).add(15, "days"),
]); ]);
const range = Math.round(moment.duration(end.diff(today)).asDays()); const range = Math.round(moment.duration(end.diff(today)).asDays());
for (var day = 0; day < range; day++) { for (var day = 0; day < range; day++) {
const current = moment(today).tz(timezone).add(day, "days").format("yyyy-MM-DD"); const current = moment(today)
const prev = moment(today).tz(timezone) .tz(timezone)
.add(day, "days")
.format("yyyy-MM-DD");
const prev = moment(today)
.tz(timezone)
.add(day - 1, "days") .add(day - 1, "days")
.format("yyyy-MM-DD"); .format("yyyy-MM-DD");
if (!!!load[current]) { if (!!!load[current]) {
@@ -194,7 +201,7 @@ exports.job = async (req, res) => {
load[startIsoFormat] = { blocked: true }; load[startIsoFormat] = { blocked: true };
} }
}); });
// //Propose the first 5 dates where we are below target. // //Propose the first 10 dates where we are below target.
const possibleDates = []; const possibleDates = [];
delete load.productionTotal; delete load.productionTotal;
@@ -204,7 +211,7 @@ exports.job = async (req, res) => {
loadKeys.forEach((loadKey) => { loadKeys.forEach((loadKey) => {
const isShopOpen = const isShopOpen =
(workingdays[dayOfWeekMapper(moment(loadKey).tz(timezone).day())] || false) && (workingdays[dayOfWeekMapper(moment(loadKey).day())] || false) &&
!load[loadKey].blocked; !load[loadKey].blocked;
if ( if (
@@ -216,10 +223,10 @@ exports.job = async (req, res) => {
possibleDates.push(new Date(loadKey).toISOString().substr(0, 10)); possibleDates.push(new Date(loadKey).toISOString().substr(0, 10));
}); });
if (possibleDates.length < 6) { if (possibleDates.length < 11) {
res.json(possibleDates); res.json(possibleDates);
} else { } else {
res.json(possibleDates.slice(0, 5)); res.json(possibleDates.slice(0, 10));
} }
} catch (error) { } catch (error) {
logger.log("smart-scheduling-error", "ERROR", req.user.email, jobId, { logger.log("smart-scheduling-error", "ERROR", req.user.email, jobId, {