Compare commits

..

38 Commits

Author SHA1 Message Date
Allan Carr
1a622f1b2c IO-2391 Remote IP comment out till proxy for X-Forwarded-For can be figured out from AWS Load balancer 2023-09-08 13:18:20 -07:00
Allan Carr
bc7d0ef171 IO-2391 Add IP address to Server API Check 2023-09-06 09:06:05 -07:00
Allan Carr
763b199646 Merged in release/2023-08-25 (pull request #954)
Release/2023 08 25
2023-08-25 19:49:35 +00:00
Allan Carr
17905fa844 Merged in feature/IO-2389-Add-Metadata-of-Transaction-Wip (pull request #953)
IO-2389 Metadata to ExportLog
2023-08-25 18:05:01 +00:00
Allan Carr
74a0b78a71 IO-2389 Metadata to ExportLog 2023-08-25 11:05:16 -07:00
Allan Carr
797a423702 Merged in feature/IO-2368-QBO-Successful-Export (pull request #945)
IO-2368 Successful Export Notification for QBO
2023-08-25 17:24:38 +00:00
Allan Carr
2fce8c9644 Merged in feature/IO-2385-Tech-Console-Attendance-Report (pull request #946)
IO-2385 Enabled Emailing of TimeTicket & Attendance from Tech Console
2023-08-25 17:24:23 +00:00
Patrick Fic
9cd39c1c3e Resolve manual appointment old data after submission. 2023-08-25 10:23:22 -07:00
Patrick Fic
6264a2f45c Merge branch 'feature/intellipay' into release/2023-08-25 2023-08-23 13:54:39 -07:00
Allan Carr
9319f492dd IO-2385 Enabled Emailing of TimeTicket & Attendance from Tech Console
Modify print center so that it will/wont display email option if technician is set and production board detail so that it wont display the remove from production / add to scoreboard if technician is set
2023-08-22 13:29:28 -07:00
Allan Carr
8f04c5a12c IO-2368 Successful Export Notification for QBO 2023-08-22 09:15:10 -07:00
Allan Carr
436a41405d Merged in release/2023-08-18 (pull request #944)
Release/2023 08 18
2023-08-18 21:41:04 +00:00
Allan Carr
a2150009db IO-2385 Tech Console to print Attendance Report from Shift Clock 2023-08-18 13:59:42 -07:00
Allan Carr
e1c785322f Merged in feature/IO-2385-Tech-Console-Attendance-Report (pull request #943)
IO-2385 Tech Console to print Attendance Report from Shift Clock
2023-08-18 20:59:23 +00:00
John Allen Delos Reyes
c1d71720ab Merged in feature/IO-2380-parts-order-receive-modal (pull request #934)
Feature/IO-2380 parts order receive modal

Approved-by: Patrick Fic
2023-08-18 18:16:04 +00:00
Allan Carr
89ff7740e2 Merged in feature/IO-2325-Ticket-Create-By (pull request #942)
IO-2325 Add in Created by from Action menu
2023-08-18 16:02:56 +00:00
Allan Carr
4e69fe819e IO-2325 Add in Created by from Action menu 2023-08-18 09:03:13 -07:00
Allan Carr
a8cc3fa190 Merged in feature/IO-2384-Owner-Related-Job-Default-Sort-Order (pull request #939)
IO-2384 Default Sort Order for Related Jobs
2023-08-17 23:19:03 +00:00
Allan Carr
10fceb7ddf IO-2384 Extend to Vehicle Related ROs 2023-08-17 16:18:56 -07:00
Allan Carr
6c1a0cff8d IO-2384 Default Sort Order for Related Jobs 2023-08-17 16:05:33 -07:00
Allan Carr
d92d2cca9a Merged in feature/IO-2247-Dashboard-Components (pull request #938)
IO-2247 Don't push manual appointments into component if they don't have a job associated.
2023-08-17 16:32:37 +00:00
Allan Carr
ea54820bc0 IO-2247 Don't push manual appointments into component if they don't have a job associated. 2023-08-17 09:32:32 -07:00
Allan Carr
62a800a2c0 Merged in feature/IO-2381-Disabled-Cancel-Appt-Button (pull request #937)
IO-2381 Disabled Cancel Appt Button
2023-08-17 01:37:20 +00:00
Allan Carr
9c408d8bf5 IO-2381 Disabled Cancel Appt Button 2023-08-16 18:37:19 -07:00
Allan Carr
45ad09c100 Merged in feature/IO-2383-Timeticket-Employee-Rangefilter (pull request #935)
IO-2383 TimeTicket Employee Range Filter
2023-08-17 00:25:11 +00:00
Allan Carr
dd5cafcd42 IO-2383 TimeTicket Employee Range Filter 2023-08-16 17:25:32 -07:00
swtmply
ddd816e7ca IO-2380 added translation to fields 2023-08-17 06:15:46 +08:00
swtmply
d646e5f285 IO-2380 added part number and price 2023-08-16 11:43:21 +08:00
Allan Carr
ba683a2e8a Merged in release/2023-08-11 (pull request #931)
Release/2023 08 11
2023-08-11 20:36:11 +00:00
Allan Carr
5209c12b89 Merged in feature/IO-2325-Ticket-Create-By (pull request #928)
IO-2325 Shift Clock Created By
2023-08-11 15:36:06 +00:00
Allan Carr
bf7aa17f65 IO-2325 Shift Clock Created By 2023-08-11 08:35:57 -07:00
Allan Carr
cd6e0dcde3 Merged in feature/IO-2325-Ticket-Create-By (pull request #926)
IO-2325 Time Ticket Creation Date
2023-08-10 19:06:58 +00:00
Allan Carr
a2822f5592 Merged in feature/IO-2376-Inactive-Employee-Login (pull request #925)
IO-2376 Prevent Inactive Employee from logging in
2023-08-10 19:06:49 +00:00
Allan Carr
ca129fa4a0 IO-2325 Time Ticket Creation Date 2023-08-10 12:06:47 -07:00
Allan Carr
eee135f4ef IO-2376 Prevent Inactive Employee from logging in 2023-08-09 18:01:28 -07:00
Allan Carr
de92b2d47e Merged in feature/IO-2375-Job-Costing-by-CSR-Filter-Label (pull request #924)
IO-2375 Filter label for Job Costing by CSR correction
2023-08-09 22:26:09 +00:00
Allan Carr
5d7384aa8b IO-2375 Filter label for Job Costing by CSR correction 2023-08-09 15:25:58 -07:00
Patrick Fic
bf18e687da Schema updates for parts dispatch. 2023-08-04 12:36:08 -07:00
52 changed files with 704 additions and 522 deletions

View File

@@ -37301,27 +37301,6 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>inserting</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>
</folder_node>
<folder_node>

View File

@@ -1,15 +1,12 @@
import { DeleteFilled } from "@ant-design/icons";
import { useLazyQuery, useMutation } from "@apollo/client";
import { useMutation, useQuery } from "@apollo/client";
import {
Button,
Card,
Col,
Form,
Input,
InputNumber,
Row,
Space,
Spin,
Statistic,
notification,
} from "antd";
import axios from "axios";
@@ -20,7 +17,7 @@ import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import {
INSERT_PAYMENT_RESPONSE,
QUERY_RO_AND_OWNER_BY_JOB_PKS,
QUERY_RO_AND_OWNER_BY_JOB_PK,
} from "../../graphql/payment_response.queries";
import { INSERT_NEW_PAYMENT } from "../../graphql/payments.queries";
import { insertAuditTrail } from "../../redux/application/application.actions";
@@ -28,8 +25,9 @@ import { toggleModalVisible } from "../../redux/modals/modals.actions";
import { selectCardPayment } from "../../redux/modals/modals.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import CurrencyFormItemComponent from "../form-items-formatted/currency-form-item.component";
import DataLabel from "../data-label/data-label.component";
import JobSearchSelectComponent from "../job-search-select/job-search-select.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
const mapStateToProps = createStructuredSelector({
cardPaymentModal: selectCardPayment,
@@ -51,21 +49,18 @@ const CardPaymentModalComponent = ({
const { context } = cardPaymentModal;
const [form] = Form.useForm();
const amount = Form.useWatch("amount", form);
const jobid = Form.useWatch("jobid", form);
const [loading, setLoading] = useState(false);
const [insertPayment] = useMutation(INSERT_NEW_PAYMENT);
const [insertPaymentResponse] = useMutation(INSERT_PAYMENT_RESPONSE);
const { t } = useTranslation();
const [, { data, refetch, queryLoading }] = useLazyQuery(
QUERY_RO_AND_OWNER_BY_JOB_PKS,
{
variables: { jobids: [context.jobid] },
skip: true,
}
);
const { data, refetch } = useQuery(QUERY_RO_AND_OWNER_BY_JOB_PK, {
variables: { jobid: context?.jobid ?? "" },
skip: !context?.jobid,
});
console.log("🚀 ~ file: card-payment-modal.component..jsx:61 ~ data:", data);
//Initialize the intellipay window.
const SetIntellipayCallbackFunctions = () => {
console.log("*** Set IntelliPay callback functions.");
@@ -81,68 +76,69 @@ const CardPaymentModalComponent = ({
window.intellipay.runOnNonApproval(async function (response) {
// Mutate unsuccessful payment
const { payments } = form.getFieldsValue();
await insertPaymentResponse({
variables: {
paymentResponse: payments.map((payment) => ({
amount: payment.amount,
paymentResponse: {
amount: response.amount,
bodyshopid: bodyshop.id,
jobid: payment.jobid,
jobid: jobid || context.jobid,
declinereason: response.declinereason,
ext_paymentid: response.paymentid.toString(),
successful: false,
response,
})),
},
},
});
payments.forEach((payment) =>
insertAuditTrail({
jobid: payment.jobid,
operation: AuditTrailMapping.failedpayment(),
})
);
insertAuditTrail({
jobid: jobid || context?.jobid,
operation: AuditTrailMapping.failedpayment(),
});
});
};
const handleFinish = async (values) => {
try {
await insertPayment({
const paymentResult = await insertPayment({
variables: {
paymentInput: values.payments.map((payment) => ({
amount: payment.amount,
paymentInput: {
amount: values.amount,
transactionid: (values.paymentResponse.paymentid || "").toString(),
payer: t("payments.labels.customer"),
type: values.paymentResponse.cardbrand,
jobid: payment.jobid,
jobid: values.jobid,
date: moment(Date.now()),
payment_responses: {
data: [
{
amount: payment.amount,
bodyshopid: bodyshop.id,
jobid: payment.jobid,
declinereason: values.paymentResponse.declinereason,
ext_paymentid: values.paymentResponse.paymentid.toString(),
successful: true,
response: values.paymentResponse,
},
],
},
})),
},
},
refetchQueries: ["GET_JOB_BY_PK"],
update(cache, { data }) {
cache.modify({
id: cache.identify({ id: values.jobid, __typename: "jobs" }),
fields: {
payments(cachedPayments) {
return [...data.insert_payments.returning, ...cachedPayments];
},
},
});
},
});
await insertPaymentResponse({
variables: {
paymentResponse: {
amount: values.amount,
bodyshopid: bodyshop.id,
paymentid: paymentResult.data.insert_payments.returning[0].id,
jobid: values.jobid,
declinereason: values.paymentResponse.declinereason,
ext_paymentid: values.paymentResponse.paymentid.toString(),
successful: true,
response: values.paymentResponse,
},
},
});
toggleModalVisible();
} catch (error) {
console.error(error);
notification.open({
type: "error",
message: t("payments.errors.inserting", { error: error.message }),
});
} finally {
setLoading(false);
}
@@ -150,16 +146,9 @@ const CardPaymentModalComponent = ({
const handleIntelliPayCharge = async () => {
setLoading(true);
//Validate
try {
await form.validateFields();
} catch (error) {
setLoading(false);
return;
}
console.warn("*** Window.Intellipay", !!window.intellipay);
try {
const response = await axios.post("/intellipay/lightbox_credentials", {
bodyshop,
refresh: !!window.intellipay,
@@ -193,175 +182,93 @@ const CardPaymentModalComponent = ({
<Form
onFinish={handleFinish}
form={form}
layout="vertical"
initialValues={{
payments: context.jobid ? [{ jobid: context.jobid }] : [],
}}
initialValues={{ jobid: context?.jobid }}
>
<Form.List name={["payments"]}>
{(fields, { add, remove, move }) => {
return (
<div>
{fields.map((field, index) => (
<Form.Item key={field.key}>
<Row gutter={[16, 16]}>
<Col span={16}>
<Form.Item
key={`${index}jobid`}
label={t("jobs.fields.ro_number")}
name={[field.name, "jobid"]}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<JobSearchSelectComponent
notExported={false}
clm_no
/>
</Form.Item>
</Col>
<Col span={6}>
<Form.Item
key={`${index}amount`}
label={t("payments.fields.amount")}
name={[field.name, "amount"]}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<CurrencyFormItemComponent />
</Form.Item>
</Col>
<Col span={2}>
<DeleteFilled
style={{ margin: "1rem" }}
onClick={() => {
remove(field.name);
}}
/>
</Col>
</Row>
</Form.Item>
))}
<Form.Item>
<Button
type="dashed"
onClick={() => {
add();
}}
style={{ width: "100%" }}
>
{t("general.actions.add")}
</Button>
</Form.Item>
</div>
);
}}
</Form.List>
<Form.Item
shouldUpdate={(prevValues, curValues) =>
prevValues.payments?.map((p) => p?.jobid).join() !==
curValues.payments?.map((p) => p?.jobid).join()
}
>
{() => {
console.log("Updating the owner info section.");
//If all of the job ids have been fileld in, then query and update the IP field.
const { payments } = form.getFieldsValue();
if (
payments?.length > 0 &&
payments?.filter((p) => p?.jobid).length === payments?.length
) {
console.log("**Calling refetch.");
refetch({ jobids: payments.map((p) => p.jobid) });
}
console.log(
"Acc info",
data,
payments && data && data.jobs.length > 0
? data.jobs.map((j) => j.ro_number).join(", ")
: null
);
return (
<>
<Input
className="ipayfield"
data-ipayname="account"
//type="hidden"
value={
payments && data && data.jobs.length > 0
? data.jobs.map((j) => j.ro_number).join(", ")
: null
}
hidden
/>
<Input
className="ipayfield"
data-ipayname="email"
// type="hidden"
value={
payments && data && data.jobs.length > 0
? data.jobs.filter((j) => j.ownr_ea)[0]?.ownr_ea
: null
}
hidden
/>
</>
);
}}
</Form.Item>
<Form.Item
shouldUpdate={(prevValues, curValues) =>
prevValues.payments?.map((p) => p?.amount).join() !==
curValues.payments?.map((p) => p?.amount).join()
}
>
{() => {
const { payments } = form.getFieldsValue();
const totalAmountToCharge = payments?.reduce((acc, val) => {
return acc + (val?.amount || 0);
}, 0);
return (
<Space style={{ float: "right" }}>
<Statistic
title="Amount To Charge"
value={totalAmountToCharge}
precision={2}
/>
<Input
className="ipayfield"
data-ipayname="amount"
//type="hidden"
value={totalAmountToCharge?.toFixed(2)}
hidden
/>
<Button
type="primary"
// data-ipayname="submit"
className="ipayfield"
loading={queryLoading || loading}
disabled={!(totalAmountToCharge > 0)}
onClick={handleIntelliPayCharge}
>
{t("job_payments.buttons.proceedtopayment")}
</Button>
</Space>
);
}}
</Form.Item>
<LayoutFormRow grow noDivider>
<Form.Item
name="jobid"
label={t("bills.fields.ro_number")}
rules={[
{
required: true,
// message: t("general.validation.required"),
},
]}
>
<JobSearchSelectComponent
disabled={context?.jobid}
notExported={false}
clm_no
onChange={(e) => {
refetch({ jobid: e });
}}
/>
</Form.Item>
</LayoutFormRow>
{/* Lighbox Input amount needs to be hidden */}
<Input
className="ipayfield"
data-ipayname="amount"
type="hidden"
value={amount}
hidden
/>
<Input
className="ipayfield"
data-ipayname="account"
type="hidden"
value={data?.jobs_by_pk.ro_number}
hidden
/>
<Input
className="ipayfield"
data-ipayname="email"
type="hidden"
value={data?.jobs_by_pk.owner.ownr_ea}
hidden
/>
{/* Lightbox payment response when it is completed */}
<Form.Item name="paymentResponse" hidden>
<Input type="hidden" />
<Input type="hidden" value={amount} />
</Form.Item>
<LayoutFormRow grow>
<Form.Item
label="Amount"
name="amount"
rules={[
{
required: true,
// message: t("general.validation.required"),
},
]}
>
<InputNumber />
</Form.Item>
<Row justify="space-around">
<Button
type="primary"
// data-ipayname="submit"
className="ipayfield"
disabled={!amount || !jobid}
onClick={handleIntelliPayCharge}
>
{t("job_payments.buttons.proceedtopayment")}
</Button>
{context?.balance && (
<DataLabel
valueStyle={{
color: context?.balance.getAmount() !== 0 ? "red" : "green",
}}
label={t("payments.labels.balance")}
>
{context?.balance.toFormat()}
</DataLabel>
)}
</Row>
</LayoutFormRow>
</Form>
</Spin>
</Card>

View File

@@ -43,7 +43,7 @@ function CardPaymentModalContainer({
{t("job_payments.buttons.goback")}
</Button>,
]}
width="80%"
width="60%"
destroyOnClose
>
<CardPaymentModalComponent />

View File

@@ -23,36 +23,40 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
const appt = []; // Flatten Data
data.scheduled_in_today.forEach((item) => {
var i = {
canceled: item.canceled,
id: item.id,
alt_transport: item.job.alt_transport,
clm_no: item.job.clm_no,
jobid: item.job.jobid,
ins_co_nm: item.job.ins_co_nm,
iouparent: item.job.iouparent,
ownerid: item.job.ownerid,
ownr_co_nm: item.job.ownr_co_nm,
ownr_ea: item.job.ownr_ea,
ownr_fn: item.job.ownr_fn,
ownr_ln: item.job.ownr_ln,
ownr_ph1: item.job.ownr_ph1,
ownr_ph2: item.job.ownr_ph2,
production_vars: item.job.production_vars,
ro_number: item.job.ro_number,
suspended: item.job.suspended,
v_make_desc: item.job.v_make_desc,
v_model_desc: item.job.v_model_desc,
v_model_yr: item.job.v_model_yr,
v_vin: item.job.v_vin,
vehicleid: item.job.vehicleid,
note: item.note,
start: moment(item.start).format("hh:mm a"),
title: item.title,
};
appt.push(i);
if (item.job) {
var i = {
canceled: item.canceled,
id: item.id,
alt_transport: item.job.alt_transport,
clm_no: item.job.clm_no,
jobid: item.job.jobid,
ins_co_nm: item.job.ins_co_nm,
iouparent: item.job.iouparent,
ownerid: item.job.ownerid,
ownr_co_nm: item.job.ownr_co_nm,
ownr_ea: item.job.ownr_ea,
ownr_fn: item.job.ownr_fn,
ownr_ln: item.job.ownr_ln,
ownr_ph1: item.job.ownr_ph1,
ownr_ph2: item.job.ownr_ph2,
production_vars: item.job.production_vars,
ro_number: item.job.ro_number,
suspended: item.job.suspended,
v_make_desc: item.job.v_make_desc,
v_model_desc: item.job.v_model_desc,
v_model_yr: item.job.v_model_yr,
v_vin: item.job.v_vin,
vehicleid: item.job.vehicleid,
note: item.note,
start: moment(item.start).format("hh:mm a"),
title: item.title,
};
appt.push(i);
}
});
appt.sort(function (a, b) {
return new moment(a.start) - new moment(b.start);
});
appt.sort ( function (a, b) { return new Date(a.start) - new Date(b.start); });
const columns = [
{
@@ -182,7 +186,12 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
};
return (
<Card title={t("dashboard.titles.scheduledintoday", {date: moment().startOf("day").format("MM/DD/YYYY")})} {...cardProps}>
<Card
title={t("dashboard.titles.scheduledintoday", {
date: moment().startOf("day").format("MM/DD/YYYY"),
})}
{...cardProps}
>
<div style={{ height: "100%" }}>
<Table
onChange={handleTableChange}

View File

@@ -1,10 +1,9 @@
import { useTreatments } from "@splitsoftware/splitio-react";
import Icon, {
BankFilled,
BarChartOutlined,
CarFilled,
ClockCircleFilled,
CheckCircleOutlined,
ClockCircleFilled,
DashboardFilled,
DollarCircleFilled,
ExportOutlined,
@@ -26,6 +25,7 @@ import Icon, {
UnorderedListOutlined,
UserOutlined,
} from "@ant-design/icons";
import { useTreatments } from "@splitsoftware/splitio-react";
import { Layout, Menu } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
@@ -254,7 +254,7 @@ function Header({
onClick={() => {
setCardPaymentContext({
actions: {},
context: {},
context: null,
});
}}
icon={<Icon component={FaCreditCard} />}
@@ -274,7 +274,11 @@ function Header({
onClick={() => {
setTimeTicketContext({
actions: {},
context: {},
context: {
created_by: currentUser.displayName
? currentUser.email.concat(" | ", currentUser.displayName)
: currentUser.email,
},
});
}}
>

View File

@@ -5,6 +5,7 @@ import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { SEARCH_VENDOR_AUTOCOMPLETE_WITH_ADDR } from "../../graphql/vendors.queries";
import { selectTechnician } from "../../redux/tech/tech.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { GenerateDocument } from "../../utils/RenderTemplate";
import { TemplateList } from "../../utils/TemplateConstants";
@@ -13,13 +14,14 @@ import VendorSearchSelect from "../vendor-search-select/vendor-search-select.com
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
technician: selectTechnician,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(mapStateToProps, mapDispatchToProps)(Jobd3RdPartyModal);
export function Jobd3RdPartyModal({ bodyshop, jobId, job }) {
export function Jobd3RdPartyModal({ bodyshop, jobId, job, technician }) {
const [isModalVisible, setIsModalVisible] = useState(false);
const { t } = useTranslation();
const [form] = Form.useForm();
@@ -212,7 +214,9 @@ export function Jobd3RdPartyModal({ bodyshop, jobId, job }) {
]}
>
<Radio.Group>
<Radio value={"e"}>{t("parts_orders.labels.email")}</Radio>
{!technician ? (
<Radio value={"e"}>{t("parts_orders.labels.email")}</Radio>
) : null}
<Radio value={"p"}>{t("parts_orders.labels.print")}</Radio>
</Radio.Group>
</Form.Item>

View File

@@ -29,11 +29,11 @@ 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 OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import ScheduleManualEvent from "../schedule-manual-event/schedule-manual-event.component";
import ScheduleAtChange from "./job-at-change.component";
import ScheduleEventColor from "./schedule-event.color.component";
import ScheduleEventNote from "./schedule-event.note.component";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -208,46 +208,56 @@ export function ScheduleEventComponent({
<Button>{t("appointments.actions.sendreminder")}</Button>
</Dropdown>
) : null}
<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>
}
>
{event.arrived ? (
<Button
// onClick={() => handleCancel(event.id)}
disabled={event.arrived}
>
{t("appointments.actions.cancel")}
</Button>
</Popover>
) : (
<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}

View File

@@ -4,16 +4,15 @@ import axios from "axios";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { useHistory } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { auth } from "../../firebase/firebase.utils";
import { auth, logImEXEvent } from "../../firebase/firebase.utils";
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
import { UPDATE_JOB } from "../../graphql/jobs.queries";
import {
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
import { logImEXEvent } from "../../firebase/firebase.utils";
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
import { useHistory } from "react-router-dom";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -173,6 +172,13 @@ export function JobsCloseExportButton({
});
}
}
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) {
notification.open({
type: "success",
key: "jobsuccessexport",
message: t("jobs.successes.exported"),
});
}
if (setSelectedJobs) {
setSelectedJobs((selectedJobs) => {
return selectedJobs.filter((i) => i !== jobId);

View File

@@ -140,63 +140,67 @@ export function JobsDetailHeaderActions({
<Menu.Item
disabled={job.status !== bodyshop.md_ro_statuses.default_scheduled}
>
<Popover
trigger="click"
disabled={job.status !== bodyshop.md_ro_statuses.default_scheduled}
content={
<Form
layout="vertical"
onFinish={async ({ lost_sale_reason }) => {
const jobUpdate = await cancelAllAppointments({
variables: {
jobid: job.id,
job: {
date_scheduled: null,
scheduled_in: null,
scheduled_completion: null,
lost_sale_reason,
status: bodyshop.md_ro_statuses.default_imported,
{job.status !== bodyshop.md_ro_statuses.default_scheduled ? (
t("menus.jobsactions.cancelallappointments")
) : (
<Popover
trigger="click"
disabled={job.status !== bodyshop.md_ro_statuses.default_scheduled}
content={
<Form
layout="vertical"
onFinish={async ({ lost_sale_reason }) => {
const jobUpdate = await cancelAllAppointments({
variables: {
jobid: job.id,
job: {
date_scheduled: null,
scheduled_in: null,
scheduled_completion: null,
lost_sale_reason,
status: bodyshop.md_ro_statuses.default_imported,
},
},
},
});
if (!jobUpdate.errors) {
notification["success"]({
message: t("appointments.successes.canceled"),
});
return;
}
}}
>
<Form.Item
name="lost_sale_reason"
label={t("jobs.fields.lost_sale_reason")}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
if (!jobUpdate.errors) {
notification["success"]({
message: t("appointments.successes.canceled"),
});
return;
}
}}
>
<Select
options={bodyshop.md_lost_sale_reasons.map((lsr) => ({
label: lsr,
value: lsr,
}))}
/>
</Form.Item>
<Button
htmlType="submit"
disabled={
job.status !== bodyshop.md_ro_statuses.default_scheduled
}
>
{t("appointments.actions.cancel")}
</Button>
</Form>
}
>
{t("menus.jobsactions.cancelallappointments")}
</Popover>
<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"
disabled={
job.status !== bodyshop.md_ro_statuses.default_scheduled
}
>
{t("appointments.actions.cancel")}
</Button>
</Form>
}
>
{t("menus.jobsactions.cancelallappointments")}
</Popover>
)}
</Menu.Item>
<Menu.Item
disabled={
@@ -242,7 +246,12 @@ export function JobsDetailHeaderActions({
setTimeTicketContext({
actions: {},
context: { jobId: job.id },
context: {
jobId: job.id,
created_by: currentUser.displayName
? currentUser.email.concat(" | ", currentUser.displayName)
: currentUser.email,
},
});
}}
>

View File

@@ -169,14 +169,20 @@ export function JobsExportAllButton({
});
}
}
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) {
notification.open({
type: "success",
key: "jobsuccessexport",
message: t("jobs.successes.exported"),
});
}
}
})
);
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) refetch();
if (!!completedCallback) completedCallback([]);
if (!!loadingCallback) loadingCallback(false);
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) refetch();
setLoading(false);
};

View File

@@ -6,6 +6,7 @@ import { Link } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { alphaSort, statusSort } from "../../utils/sorters";
import OwnerDetailUpdateJobsComponent from "../owner-detail-update-jobs/owner-detail-update-jobs.component";
const mapStateToProps = createStructuredSelector({
@@ -15,6 +16,15 @@ const mapStateToProps = createStructuredSelector({
function OwnerDetailJobsComponent({ bodyshop, owner }) {
const { t } = useTranslation();
const [selectedJobs, setSelectedJobs] = useState([]);
const [state, setState] = useState({
sortedInfo: {},
filteredInfo: { text: "" },
});
const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
};
const columns = [
{
title: t("jobs.fields.ro_number"),
@@ -26,6 +36,9 @@ function OwnerDetailJobsComponent({ bodyshop, owner }) {
{record.ro_number || t("general.labels.na")}
</Link>
),
sorter: (a, b) => alphaSort(a.ro_number, b.ro_number),
sortOrder:
state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
},
{
title: t("jobs.fields.vehicle"),
@@ -46,11 +59,17 @@ function OwnerDetailJobsComponent({ bodyshop, owner }) {
title: t("jobs.fields.clm_no"),
dataIndex: "clm_no",
key: "clm_no",
sorter: (a, b) => alphaSort(a.clm_no, b.clm_no),
sortOrder:
state.sortedInfo.columnKey === "clm_no" && state.sortedInfo.order,
},
{
title: t("jobs.fields.status"),
dataIndex: "status",
key: "status",
sorter: (a, b) => statusSort(a.status, b.status, bodyshop.md_ro_statuses.statuses),
sortOrder:
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
},
{
@@ -60,6 +79,9 @@ function OwnerDetailJobsComponent({ bodyshop, owner }) {
render: (text, record) => (
<CurrencyFormatter>{record.clm_total}</CurrencyFormatter>
),
sorter: (a, b) => a.clm_total - b.clm_total,
sortOrder:
state.sortedInfo.columnKey === "clm_total" && state.sortedInfo.order,
},
];
@@ -80,6 +102,7 @@ function OwnerDetailJobsComponent({ bodyshop, owner }) {
scroll={{ x: true }}
rowKey="id"
dataSource={owner.jobs}
onChange={handleTableChange}
rowSelection={{
onSelect: (record, selected, selectedRows) => {
setSelectedJobs(selectedRows ? selectedRows.map((i) => i.id) : []);

View File

@@ -113,6 +113,8 @@ export function PartsOrderListTableComponent({
id: pol.id,
line_desc: pol.line_desc,
quantity: pol.quantity,
act_price: pol.act_price,
oem_partno: pol.oem_partno,
};
}),
},

View File

@@ -79,6 +79,20 @@ export function PartsReceiveModalComponent({ bodyshop, form }) {
>
<Input />
</Form.Item>
<Form.Item
label={t("joblines.fields.oem_partno")}
key={`${index}oem_partno`}
name={[field.name, "oem_partno"]}
>
<Input disabled />
</Form.Item>
<Form.Item
label={t("joblines.fields.act_price")}
key={`${index}act_price`}
name={[field.name, "act_price"]}
>
<Input disabled />
</Form.Item>
<Form.Item
label={t("joblines.fields.location")}
key={`${index}location`}

View File

@@ -1,20 +1,19 @@
import { useMutation } from "@apollo/client";
import { Button, notification } from "antd";
import axios from "axios";
import _ from "lodash";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { Link } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { auth } from "../../firebase/firebase.utils";
import { auth, logImEXEvent } from "../../firebase/firebase.utils";
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
import { UPDATE_BILLS } from "../../graphql/bills.queries";
import {
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
import { logImEXEvent } from "../../firebase/firebase.utils";
import _ from "lodash";
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
import { Link } from "react-router-dom";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -164,6 +163,13 @@ export function PayableExportAll({
});
}
}
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) {
notification.open({
type: "success",
key: "billsuccessexport",
message: t("bills.successes.exported"),
});
}
}
})()
);
@@ -173,7 +179,6 @@ export function PayableExportAll({
if (!!completedCallback) completedCallback([]);
if (!!loadingCallback) loadingCallback(false);
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) refetch();
setLoading(false);
};

View File

@@ -4,16 +4,15 @@ import axios from "axios";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { Link } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { auth } from "../../firebase/firebase.utils";
import { auth, logImEXEvent } from "../../firebase/firebase.utils";
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
import { UPDATE_BILLS } from "../../graphql/bills.queries";
import {
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
import { logImEXEvent } from "../../firebase/firebase.utils";
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
import { Link } from "react-router-dom";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -167,7 +166,13 @@ export function PayableExportButton({
});
}
}
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) refetch();
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) {
notification.open({
type: "success",
key: "billsuccessexport",
message: t("bills.successes.exported"),
});
}
if (setSelectedBills) {
setSelectedBills((selectedBills) => {
@@ -177,6 +182,7 @@ export function PayableExportButton({
}
if (!!loadingCallback) loadingCallback(false);
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) refetch();
setLoading(false);
};

View File

@@ -5,14 +5,7 @@ import {
INSERT_PAYMENT_RESPONSE,
QUERY_PAYMENT_RESPONSE_BY_PAYMENT_ID,
} from "../../graphql/payment_response.queries";
import {
Button,
Descriptions,
InputNumber,
Modal,
Space,
notification,
} from "antd";
import { Button, Descriptions, InputNumber, Modal, notification } from "antd";
import moment from "moment";
import axios from "axios";
import { INSERT_NEW_PAYMENT } from "../../graphql/payments.queries";
@@ -160,17 +153,15 @@ const PaymentExpandedRowComponent = ({ record, bodyshop }) => {
</Descriptions.Item>
{payment_response && (
<Descriptions.Item label={t("job_payments.titles.refundamount")}>
<Space>
<InputNumber
onChange={setRefundAmount}
max={max_refundable_amount}
min={0}
/>
<InputNumber
onChange={setRefundAmount}
max={max_refundable_amount}
min={0}
/>
<Button onClick={() => showConfirm(payment_response)}>
{t("job_payments.buttons.refundpayment")}
</Button>
</Space>
<Button onClick={() => showConfirm(payment_response)}>
{t("job_payments.buttons.refundpayment")}
</Button>
</Descriptions.Item>
)}
</Descriptions>

View File

@@ -172,8 +172,15 @@ export function PaymentExportButton({
});
}
}
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) refetch();
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) {
notification.open({
type: "success",
key: "paymentsuccessexport",
message: t("payments.successes.exported"),
});
}
if (!!loadingCallback) loadingCallback(false);
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) refetch();
setLoading(false);
};

View File

@@ -25,7 +25,7 @@ export function PaymentsExportAllButton({
disabled,
loadingCallback,
completedCallback,
refetch
refetch,
}) {
const { t } = useTranslation();
const [updatePayments] = useMutation(UPDATE_PAYMENTS);
@@ -150,6 +150,13 @@ export function PaymentsExportAllButton({
});
}
}
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) {
notification.open({
type: "success",
key: "paymentsuccessexport",
message: t("payments.successes.exported"),
});
}
}
})()
);

View File

@@ -5,11 +5,13 @@ import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { setEmailOptions } from "../../redux/email/email.actions";
import { selectPrintCenter } from "../../redux/modals/modals.selectors";
import { selectTechnician } from "../../redux/tech/tech.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { GenerateDocument } from "../../utils/RenderTemplate";
const mapStateToProps = createStructuredSelector({
printCenterModal: selectPrintCenter,
bodyshop: selectBodyshop,
technician: selectTechnician,
});
const mapDispatchToProps = (dispatch) => ({
setEmailOptions: (e) => dispatch(setEmailOptions(e)),
@@ -22,6 +24,7 @@ export function PrintCenterItemComponent({
id,
bodyshop,
disabled,
technician,
}) {
const [loading, setLoading] = useState(false);
const { context } = printCenterModal;
@@ -44,19 +47,24 @@ export function PrintCenterItemComponent({
<Space wrap>
{item.title}
<PrinterOutlined onClick={renderToNewWindow} />
<MailOutlined
onClick={() => {
GenerateDocument(
{
name: item.key,
variables: { id: id },
},
{ to: context.job && context.job.ownr_ea, subject: item.subject },
"e",
id
);
}}
/>
{!technician ? (
<MailOutlined
onClick={() => {
GenerateDocument(
{
name: item.key,
variables: { id: id },
},
{
to: context.job && context.job.ownr_ea,
subject: item.subject,
},
"e",
id
);
}}
/>
) : null}
{loading && <Spin />}
</Space>
</li>

View File

@@ -1,31 +1,33 @@
import { PrinterFilled } from "@ant-design/icons";
import { useQuery } from "@apollo/client";
import { Descriptions, Drawer, Space, PageHeader, Button } from "antd";
import { Button, Descriptions, Drawer, PageHeader, Space } from "antd";
import queryString from "query-string";
import React from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { useHistory, useLocation } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { QUERY_JOB_CARD_DETAILS } from "../../graphql/jobs.queries";
import { setModalContext } from "../../redux/modals/modals.actions";
import { selectTechnician } from "../../redux/tech/tech.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { DateFormatter } from "../../utils/DateFormatter";
import AlertComponent from "../alert/alert.component";
import StartChatButton from "../chat-open-button/chat-open-button.component";
import JobAtChange from "../job-at-change/job-at-change.component";
import JobDetailCardsDocumentsComponent from "../job-detail-cards/job-detail-cards.documents.component";
import JobDetailCardsNotesComponent from "../job-detail-cards/job-detail-cards.notes.component";
import JobDetailCardsPartsComponent from "../job-detail-cards/job-detail-cards.parts.component";
import JobEmployeeAssignments from "../job-employee-assignments/job-employee-assignments.container";
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
import ProductionRemoveButton from "../production-remove-button/production-remove-button.component";
import JobAtChange from "../job-at-change/job-at-change.component";
import { PrinterFilled } from "@ant-design/icons";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { setModalContext } from "../../redux/modals/modals.actions";
import ScoreboardAddButton from "../job-scoreboard-add-button/job-scoreboard-add-button.component";
import { selectBodyshop } from "../../redux/user/user.selectors";
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import ProductionRemoveButton from "../production-remove-button/production-remove-button.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
technician: selectTechnician,
});
const mapDispatchToProps = (dispatch) => ({
setPrintCenterContext: (context) =>
@@ -40,6 +42,7 @@ export function ProductionListDetail({
bodyshop,
jobs,
setPrintCenterContext,
technician,
}) {
const search = queryString.parse(useLocation().search);
const history = useHistory();
@@ -66,7 +69,9 @@ export function ProductionListDetail({
title={theJob.ro_number}
extra={
<Space wrap>
<ProductionRemoveButton jobId={theJob.id} />{" "}
{!technician ? (
<ProductionRemoveButton jobId={theJob.id} />
) : null}
<Button
onClick={() => {
setPrintCenterContext({
@@ -82,7 +87,9 @@ export function ProductionListDetail({
<PrinterFilled />
{t("jobs.actions.printCenter")}
</Button>
<ScoreboardAddButton job={data ? data.jobs_by_pk : {}} />
{!technician ? (
<ScoreboardAddButton job={data ? data.jobs_by_pk : {}} />
) : null}
</Space>
}
/>

View File

@@ -59,11 +59,12 @@ export function ScheduleManualEvent({ bodyshop, event }) {
refetchQueries: ["QUERY_ALL_ACTIVE_APPOINTMENTS"],
});
}
form.resetFields();
setVisibility(false);
} catch (error) {
console.log(error);
} finally {
setLoading(false);
setVisibility(false);
}
};

View File

@@ -1,21 +1,25 @@
import { useMutation } from "@apollo/client";
import { Button, Card, Form, notification, Space } from "antd";
import axios from "axios";
import moment from "moment";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { INSERT_NEW_TIME_TICKET } from "../../graphql/timetickets.queries";
import { selectTechnician } from "../../redux/tech/tech.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import TechClockInComponent from "./tech-job-clock-in-form.component";
import TechJobPrintTickets from "../tech-job-print-tickets/tech-job-print-tickets.component";
import moment from "moment";
import { setModalContext } from "../../redux/modals/modals.actions";
import { selectTechnician } from "../../redux/tech/tech.selectors";
import {
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
import TechJobPrintTickets from "../tech-job-print-tickets/tech-job-print-tickets.component";
import TechClockInComponent from "./tech-job-clock-in-form.component";
const mapStateToProps = createStructuredSelector({
technician: selectTechnician,
bodyshop: selectBodyshop,
currentUser: selectCurrentUser,
});
const mapDispatchToProps = (dispatch) => ({
setTimeTicketContext: (context) =>
@@ -25,9 +29,10 @@ export function TechClockInContainer({
setTimeTicketContext,
technician,
bodyshop,
currentUser,
}) {
console.log(
"🚀 ~ file: tech-job-clock-in-form.container.jsx:29 ~ technician:",
"🚀 ~ file: tech-job-clock-in-form.container.jsx:30 ~ technician:",
technician
);
const [form] = Form.useForm();
@@ -66,6 +71,12 @@ export function TechClockInContainer({
values.cost_center
);
}),
created_by: currentUser.email.concat(
" | ",
technician.employee_number
.concat(" ", technician.first_name, " ", technician.last_name)
.trim()
),
},
],
},
@@ -100,13 +111,24 @@ export function TechClockInContainer({
employeeid: technician.id,
flat_rate: emps.flat_rate,
},
created_by: currentUser.email.concat(
" | ",
technician.employee_number
.concat(
" ",
technician.first_name,
" ",
technician.last_name
)
.trim()
),
},
});
}}
>
{t("timetickets.actions.enter")}
</Button>
<TechJobPrintTickets />
<TechJobPrintTickets attendacePrint={false} />
<Button
type="primary"
onClick={() => form.submit()}

View File

@@ -1,4 +1,4 @@
import { Button, Card, DatePicker, Form, Popover, Space } from "antd";
import { Button, Card, DatePicker, Form, Popover, Radio, Space } from "antd";
import moment from "moment";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
@@ -21,12 +21,13 @@ export default connect(
mapDispatchToProps
)(TechJobPrintTickets);
export function TechJobPrintTickets({ technician, event }) {
export function TechJobPrintTickets({ technician, event, attendacePrint }) {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const [form] = Form.useForm();
const [visibility, setVisibility] = useState(false);
const Templates = TemplateList("report_center");
useEffect(() => {
if (visibility && event) {
@@ -44,7 +45,10 @@ export function TechJobPrintTickets({ technician, event }) {
try {
await GenerateDocument(
{
name: TemplateList().timetickets_employee.key,
name:
attendacePrint === true
? Templates.attendance_employee.key
: Templates.timetickets_employee.key,
variables: {
...(start
? { start: moment(start).startOf("day").format("YYYY-MM-DD") }
@@ -60,9 +64,12 @@ export function TechJobPrintTickets({ technician, event }) {
},
{
to: technician.email,
subject: TemplateList().timetickets_employee.subject,
subject:
attendacePrint === true
? Templates.attendance_employee.subject
: Templates.timetickets_employee.subject,
},
"p"
values.sendby // === "email" ? "e" : "p"
);
} catch (error) {
console.log(error);
@@ -92,10 +99,25 @@ export function TechJobPrintTickets({ technician, event }) {
format={"MM/DD/YYYY"}
/>
</Form.Item>
<Form.Item dependencies={["dates"]}>
{() => {
return (
<Form.Item
label={t("general.labels.sendby")}
name="sendby"
initialValue="p"
>
<Radio.Group>
<Radio value="e">{t("general.labels.email")}</Radio>
<Radio value="p">{t("general.labels.print")}</Radio>
</Radio.Group>
</Form.Item>
);
}}
</Form.Item>
<Space wrap>
<Button type="primary" onClick={() => form.submit()}>
{t("general.actions.print")}
{t("reportcenter.actions.generate")}
</Button>
<Button
onClick={() => {
@@ -118,7 +140,7 @@ export function TechJobPrintTickets({ technician, event }) {
return (
<Popover content={overlay} visible={visibility}>
<Button loading={loading} onClick={handleClick}>
{t("general.actions.print")}
{t("general.labels.reports")}
</Button>
</Popover>
);

View File

@@ -9,9 +9,10 @@ import { createStructuredSelector } from "reselect";
import {
selectAuthLevel,
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
import { onlyUnique } from "../../utils/arrayHelper";
import { DateFormatter, DateTimeFormatter } from "../../utils/DateFormatter";
import { onlyUnique } from "../../utils/arrayHelper";
import { alphaSort, dateSort } from "../../utils/sorters";
import RbacWrapper, {
HasRbacAccess,
@@ -20,6 +21,7 @@ import TimeTicketEnterButton from "../time-ticket-enter-button/time-ticket-enter
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
authLevel: selectAuthLevel,
currentUser: selectCurrentUser,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
@@ -29,6 +31,7 @@ export default connect(mapStateToProps, mapDispatchToProps)(TimeTicketList);
export function TimeTicketList({
bodyshop,
authLevel,
currentUser,
disabled,
loading,
timetickets,
@@ -193,7 +196,15 @@ export function TimeTicketList({
}
},
},
{
title: t("timetickets.fields.created_by"),
dataIndex: "created_by",
key: "created_by",
sorter: (a, b) => alphaSort(a.created_by, b.created_by),
sortOrder:
state.sortedInfo.columnKey === "created_by" && state.sortedInfo.order,
render: (text, record) => record.created_by,
},
{
title: t("general.labels.actions"),
dataIndex: "actions",
@@ -254,7 +265,12 @@ export function TimeTicketList({
(techConsole ? null : (
<TimeTicketEnterButton
actions={{ refetch }}
context={{ jobId: jobId }}
context={{
jobId: jobId,
created_by: currentUser.displayName
? currentUser.email.concat(" | ", currentUser.displayName)
: currentUser.email,
}}
disabled={disabled}
>
{t("timetickets.actions.enter")}

View File

@@ -1,5 +1,5 @@
import { useMutation, useQuery } from "@apollo/client";
import { Button, Form, Modal, notification, PageHeader, Space } from "antd";
import { Button, Form, Modal, PageHeader, Space, notification } from "antd";
import moment from "moment";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
@@ -77,6 +77,7 @@ export function TimeTicketModalContainer({
)[0].rate
: null,
bodyshopid: bodyshop.id,
created_by: timeTicketModal.context.created_by,
},
],
},

View File

@@ -4,48 +4,67 @@ import { useTranslation } from "react-i18next";
import { DateTimeFormatter } from "../../utils/DateFormatter";
import DataLabel from "../data-label/data-label.component";
import TechClockOffButton from "../tech-job-clock-out-button/tech-job-clock-out-button.component";
import TechJobPrintTickets from "../tech-job-print-tickets/tech-job-print-tickets.component";
export default function TimeTicketShiftActive({ timetickets, refetch }) {
export default function TimeTicketShiftActive({
timetickets,
refetch,
isTechConsole,
}) {
const { t } = useTranslation();
return (
<div>
{timetickets.length > 0 ? (
<div>
<Typography.Title level={2}>
{t("timetickets.labels.shiftalreadyclockedon")}
</Typography.Title>
<List
grid={{
gutter: 32,
xs: 1,
sm: 2,
md: 3,
lg: 4,
xl: 5,
xxl: 6,
}}
dataSource={timetickets || []}
renderItem={(ticket) => (
<List.Item>
<Card
title={t(ticket.memo)}
actions={[
<TechClockOffButton
jobId={ticket.jobid}
timeTicketId={ticket.id}
completedCallback={refetch}
isShiftTicket
/>,
]}
>
<DataLabel label={t("timetickets.fields.clockon")}>
<DateTimeFormatter>{ticket.clockon}</DateTimeFormatter>
</DataLabel>
</Card>
</List.Item>
)}
></List>
<div
style={{
display: "flex",
justifyContent: "space-between",
flexDirection: "column",
height: "100%",
}}
>
<div style={{ display: "flex", justifyContent: "space-between" }}>
<Typography.Title level={2}>
{t("timetickets.labels.shiftalreadyclockedon")}
</Typography.Title>
{isTechConsole ? (
<TechJobPrintTickets attendacePrint={true} />
) : null}
</div>
<div style={{ flexGrow: 1 }}>
<List
grid={{
gutter: 32,
xs: 1,
sm: 2,
md: 3,
lg: 4,
xl: 5,
xxl: 6,
}}
dataSource={timetickets || []}
renderItem={(ticket) => (
<List.Item>
<Card
title={t(ticket.memo)}
actions={[
<TechClockOffButton
jobId={ticket.jobid}
timeTicketId={ticket.id}
completedCallback={refetch}
isShiftTicket
/>,
]}
>
<DataLabel label={t("timetickets.fields.clockon")}>
<DateTimeFormatter>{ticket.clockon}</DateTimeFormatter>
</DataLabel>
</Card>
</List.Item>
)}
></List>
</div>
</div>
) : null}
</div>

View File

@@ -1,5 +1,5 @@
import { useMutation } from "@apollo/client";
import { Button, Form, notification } from "antd";
import { Button, Form, Space, notification } from "antd";
import axios from "axios";
import moment from "moment";
import React, { useMemo, useState } from "react";
@@ -12,6 +12,7 @@ import {
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
import TechJobPrintTickets from "../tech-job-print-tickets/tech-job-print-tickets.component";
import TimeTicektShiftComponent from "./time-ticket-shift-form.component";
const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser,
@@ -29,6 +30,10 @@ export function TimeTicektShiftContainer({
isTechConsole,
checkIfAlreadyClocked,
}) {
console.log(
"🚀 ~ file: time-ticket-shift-form.container.jsx:28 ~ technician:",
technician
);
const [form] = Form.useForm();
const [insertTimeTicket] = useMutation(INSERT_NEW_TIME_TICKET);
const { t } = useTranslation();
@@ -65,6 +70,21 @@ export function TimeTicektShiftContainer({
clockon: theTime,
date: theTime,
memo: values.memo,
created_by: isTechConsole
? currentUser.email.concat(
" | ",
technician.employee_number
.concat(
" ",
technician.first_name,
" ",
technician.last_name
)
.trim()
)
: currentUser.displayName
? currentUser.email.concat(" | ", currentUser.displayName)
: currentUser.email,
},
],
},
@@ -98,9 +118,14 @@ export function TimeTicektShiftContainer({
initialValues={{ cost_center: t("timetickets.labels.shift") }}
>
<TimeTicektShiftComponent form={form} />
<Button htmlType="submit" loading={loading}>
{t("timetickets.actions.clockin")}
</Button>
<Space wrap>
<Button htmlType="submit" loading={loading} type="primary">
{t("timetickets.actions.clockin")}
</Button>
{isTechConsole === true ? (
<TechJobPrintTickets attendacePrint={true} />
) : null}
</Space>
</Form>
</div>
);

View File

@@ -76,6 +76,7 @@ export function TimeTicketShiftContainer({
<TimeTicketShiftActive
timetickets={data ? data.timetickets : []}
refetch={refetch}
isTechConsole={isTechConsole}
/>
) : (
<TimeTicketShiftFormContainer

View File

@@ -6,8 +6,9 @@ import { Link } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import VehicleDetailUpdateJobsComponent from "../vehicle-detail-update-jobs/vehicle-detail-update-jobs.component";
import { alphaSort, statusSort } from "../../utils/sorters";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import VehicleDetailUpdateJobsComponent from "../vehicle-detail-update-jobs/vehicle-detail-update-jobs.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -16,6 +17,14 @@ const mapStateToProps = createStructuredSelector({
export function VehicleDetailJobsComponent({ vehicle, bodyshop }) {
const { t } = useTranslation();
const [selectedJobs, setSelectedJobs] = useState([]);
const [state, setState] = useState({
sortedInfo: {},
filteredInfo: { text: "" },
});
const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
};
const columns = [
{
@@ -28,6 +37,9 @@ export function VehicleDetailJobsComponent({ vehicle, bodyshop }) {
{record.ro_number || t("general.labels.na")}
</Link>
),
sorter: (a, b) => alphaSort(a.ro_number, b.ro_number),
sortOrder:
state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
},
{
title: t("jobs.fields.owner"),
@@ -43,11 +55,17 @@ export function VehicleDetailJobsComponent({ vehicle, bodyshop }) {
title: t("jobs.fields.clm_no"),
dataIndex: "clm_no",
key: "clm_no",
sorter: (a, b) => alphaSort(a.clm_no, b.clm_no),
sortOrder:
state.sortedInfo.columnKey === "clm_no" && state.sortedInfo.order,
},
{
title: t("jobs.fields.status"),
dataIndex: "status",
key: "status",
sorter: (a, b) => statusSort(a.status, b.status, bodyshop.md_ro_statuses.statuses),
sortOrder:
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
},
{
@@ -57,6 +75,9 @@ export function VehicleDetailJobsComponent({ vehicle, bodyshop }) {
render: (text, record) => (
<CurrencyFormatter>{record.clm_total}</CurrencyFormatter>
),
sorter: (a, b) => a.clm_total - b.clm_total,
sortOrder:
state.sortedInfo.columnKey === "clm_total" && state.sortedInfo.order,
},
];
@@ -76,6 +97,7 @@ export function VehicleDetailJobsComponent({ vehicle, bodyshop }) {
rowKey="id"
scroll={{ x: true }}
dataSource={vehicle.jobs}
onChange={handleTableChange}
rowSelection={{
onSelect: (record, selected, selectedRows) => {
setSelectedJobs(selectedRows ? selectedRows.map((i) => i.id) : []);

View File

@@ -57,6 +57,7 @@ export const GET_LINE_TICKET_BY_PK = gql`
actualhrs
ciecacode
cost_center
created_by
date
id
jobid

View File

@@ -69,7 +69,7 @@ export const QUERY_OWNER_BY_ID = gql`
preferred_contact
note
tax_number
jobs {
jobs(order_by: { date_open: desc }) {
id
ro_number
clm_no

View File

@@ -28,14 +28,16 @@ export const QUERY_PAYMENT_RESPONSE_BY_PAYMENT_ID = gql`
}
`;
export const QUERY_RO_AND_OWNER_BY_JOB_PKS = gql`
query QUERY_RO_AND_OWNER_BY_JOB_PKS($jobids: [uuid!]!) {
jobs(where: { id: { _in: $jobids } }) {
export const QUERY_RO_AND_OWNER_BY_JOB_PK = gql`
query QUERY_RO_AND_OWNER_BY_JOB_PK($jobid: uuid!) {
jobs_by_pk(id: $jobid) {
ro_number
ownr_fn
ownr_ln
ownr_ea
ownr_zip
owner {
ownr_fn
ownr_ln
ownr_ea
ownr_zip
}
}
}
`;

View File

@@ -37,6 +37,7 @@ export const QUERY_TIME_TICKETS_IN_RANGE = gql`
clockon
cost_center
created_at
created_by
date
id
rate
@@ -80,6 +81,7 @@ export const QUERY_TIME_TICKETS_TECHNICIAN_IN_RANGE = gql`
clockon
cost_center
created_at
created_by
date
id
rate
@@ -112,6 +114,7 @@ export const QUERY_TIME_TICKETS_TECHNICIAN_IN_RANGE = gql`
clockon
cost_center
created_at
created_by
date
id
rate
@@ -151,6 +154,7 @@ export const QUERY_TIME_TICKETS_IN_RANGE_SB = gql`
clockon
cost_center
created_at
created_by
date
id
rate
@@ -181,6 +185,7 @@ export const QUERY_TIME_TICKETS_IN_RANGE_SB = gql`
clockon
cost_center
created_at
created_by
date
id
rate
@@ -210,6 +215,7 @@ export const INSERT_NEW_TIME_TICKET = gql`
insert_timetickets(objects: $timeTicketInput) {
returning {
id
created_by
clockon
clockoff
employeeid

View File

@@ -28,11 +28,10 @@ export const QUERY_VEHICLE_BY_ID = gql`
updated_at
trim_color
notes
jobs {
jobs(order_by: { date_open: desc }) {
id
ro_number
ownr_fn
ownr_ln
owner {
id

View File

@@ -1,21 +1,24 @@
import { BackTop, Layout } from "antd";
import React, { lazy, Suspense, useEffect } from "react";
import React, { Suspense, lazy, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { Redirect, Route, Switch } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import ErrorBoundary from "../../components/error-boundary/error-boundary.component";
import FeatureWrapper from "../../components/feature-wrapper/feature-wrapper.component";
import LoadingSpinner from "../../components/loading-spinner/loading-spinner.component";
import TechHeader from "../../components/tech-header/tech-header.component";
import TechSider from "../../components/tech-sider/tech-sider.component";
import { selectTechnician } from "../../redux/tech/tech.selectors";
import FeatureWrapper from "../../components/feature-wrapper/feature-wrapper.component";
import "./tech.page.styles.scss";
import UpdateAlert from "../../components/update-alert/update-alert.component";
import { selectTechnician } from "../../redux/tech/tech.selectors";
import "./tech.page.styles.scss";
const TimeTicketModalContainer = lazy(() =>
import("../../components/time-ticket-modal/time-ticket-modal.container")
);
const EmailOverlayContainer = lazy(() =>
import("../../components/email-overlay/email-overlay.container.jsx")
);
const PrintCenterModalContainer = lazy(() =>
import("../../components/print-center-modal/print-center-modal.container")
);
@@ -69,6 +72,7 @@ export function TechPage({ technician, match }) {
>
<FeatureWrapper featureName="tech-console">
<TimeTicketModalContainer />
<EmailOverlayContainer />
<PrintCenterModalContainer />
<Switch>
<Route

View File

@@ -1093,6 +1093,7 @@
"passwordsdonotmatch": "The passwords you have entered do not match.",
"print": "Print",
"refresh": "Refresh",
"reports": "Reports",
"required": "Required",
"saturday": "Saturday",
"search": "Search...",
@@ -2206,8 +2207,7 @@
},
"errors": {
"exporting": "Error exporting payment(s). {{error}}",
"exporting-partner": "Error exporting to partner. Please check the partner interaction log for more errors.",
"inserting": "Error inserting payment. {{error}}"
"exporting-partner": "Error exporting to partner. Please check the partner interaction log for more errors."
},
"fields": {
"amount": "Amount",
@@ -2736,6 +2736,7 @@
"clockon": "Clocked In",
"committed": "",
"cost_center": "Cost Center",
"created_by": "Created By",
"date": "Ticket Date",
"efficiency": "Efficiency",
"employee": "Employee",

View File

@@ -1093,6 +1093,7 @@
"passwordsdonotmatch": "",
"print": "",
"refresh": "",
"reports": "",
"required": "",
"saturday": "",
"search": "Buscar...",
@@ -2206,8 +2207,7 @@
},
"errors": {
"exporting": "",
"exporting-partner": "",
"inserting": ""
"exporting-partner": ""
},
"fields": {
"amount": "",
@@ -2736,6 +2736,7 @@
"clockon": "",
"committed": "",
"cost_center": "",
"created_by": "",
"date": "",
"efficiency": "",
"employee": "",

View File

@@ -1093,6 +1093,7 @@
"passwordsdonotmatch": "",
"print": "",
"refresh": "",
"reports": "",
"required": "",
"saturday": "",
"search": "Chercher...",
@@ -2206,8 +2207,7 @@
},
"errors": {
"exporting": "",
"exporting-partner": "",
"inserting": ""
"exporting-partner": ""
},
"fields": {
"amount": "",
@@ -2736,6 +2736,7 @@
"clockon": "",
"committed": "",
"cost_center": "",
"created_by": "",
"date": "",
"efficiency": "",
"employee": "",

View File

@@ -1040,7 +1040,7 @@ export const TemplateList = (type, context) => {
disabled: false,
rangeFilter: {
object: i18n.t("reportcenter.labels.objects.jobs"),
field: i18n.t("jobs.fields.date_open"),
field: i18n.t("jobs.fields.date_invoiced"),
},
group: "jobs",
},
@@ -1135,6 +1135,10 @@ export const TemplateList = (type, context) => {
key: "timetickets_employee",
idtype: "employee",
disabled: false,
rangeFilter: {
object: i18n.t("reportcenter.labels.objects.timetickets"),
field: i18n.t("timetickets.fields.date"),
},
group: "payroll",
},
attendance_detail: {

View File

@@ -4649,6 +4649,7 @@
_eq: X-Hasura-User-Id
- active:
_eq: true
allow_aggregations: true
update_permissions:
- role: user
permission:
@@ -4726,6 +4727,7 @@
_eq: X-Hasura-User-Id
- active:
_eq: true
allow_aggregations: true
update_permissions:
- role: user
permission:
@@ -5554,6 +5556,7 @@
- committed_at
- cost_center
- created_at
- created_by
- date
- employeeid
- flat_rate
@@ -5578,6 +5581,7 @@
- committed_at
- cost_center
- created_at
- created_by
- date
- employeeid
- flat_rate
@@ -5611,6 +5615,7 @@
- committed_at
- cost_center
- created_at
- created_by
- date
- employeeid
- flat_rate

View File

@@ -0,0 +1 @@
DROP INDEX IF EXISTS "public"."parts_dispatch_employeeid";

View File

@@ -0,0 +1,2 @@
CREATE INDEX "parts_dispatch_employeeid" on
"public"."parts_dispatch" using btree ("employeeid");

View File

@@ -0,0 +1 @@
DROP INDEX IF EXISTS "public"."parts_dispatch_dispatchid";

View File

@@ -0,0 +1,2 @@
CREATE INDEX "parts_dispatch_dispatchid" on
"public"."parts_dispatch_lines" using btree ("partsdispatchid");

View File

@@ -0,0 +1 @@
DROP INDEX IF EXISTS "public"."parts_dispatch_line_accepted_at";

View File

@@ -0,0 +1,2 @@
CREATE INDEX "parts_dispatch_line_accepted_at" on
"public"."parts_dispatch_lines" using btree ("accepted_at");

View File

@@ -0,0 +1,4 @@
-- Could not auto-generate a down migration.
-- Please write an appropriate down migration for the SQL below:
-- alter table "public"."timetickets" add column "created_by" text
-- null;

View File

@@ -0,0 +1,2 @@
alter table "public"."timetickets" add column "created_by" text
null;

View File

@@ -42,13 +42,13 @@ const io = new Server(server, {
});
exports.io = io;
require("./server/web-sockets/web-socket");
//app.use(fb.validateFirebaseIdToken);
// app.set('trust proxy', true)
// app.use(fb.validateFirebaseIdToken);
app.use(compression());
app.use(cookieParser());
app.use(bodyParser.json({ limit: "50mb" }));
app.use(bodyParser.urlencoded({ limit: "50mb", extended: true }));
//app.use(enforce.HTTPS({ trustProtoHeader: true }));
// app.use(enforce.HTTPS({ trustProtoHeader: true }));
app.use(
cors({ credentials: true, exposedHeaders: ["set-cookie"] })
// cors({
@@ -71,10 +71,17 @@ app.get("/test", async function (req, res) {
const commit = require("child_process").execSync(
"git rev-parse --short HEAD"
);
// console.log(app.get('trust proxy'));
// console.log("remoteAddress", req.socket.remoteAddress);
// console.log("X-Forwarded-For", req.header('x-forwarded-for'));
logger.log("test-api-status", "DEBUG", "api", { commit });
// sendEmail.sendServerEmail({
// subject: `API Check - ${process.env.NODE_ENV}`,
// text: `Server API check has come in. Remote IP: ${req.socket.remoteAddress}, X-Forwarded-For: ${req.header('x-forwarded-for')}`,
// });
sendEmail.sendServerEmail({
subject: `API Check - ${process.env.NODE_ENV}`,
text: `Server API check has come in. `,
text: `Server API check has come in.`,
});
res.status(200).send(`OK - ${commit}`);
});

View File

@@ -643,6 +643,7 @@ async function InsertAccountPostingData(socket) {
wips.push(item);
});
socket.transWips = wips;
const { data: AccountPostingChange } = await axios.post(
PBS_ENDPOINTS.AccountingPostingChange,
@@ -697,6 +698,7 @@ async function MarkJobExported(socket, jobid) {
jobid: jobid,
successful: true,
useremail: socket.user.email,
metadata: socket.transWips,
},
bill: {
exported: true,

View File

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

View File

@@ -23,7 +23,7 @@ exports.techLogin = async (req, res) => {
let technician;
if (result.employees && result.employees[0]) {
const dbRecord = result.employees[0];
if (dbRecord.pin === pin) {
if (dbRecord.pin === pin && dbRecord.active === true) {
valid = true;
delete dbRecord.pin;
technician = dbRecord;