Merged in development (pull request #21)

Final push for Sprint 37.
This commit is contained in:
Patrick Fic
2021-02-27 00:21:44 +00:00
19 changed files with 178 additions and 42 deletions

View File

@@ -4099,6 +4099,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>checklist-view</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>close</name> <name>close</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -27559,6 +27580,48 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>noemployeeforuser</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>
<name>noemployeeforuser_sub</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>
</children> </children>
</folder_node> </folder_node>
<folder_node> <folder_node>

View File

@@ -18,7 +18,9 @@ import { setModalContext } from "../../redux/modals/modals.actions";
import CurrencyFormatter from "../../utils/CurrencyFormatter"; import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { DateFormatter } from "../../utils/DateFormatter"; import { DateFormatter } from "../../utils/DateFormatter";
import { alphaSort } from "../../utils/sorters"; import { alphaSort } from "../../utils/sorters";
import { TemplateList } from "../../utils/TemplateConstants";
import BillDeleteButton from "../bill-delete-button/bill-delete-button.component"; import BillDeleteButton from "../bill-delete-button/bill-delete-button.component";
import PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
//jobRO: selectJobReadOnly, //jobRO: selectJobReadOnly,
@@ -49,7 +51,7 @@ export function BillsListTableComponent({
}); });
const search = queryString.parse(useLocation().search); const search = queryString.parse(useLocation().search);
const selectedBill = search.billid; const selectedBill = search.billid;
const Templates = TemplateList("bill");
const bills = billsQuery.data ? billsQuery.data.bills : []; const bills = billsQuery.data ? billsQuery.data.bills : [];
const { refetch } = billsQuery; const { refetch } = billsQuery;
const columns = [ const columns = [
@@ -115,7 +117,7 @@ export function BillsListTableComponent({
dataIndex: "actions", dataIndex: "actions",
key: "actions", key: "actions",
render: (text, record) => ( render: (text, record) => (
<Space> <Space wrap>
{record.exported ? ( {record.exported ? (
<Button disabled>{t("bills.actions.edit")}</Button> <Button disabled>{t("bills.actions.edit")}</Button>
) : ( ) : (
@@ -126,6 +128,15 @@ export function BillsListTableComponent({
</Link> </Link>
)} )}
<BillDeleteButton bill={record} /> <BillDeleteButton bill={record} />
{record.isinhouse && (
<PrintWrapperComponent
templateObject={{
name: Templates.inhouse_invoice.key,
variables: { id: record.id },
}}
messageObject={{ subject: Templates.inhouse_invoice.subject }}
/>
)}
</Space> </Space>
), ),
}, },
@@ -275,7 +286,7 @@ export function BillsListTableComponent({
.map((i) => { .map((i) => {
return { return {
line_desc: i.line_desc, line_desc: i.line_desc,
// db_price: i.actual_price, // db_price: i.actual_price,
act_price: i.actual_price, act_price: i.actual_price,
quantity: i.quantity, quantity: i.quantity,
joblineid: i.joblineid, joblineid: i.joblineid,

View File

@@ -246,6 +246,8 @@ export function JobLinesComponent({
sorter: (a, b) => alphaSort(a.status, b.status), sorter: (a, b) => alphaSort(a.status, b.status),
sortOrder: sortOrder:
state.sortedInfo.columnKey === "status" && state.sortedInfo.order, state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
filteredValue: state.filteredInfo.status || null,
filters: filters:
(jobLines && (jobLines &&
jobLines jobLines

View File

@@ -40,7 +40,7 @@ export function PartsOrderListTableComponent({
setPartsReceiveContext, setPartsReceiveContext,
}) { }) {
const responsibilityCenters = bodyshop.md_responsibility_centers; const responsibilityCenters = bodyshop.md_responsibility_centers;
const Templates = TemplateList("partsorder");
const { t } = useTranslation(); const { t } = useTranslation();
const [state, setState] = useState({ const [state, setState] = useState({
sortedInfo: {}, sortedInfo: {},
@@ -105,7 +105,7 @@ export function PartsOrderListTableComponent({
dataIndex: "actions", dataIndex: "actions",
key: "actions", key: "actions",
render: (text, record) => ( render: (text, record) => (
<Space> <Space wrap>
<Button <Button
disabled={jobRO || record.return} disabled={jobRO || record.return}
onClick={() => { onClick={() => {
@@ -165,10 +165,15 @@ export function PartsOrderListTableComponent({
<PrintWrapper <PrintWrapper
templateObject={{ templateObject={{
name: record.isReturn name: record.isReturn
? TemplateList("partsorder").parts_return_slip.key ? Templates.parts_return_slip.key
: TemplateList("partsorder").parts_order.key, : Templates.parts_order.key,
variables: { id: record.id }, variables: { id: record.id },
}} }}
messageObject={{
subject: record.isReturn
? Templates.parts_return_slip.subject
: Templates.parts_order.subject,
}}
/> />
</Space> </Space>
), ),

View File

@@ -1,6 +1,6 @@
import { MailFilled, PrinterFilled } from "@ant-design/icons"; import { MailFilled, PrinterFilled } from "@ant-design/icons";
import { Space } from "antd"; import { Space, Spin } from "antd";
import React from "react"; import React, { useState } from "react";
import { GenerateDocument } from "../../utils/RenderTemplate"; import { GenerateDocument } from "../../utils/RenderTemplate";
export default function PrintWrapperComponent({ export default function PrintWrapperComponent({
@@ -8,15 +8,19 @@ export default function PrintWrapperComponent({
messageObject = {}, messageObject = {},
children, children,
}) { }) {
const [loading, setLoading] = useState(false);
const handlePrint = async (type) => {
setLoading(true);
await GenerateDocument(templateObject, messageObject, type);
setLoading(false);
};
return ( return (
<Space> <Space>
{children || null} {children || null}
<PrinterFilled <PrinterFilled onClick={() => handlePrint("p")} />
onClick={() => GenerateDocument(templateObject, {}, "p")} <MailFilled onClick={() => handlePrint("e")} />
/> {loading && <Spin size="small" />}
<MailFilled
onClick={() => GenerateDocument(templateObject, messageObject, "e")}
/>
</Space> </Space>
); );
} }

View File

@@ -1,11 +1,11 @@
import { useMutation } from "@apollo/client"; import { useMutation } from "@apollo/client";
import { Dropdown, Menu } from "antd"; import { Dropdown, Menu, Spin } from "antd";
import React from "react"; import React, { useState } from "react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { logImEXEvent } from "../../firebase/firebase.utils";
import { UPDATE_JOB } from "../../graphql/jobs.queries"; import { UPDATE_JOB } from "../../graphql/jobs.queries";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import { logImEXEvent } from "../../firebase/firebase.utils";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
@@ -13,21 +13,22 @@ const mapStateToProps = createStructuredSelector({
export function ProductionListColumnStatus({ record, bodyshop }) { export function ProductionListColumnStatus({ record, bodyshop }) {
const [updateJob] = useMutation(UPDATE_JOB); const [updateJob] = useMutation(UPDATE_JOB);
const [loading, setLoading] = useState(false);
const handleSetStatus = (e) => { const handleSetStatus = async (e) => {
logImEXEvent("production_change_status"); logImEXEvent("production_change_status");
e.stopPropagation(); // e.stopPropagation();
setLoading(true);
const { key } = e; const { key } = e;
updateJob({ await updateJob({
variables: { variables: {
jobId: record.id, jobId: record.id,
job: { job: {
status: key, status: key,
}, },
}, },
}).then(() => {
if (record.refetch) record.refetch();
}); });
setLoading(false);
}; };
return ( return (
@@ -42,9 +43,12 @@ export function ProductionListColumnStatus({ record, bodyshop }) {
))} ))}
</Menu> </Menu>
} }
trigger={["contextMenu"]} trigger={["click"]}
> >
<div style={{ width: "100%", height: "19px" }}>{record.status}</div> <div style={{ width: "100%", height: "19px", cursor: "pointer" }}>
{record.status}
{loading && <Spin size="small" />}
</div>
</Dropdown> </Dropdown>
); );
} }

View File

@@ -80,12 +80,13 @@ export function ScheduleEventComponent({
) : null} ) : null}
<Button <Button
onClick={() => { onClick={() => {
const Template = TemplateList("job").appointment_reminder;
GenerateDocument( GenerateDocument(
{ {
name: TemplateList("job").appointment_reminder.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 },
"e" "e"
); );
}} }}

View File

@@ -30,6 +30,7 @@ function ShopEmployeesContainer({ bodyshop }) {
const [updateEmployee] = useMutation(UPDATE_EMPLOYEE); const [updateEmployee] = useMutation(UPDATE_EMPLOYEE);
const [insertEmployees] = useMutation(INSERT_EMPLOYEES); const [insertEmployees] = useMutation(INSERT_EMPLOYEES);
const [deleteEmployee] = useMutation(DELETE_EMPLOYEE); const [deleteEmployee] = useMutation(DELETE_EMPLOYEE);
const handleDelete = (id) => { const handleDelete = (id) => {
logImEXEvent("shop_employee_delete"); logImEXEvent("shop_employee_delete");
@@ -57,7 +58,13 @@ function ShopEmployeesContainer({ bodyshop }) {
logImEXEvent("shop_employee_update"); logImEXEvent("shop_employee_update");
updateEmployee({ updateEmployee({
variables: { id: employeeState[0].id, employee: values }, variables: {
id: employeeState[0].id,
employee: {
...values,
user_email: values.user_email === "" ? null : values.user_email,
},
},
}) })
.then((r) => { .then((r) => {
notification["success"]({ notification["success"]({

View File

@@ -105,6 +105,8 @@ export default function TimeTicketList({
sorter: (a, b) => a.memo - b.memo, sorter: (a, b) => a.memo - b.memo,
sortOrder: sortOrder:
state.sortedInfo.columnKey === "memo" && state.sortedInfo.order, state.sortedInfo.columnKey === "memo" && state.sortedInfo.order,
render: (text, record) =>
record.clockon || record.clockoff ? t(record.memo) : record.memo,
}, },
{ {
title: t("timetickets.fields.clockon"), title: t("timetickets.fields.clockon"),

View File

@@ -40,10 +40,6 @@ export function TimeTicektShiftContainer({
return assoc && assoc.user && assoc.user.employee && assoc.user.employee.id; return assoc && assoc.user && assoc.user.employee && assoc.user.employee.id;
}, [bodyshop, currentUser.email]); }, [bodyshop, currentUser.email]);
console.log(
"🚀 ~ file: time-ticket-shift-form.container.jsx ~ line 42 ~ employeeId",
employeeId
);
const handleFinish = async (values) => { const handleFinish = async (values) => {
setLoading(true); setLoading(true);

View File

@@ -1,5 +1,7 @@
import { useQuery } from "@apollo/client"; import { useQuery } from "@apollo/client";
import { Result } from "antd";
import React, { useMemo } from "react"; import React, { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { QUERY_ACTIVE_SHIFT_TIME_TICKETS } from "../../graphql/timetickets.queries"; import { QUERY_ACTIVE_SHIFT_TIME_TICKETS } from "../../graphql/timetickets.queries";
@@ -28,6 +30,7 @@ export function TimeTicketShiftContainer({
currentUser, currentUser,
isTechConsole, isTechConsole,
}) { }) {
const { t } = useTranslation();
const employeeId = useMemo(() => { const employeeId = useMemo(() => {
const assoc = bodyshop.associations.filter( const assoc = bodyshop.associations.filter(
(a) => a.useremail === currentUser.email (a) => a.useremail === currentUser.email
@@ -48,6 +51,17 @@ export function TimeTicketShiftContainer({
if (loading) return <LoadingSpinner />; if (loading) return <LoadingSpinner />;
if (error) return <AlertComponent message={error.message} type="error" />; if (error) return <AlertComponent message={error.message} type="error" />;
if (!employeeId)
return (
<div>
<Result
status="500"
title={t("timetickets.errors.noemployeeforuser")}
subTitle={t("timetickets.errors.noemployeeforuser_sub")}
/>
</div>
);
return ( return (
<div> <div>
{data.timetickets.length > 0 ? ( {data.timetickets.length > 0 ? (

View File

@@ -44,6 +44,7 @@ export const QUERY_ALL_BILLS_PAGINATED = gql`
total total
invoice_number invoice_number
date date
isinhouse
exported exported
job { job {
id id
@@ -80,6 +81,7 @@ export const QUERY_BILLS_BY_JOBID = gql`
order_date order_date
deliver_by deliver_by
return return
parts_order_lines { parts_order_lines {
id id
act_price act_price
@@ -114,6 +116,7 @@ export const QUERY_BILLS_BY_JOBID = gql`
state_tax_rate state_tax_rate
local_tax_rate local_tax_rate
is_credit_memo is_credit_memo
isinhouse
exported exported
billlines { billlines {
actual_price actual_price
@@ -147,6 +150,7 @@ export const QUERY_BILL_BY_PK = gql`
local_tax_rate local_tax_rate
state_tax_rate state_tax_rate
federal_tax_rate federal_tax_rate
isinhouse
vendor { vendor {
id id
name name

View File

@@ -7,10 +7,12 @@ import { connect } from "react-redux";
import { Link, useHistory, useLocation } from "react-router-dom"; import { Link, useHistory, useLocation } from "react-router-dom";
import BillDeleteButton from "../../components/bill-delete-button/bill-delete-button.component"; import BillDeleteButton from "../../components/bill-delete-button/bill-delete-button.component";
import PartsOrderModalContainer from "../../components/parts-order-modal/parts-order-modal.container"; import PartsOrderModalContainer from "../../components/parts-order-modal/parts-order-modal.container";
import PrintWrapperComponent from "../../components/print-wrapper/print-wrapper.component";
import { setModalContext } from "../../redux/modals/modals.actions"; import { setModalContext } from "../../redux/modals/modals.actions";
import CurrencyFormatter from "../../utils/CurrencyFormatter"; import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { DateFormatter } from "../../utils/DateFormatter"; import { DateFormatter } from "../../utils/DateFormatter";
import { alphaSort } from "../../utils/sorters"; import { alphaSort } from "../../utils/sorters";
import { TemplateList } from "../../utils/TemplateConstants";
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
setPartsOrderContext: (context) => setPartsOrderContext: (context) =>
@@ -34,6 +36,7 @@ export function BillsListPage({
const history = useHistory(); const history = useHistory();
const search = queryString.parse(useLocation().search); const search = queryString.parse(useLocation().search);
const { page } = search; const { page } = search;
const Templates = TemplateList("bill");
const columns = [ const columns = [
{ {
@@ -136,7 +139,7 @@ export function BillsListPage({
dataIndex: "actions", dataIndex: "actions",
key: "actions", key: "actions",
render: (text, record) => ( render: (text, record) => (
<Space> <Space wrap>
<Link to={`/manage/bills?billid=${record.id}`}> <Link to={`/manage/bills?billid=${record.id}`}>
<Button>{t("bills.actions.edit")}</Button> <Button>{t("bills.actions.edit")}</Button>
</Link> </Link>
@@ -153,7 +156,7 @@ export function BillsListPage({
linesToOrder: record.billlines.map((i) => { linesToOrder: record.billlines.map((i) => {
return { return {
line_desc: i.line_desc, line_desc: i.line_desc,
// db_price: i.actual_price, // db_price: i.actual_price,
act_price: i.actual_price, act_price: i.actual_price,
quantity: i.quantity, quantity: i.quantity,
joblineid: i.joblineid, joblineid: i.joblineid,
@@ -167,6 +170,15 @@ export function BillsListPage({
{t("bills.actions.return")} {t("bills.actions.return")}
</Button> </Button>
<BillDeleteButton bill={record} /> <BillDeleteButton bill={record} />
{record.isinhouse && (
<PrintWrapperComponent
templateObject={{
name: Templates.inhouse_invoice.key,
variables: { id: record.id },
}}
messageObject={{ subject: Templates.inhouse_invoice.subject }}
/>
)}
</Space> </Space>
), ),
}, },

View File

@@ -5,9 +5,7 @@ import TimeTicketShift from "../../components/time-ticket-shift/time-ticket-shif
export default function ShiftClock() { export default function ShiftClock() {
return ( return (
<RbacWrapper action="shiftclock:view"> <RbacWrapper action="shiftclock:view">
<div> <TimeTicketShift />
<TimeTicketShift />
</div>
</RbacWrapper> </RbacWrapper>
); );
} }

View File

@@ -9,7 +9,11 @@ import {
import rootReducer from "./root.reducer"; import rootReducer from "./root.reducer";
import rootSaga from "./root.saga"; import rootSaga from "./root.saga";
import * as Sentry from "@sentry/react";
const sentryReduxEnhancer = Sentry.createReduxEnhancer({
// Optionally pass options
});
const sagaMiddleWare = createSagaMiddleware(); const sagaMiddleWare = createSagaMiddleware();
const reduxSyncConfig = { const reduxSyncConfig = {
@@ -32,7 +36,8 @@ const composeEnhancers =
: compose; : compose;
const enhancer = composeEnhancers( const enhancer = composeEnhancers(
applyMiddleware(...middlewares) applyMiddleware(...middlewares),
sentryReduxEnhancer
// other store enhancers if any // other store enhancers if any
); );

View File

@@ -270,6 +270,7 @@
"jobs": { "jobs": {
"admin": "Jobs -> Admin", "admin": "Jobs -> Admin",
"available-list": "Jobs -> Available List", "available-list": "Jobs -> Available List",
"checklist-view": "Jobs -> Checklist View",
"close": "Jobs -> Close", "close": "Jobs -> Close",
"create": "Jobs -> Create", "create": "Jobs -> Create",
"deliver": "Jobs -> Deliver", "deliver": "Jobs -> Deliver",
@@ -1691,7 +1692,9 @@
"clockingin": "Error while clocking in. {{message}}", "clockingin": "Error while clocking in. {{message}}",
"clockingout": "Error while clocking out. {{message}}", "clockingout": "Error while clocking out. {{message}}",
"creating": "Error creating time ticket. {{message}}", "creating": "Error creating time ticket. {{message}}",
"deleting": "Error deleting time ticket. {{message}}" "deleting": "Error deleting time ticket. {{message}}",
"noemployeeforuser": "Unable to use Shift Clock",
"noemployeeforuser_sub": "An employee record has not been created for this user. Please create one before using the shift clock. "
}, },
"fields": { "fields": {
"actualhrs": "Actual Hours", "actualhrs": "Actual Hours",

View File

@@ -270,6 +270,7 @@
"jobs": { "jobs": {
"admin": "", "admin": "",
"available-list": "", "available-list": "",
"checklist-view": "",
"close": "", "close": "",
"create": "", "create": "",
"deliver": "", "deliver": "",
@@ -1691,7 +1692,9 @@
"clockingin": "", "clockingin": "",
"clockingout": "", "clockingout": "",
"creating": "", "creating": "",
"deleting": "" "deleting": "",
"noemployeeforuser": "",
"noemployeeforuser_sub": ""
}, },
"fields": { "fields": {
"actualhrs": "", "actualhrs": "",

View File

@@ -270,6 +270,7 @@
"jobs": { "jobs": {
"admin": "", "admin": "",
"available-list": "", "available-list": "",
"checklist-view": "",
"close": "", "close": "",
"create": "", "create": "",
"deliver": "", "deliver": "",
@@ -1691,7 +1692,9 @@
"clockingin": "", "clockingin": "",
"clockingout": "", "clockingout": "",
"creating": "", "creating": "",
"deleting": "" "deleting": "",
"noemployeeforuser": "",
"noemployeeforuser_sub": ""
}, },
"fields": { "fields": {
"actualhrs": "", "actualhrs": "",

View File

@@ -252,7 +252,6 @@ exports.QUERY_EMPLOYEE_PIN = `query QUERY_EMPLOYEE_PIN($shopId: uuid!, $employee
first_name first_name
employee_number employee_number
id id
cost_center
pin pin
} }
}`; }`;