Compare commits

..

42 Commits

Author SHA1 Message Date
swtmply
51e446a04c IO-1412 Fixed column resize 2023-04-11 22:13:38 +08:00
Patrick Fic
146bf95e51 IO-2086 Resolve additional gallery import issue & tech login issue. 2023-04-05 17:22:18 -07:00
Patrick Fic
58defad2ea IO-2086 Resolve imports for additional classes with gallery. 2023-04-05 17:21:47 -07:00
Patrick Fic
9b4247d6f6 Revert "fixed column resize"
This reverts commit 6e7d1abd70.
2023-04-05 14:20:15 -07:00
Patrick Fic
8a6d94f193 Resolve time ticket posting issue. 2023-04-05 14:02:34 -07:00
swtmply
6e7d1abd70 fixed column resize 2023-04-05 02:35:07 +08:00
swtmply
6bd74aae87 document galleries 2023-04-01 03:26:08 +08:00
Patrick Fic
f21caa10fc Merged in release/2023-03-21 (pull request #697)
IO-2213 Resolve issue with filtering on schedule screen.
2023-03-21 16:10:03 +00:00
Patrick Fic
3d164eb070 IO-2213 Resolve issue with filtering on schedule screen. 2023-03-21 09:09:03 -07:00
Patrick Fic
88ab3a21e2 Merged in release/2023-03-17 (pull request #695)
Release/2023 03 17
2023-03-17 18:44:06 +00:00
Patrick Fic
45bc1893a0 Language updates & default for lost sale reason. 2023-03-17 11:23:29 -07:00
Allan Carr
d053e682d7 IO-2193 Delete Owner & Vehicle
Pass error message on deleting to translation
2023-03-16 17:58:12 -07:00
Allan Carr
984a4a4cf6 IO-2193 Delete Owner & Vehicle 2023-03-16 16:44:12 -07:00
Patrick Fic
9438ef9683 IO-2177 Add enforce category on conversion. 2023-03-16 14:15:29 -07:00
Allan Carr
10a354e479 IO-2220 Customer List Report
Customer List report for Loewen Body Shop
2023-03-16 11:04:12 -07:00
Patrick Fic
31092c20a9 Updated hasura metadata 2023-03-16 07:59:40 -07:00
Patrick Fic
86beaf049c IO-2219 Resolve employees not needing job to post time ticket. 2023-03-14 14:41:14 -07:00
Patrick Fic
3bc0653230 IO-2215 Critical Parts scanning.
IO-2215 Critical Parts Scanning

IO-2215 critical parts scanning
2023-03-14 12:30:55 -07:00
Patrick Fic
b950b3f825 IO-2215 Critical Parts Scanning 2023-03-14 08:41:35 -07:00
Patrick Fic
3891fbefdf IO-2215 Critical parts scanning 2023-03-14 08:41:26 -07:00
Patrick Fic
73bcc72fc3 IO-2213 Add Ins Co filtering to several pages. 2023-03-13 14:37:05 -07:00
Patrick Fic
f3911859c7 IO-2214 Add lost sale reason tracking. 2023-03-13 12:07:25 -07:00
Patrick Fic
12e3d61cfb Merged in release/2023-03-10 (pull request #689)
Release/2023 03 10
2023-03-10 22:55:07 +00:00
Patrick Fic
4cba91e097 IO-2203 Resolve RO staying after payment modal close. 2023-03-09 11:23:46 -08:00
Patrick Fic
c695aea12e Autohouse change to respect flat rate employees 2023-03-09 11:08:47 -08:00
Patrick Fic
111f554dea IO-9 Remove license decode button. 2023-03-08 16:26:47 -08:00
Patrick Fic
fe2a731b5f Merged in release/2023-03-03 (pull request #687)
Release/2023 03 03
2023-03-06 19:48:13 +00:00
Patrick Fic
61e6511547 Add missing field to query. 2023-03-06 11:42:58 -08:00
Patrick Fic
85346e203b Update to env file. 2023-03-06 11:41:00 -08:00
Patrick Fic
c565e2199d IO-2195 Add additional check for conversation query. 2023-03-03 09:28:10 -08:00
Patrick Fic
8fa0946cfa IO-2195 Conversation query performance improvements. 2023-03-03 09:00:44 -08:00
Patrick Fic
a1ab254d6f Update hasura metadata. 2023-03-02 20:38:10 -08:00
Patrick Fic
99d3943955 Improve changing active shop profile methods to avoid multiple queries. 2023-03-02 20:04:54 -08:00
Patrick Fic
2415b4c2b4 IO-2194 Static control numbers for CDK. 2023-03-02 19:39:24 -08:00
Patrick Fic
99c7ba1fbc Disable ARMS on server side. 2023-03-02 19:38:37 -08:00
Patrick Fic
36e593f806 Io-2192 Add predefined vehicles list. 2023-03-02 19:06:08 -08:00
Patrick Fic
d825c04850 Package update modifications & IO-2192 Add ability to preselect make and model when creating a job. 2023-03-02 08:50:05 -08:00
Patrick Fic
ca7dfacec4 Package updates. 2023-03-01 13:01:48 -08:00
Patrick Fic
1138540518 Merged in release/2023-02-24 (pull request #682)
Release/2023 02 24
2023-02-27 18:11:31 +00:00
Allan Carr
59db305cb8 IO-2186 Production Over Time
Add new report requested by Loewen Body Shop
2023-02-23 11:14:00 -08:00
Patrick Fic
fea69fe3a5 Add payment response object. 2023-02-22 08:15:58 -08:00
Patrick Fic
43e4ff911e Merged in release/2023-02-17 (pull request #679)
Release/2023 02 17
2023-02-17 23:39:24 +00:00
87 changed files with 14181 additions and 11226 deletions

View File

@@ -3665,6 +3665,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>addpartsrule</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>addspeedprint</name>
<definition_loaded>false</definition_loaded>
@@ -4339,6 +4360,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>dms_control_override</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>dms_wip_acctnumber</name>
<definition_loaded>false</definition_loaded>
@@ -4551,6 +4593,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>enforce_conversion_category</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>enforce_conversion_csr</name>
<definition_loaded>false</definition_loaded>
@@ -5390,6 +5453,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>md_lost_sale_reasons</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>md_parts_order_comment</name>
<definition_loaded>false</definition_loaded>
@@ -5411,6 +5495,53 @@
</translation>
</translations>
</concept_node>
<folder_node>
<name>md_parts_scan</name>
<children>
<concept_node>
<name>expression</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>flags</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>
<concept_node>
<name>md_payment_types</name>
<definition_loaded>false</definition_loaded>
@@ -9588,6 +9719,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>partsscan</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>printlater</name>
<definition_loaded>false</definition_loaded>
@@ -20737,6 +20889,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>sendpartspricechange</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>sendtodms</name>
<definition_loaded>false</definition_loaded>
@@ -21183,6 +21356,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>partspricechange</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>saving</name>
<definition_loaded>false</definition_loaded>
@@ -24132,6 +24326,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>lost_sale_reason</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>ma2s</name>
<definition_loaded>false</definition_loaded>
@@ -40754,6 +40969,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>customer_list</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>cycle_time_analysis</name>
<definition_loaded>false</definition_loaded>
@@ -42140,6 +42376,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>production_over_time</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>psr_by_make</name>
<definition_loaded>false</definition_loaded>
@@ -42161,6 +42418,48 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>purchase_return_ratio_grouped_by_vendor_detail</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>purchase_return_ratio_grouped_by_vendor_summary</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>purchases_by_cost_center_detail</name>
<definition_loaded>false</definition_loaded>
@@ -42696,6 +42995,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>ins_co_nm_filter</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>intake</name>
<definition_loaded>false</definition_loaded>

View File

@@ -8,6 +8,6 @@ REACT_APP_CLOUDINARY_API_KEY=957865933348715
REACT_APP_CLOUDINARY_THUMB_TRANSFORMATIONS=c_fill,h_250,w_250
REACT_APP_FIREBASE_PUBLIC_VAPID_KEY='BG3tzU7L2BXlGZ_3VLK4PNaRceoEXEnmHfxcVbRMF5o5g05ejslhVPki9kBM9cBBT-08Ad9kN3HSpS6JmrWD6h4'
REACT_APP_STRIPE_PUBLIC_KEY=pk_test_51GqB4TJl3nQjrZ0wCQWAxAhlNF8jKe0tipIa6ExBaxwJGitwvFsIZUEua4dUzaMIAuXp4qwYHXx7lgjyQSwP0Pe900vzm38C7g
REACT_APP_AXIOS_BASE_API_URL=https://api.imex.online/
REACT_APP_AXIOS_BASE_API_URL=http://localhost:4000
REACT_APP_REPORTS_SERVER_URL=https://reports3.test.imex.online
REACT_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc

View File

@@ -4,69 +4,70 @@
"private": true,
"proxy": "http://localhost:4000",
"dependencies": {
"@apollo/client": "^3.6.9",
"@apollo/client": "^3.7.9",
"@asseinfo/react-kanban": "^2.2.0",
"@craco/craco": "^6.4.5",
"@craco/craco": "^7.0.0",
"@fingerprintjs/fingerprintjs": "^3.3.3",
"@jsreport/browser-client": "^3.1.0",
"@sentry/react": "^7.28.1",
"@sentry/tracing": "^7.28.1",
"@splitsoftware/splitio-react": "^1.6.0",
"@sentry/react": "^7.40.0",
"@sentry/tracing": "^7.40.0",
"@splitsoftware/splitio-react": "^1.8.1",
"@tanem/react-nprogress": "^5.0.8",
"antd": "^4.22.3",
"apollo-link-logger": "^2.0.0",
"axios": "^0.27.2",
"craco-less": "^1.20.0",
"antd": "^4.24.8",
"apollo-link-logger": "^2.0.1",
"axios": "^1.3.4",
"craco-less": "^2.0.0",
"dinero.js": "^1.9.1",
"dotenv": "^16.0.1",
"enquire-js": "^0.2.1",
"env-cmd": "^10.1.0",
"exifr": "^7.1.3",
"firebase": "^9.9.1",
"graphql": "^16.5.0",
"i18next": "^21.8.14",
"i18next-browser-languagedetector": "^6.1.4",
"firebase": "^9.17.1",
"graphql": "^16.6.0",
"i18next": "^22.4.10",
"i18next-browser-languagedetector": "^7.0.1",
"jsoneditor": "^9.9.0",
"jsreport-browser-client-dist": "^1.3.0",
"libphonenumber-js": "^1.10.9",
"libphonenumber-js": "^1.10.21",
"logrocket": "^3.0.1",
"markerjs2": "^2.22.0",
"markerjs2": "^2.28.1",
"moment-business-days": "^1.2.0",
"moment-timezone": "^0.5.34",
"normalize-url": "^7.0.3",
"phone": "^3.1.23",
"moment-timezone": "^0.5.41",
"normalize-url": "^8.0.0",
"phone": "^3.1.35",
"preval.macro": "^5.0.0",
"prop-types": "^15.8.1",
"query-string": "^7.1.1",
"query-string": "^7.1.3",
"rc-queue-anim": "^2.0.0",
"rc-scroll-anim": "^2.7.6",
"react": "^17.0.2",
"react-big-calendar": "^1.5.0",
"react-big-calendar": "^1.6.8",
"react-color": "^2.19.3",
"react-cookie": "^4.1.1",
"react-dom": "^17.0.2",
"react-drag-listview": "^0.2.1",
"react-grid-gallery": "^0.5.5",
"react-grid-gallery": "^1.0.0",
"react-grid-layout": "^1.3.4",
"react-i18next": "^11.18.1",
"react-icons": "^4.4.0",
"react-number-format": "^4.9.3",
"react-redux": "^7.2.8",
"react-i18next": "^12.2.0",
"react-icons": "^4.7.1",
"react-image-lightbox": "^5.1.4",
"react-number-format": "^5.1.3",
"react-redux": "^8.0.5",
"react-resizable": "^3.0.4",
"react-router-dom": "^5.3.0",
"react-scripts": "^4.0.3",
"react-scripts": "^5.0.1",
"react-sticky": "^6.0.3",
"react-sublime-video": "^0.2.5",
"react-virtualized": "^9.22.3",
"recharts": "^2.1.12",
"redux": "^4.2.0",
"recharts": "^2.4.3",
"redux": "^4.2.1",
"redux-persist": "^6.0.0",
"redux-saga": "^1.1.3",
"redux-saga": "^1.2.2",
"redux-state-sync": "^3.1.4",
"reselect": "^4.1.6",
"sass": "^1.54.0",
"socket.io-client": "^4.5.1",
"styled-components": "^5.3.5",
"reselect": "^4.1.7",
"sass": "^1.58.3",
"socket.io-client": "^4.6.1",
"styled-components": "^5.3.6",
"subscriptions-transport-ws": "^0.11.0",
"web-vitals": "^2.1.4",
"workbox-background-sync": "^6.5.3",

View File

@@ -42,6 +42,7 @@ export function ChatConversationContainer({ bodyshop, selectedConversation }) {
MARK_MESSAGES_AS_READ_BY_CONVERSATION,
{
variables: { conversationId: selectedConversation },
refetchQueries: ["UNREAD_CONVERSATION_COUNT"],
update(cache) {
cache.modify({
id: cache.identify({

View File

@@ -10,7 +10,10 @@ import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { CONVERSATION_LIST_QUERY } from "../../graphql/conversations.queries";
import {
CONVERSATION_LIST_QUERY,
UNREAD_CONVERSATION_COUNT,
} from "../../graphql/conversations.queries";
import { toggleChatVisible } from "../../redux/messaging/messaging.actions";
import {
selectChatVisible,
@@ -37,9 +40,17 @@ export function ChatPopupComponent({
}) {
const { t } = useTranslation();
const [pollInterval, setpollInterval] = useState(0);
const { data: unreadData } = useQuery(UNREAD_CONVERSATION_COUNT, {
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
...(pollInterval > 0 ? { pollInterval } : {}),
});
const { loading, data, refetch, called } = useQuery(CONVERSATION_LIST_QUERY, {
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
skip: !chatVisible,
...(pollInterval > 0 ? { pollInterval } : {}),
});
@@ -57,12 +68,14 @@ export function ChatPopupComponent({
if (called && chatVisible) refetch();
}, [chatVisible, called, refetch]);
const unreadCount = data
? data.conversations.reduce(
(acc, val) => val.messages_aggregate.aggregate.count + acc,
0
)
: 0;
// const unreadCount = data
// ? data.conversations.reduce(
// (acc, val) => val.messages_aggregate.aggregate.count + acc,
// 0
// )
// : 0;
const unreadCount = unreadData?.messages_aggregate.aggregate.count || 0;
return (
<Badge count={unreadCount}>

View File

@@ -4,7 +4,7 @@ import moment from "moment";
import React from "react";
import { useTranslation } from "react-i18next";
import { DateFormatter } from "../../utils/DateFormatter";
import ContractLicenseDecodeButton from "../contract-license-decode-button/contract-license-decode-button.component";
//import ContractLicenseDecodeButton from "../contract-license-decode-button/contract-license-decode-button.component";
import ContractStatusSelector from "../contract-status-select/contract-status-select.component";
import ContractsRatesChangeButton from "../contracts-rates-change-button/contracts-rates-change-button.component";
import CourtesyCarFuelSlider from "../courtesy-car-fuel-select/courtesy-car-fuel-select.component";
@@ -165,7 +165,9 @@ export default function ContractFormComponent({
/>
</div>
)}
<ContractLicenseDecodeButton form={form} />
{
//<ContractLicenseDecodeButton form={form} />
}
</Space>
</div>
<LayoutFormRow header={t("contracts.labels.driverinformation")}>

View File

@@ -3,9 +3,11 @@ import {
Button,
Divider,
Dropdown,
Form,
Menu,
notification,
Popover,
Select,
Space,
} from "antd";
import parsePhoneNumber from "libphonenumber-js";
@@ -59,7 +61,10 @@ export function ScheduleEventComponent({
const blockContent = (
<div>
<Button onClick={() => handleCancel(event.id)} disabled={event.arrived}>
<Button
onClick={() => handleCancel({ id: event.id })}
disabled={event.arrived}
>
{t("appointments.actions.cancel")}
</Button>
</div>
@@ -203,10 +208,46 @@ export function ScheduleEventComponent({
<Button>{t("appointments.actions.sendreminder")}</Button>
</Dropdown>
) : null}
<Button onClick={() => handleCancel(event.id)} disabled={event.arrived}>
{t("appointments.actions.cancel")}
</Button>
<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}
@@ -249,7 +290,7 @@ export function ScheduleEventComponent({
const RegularEvent = event.isintake ? (
<Space
wrap
size='small'
size="small"
style={{
backgroundColor:
event.color && event.color.hex ? event.color.hex : event.color,

View File

@@ -11,7 +11,7 @@ export default function ScheduleEventContainer({ bodyshop, event, refetch }) {
const { t } = useTranslation();
const [cancelAppointment] = useMutation(CANCEL_APPOINTMENT_BY_ID);
const [updateJob] = useMutation(UPDATE_JOB);
const handleCancel = async (id) => {
const handleCancel = async ({ id, lost_sale_reason }) => {
logImEXEvent("schedule_cancel_appt");
const cancelAppt = await cancelAppointment({
@@ -38,7 +38,8 @@ export default function ScheduleEventContainer({ bodyshop, event, refetch }) {
job: {
date_scheduled: null,
scheduled_in: null,
scheduled_completion:null,
scheduled_completion: null,
lost_sale_reason,
status: bodyshop.md_ro_statuses.default_imported,
},
},

View File

@@ -103,7 +103,12 @@ export function JobLinesComponent({
fixed: "left",
key: "line_desc",
sorter: (a, b) => alphaSort(a.line_desc, b.line_desc),
onCell: (record) => ({ className: record.manual_line && "job-line-manual" }),
onCell: (record) => ({
className: record.manual_line && "job-line-manual",
style: {
...(record.critical ? { boxShadow: " -.5em 0 0 #FFC107" } : {}),
},
}),
sortOrder:
state.sortedInfo.columnKey === "line_desc" && state.sortedInfo.order,
ellipsis: true,
@@ -343,7 +348,7 @@ export function JobLinesComponent({
onClick={() => {
setJobLineEditContext({
actions: { refetch: refetch, submit: form && form.submit },
context: record,
context: { ...record, jobid: job.id },
});
}}
>

View File

@@ -14,8 +14,12 @@ import UndefinedToNull from "../../utils/undefinedtonull";
import JobLinesUpdsertModal from "./job-lines-upsert-modal.component";
import Axios from "axios";
import Dinero from "dinero.js";
import CriticalPartsScan from "../../utils/criticalPartsScan";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { useTreatments } from "@splitsoftware/splitio-react";
const mapStateToProps = createStructuredSelector({
jobLineEditModal: selectJobLineEditModal,
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
toggleModalVisible: () => dispatch(toggleModalVisible("jobLineEdit")),
@@ -24,7 +28,13 @@ const mapDispatchToProps = (dispatch) => ({
function JobLinesUpsertModalContainer({
jobLineEditModal,
toggleModalVisible,
bodyshop,
}) {
const { CriticalPartsScanning } = useTreatments(
["CriticalPartsScanning"],
{},
bodyshop.imexshopid
);
const { t } = useTranslation();
const [insertJobLine] = useMutation(INSERT_NEW_JOB_LINE);
const [updateJobLine] = useMutation(UPDATE_JOB_LINE);
@@ -109,6 +119,9 @@ function JobLinesUpsertModalContainer({
}
toggleModalVisible();
}
if (CriticalPartsScanning.treatment === "on") {
CriticalPartsScan(jobLineEditModal.context.jobid);
}
setLoading(false);
};

View File

@@ -3,8 +3,9 @@ import {
useApolloClient,
useLazyQuery,
useMutation,
useQuery,
useQuery
} from "@apollo/client";
import { useTreatments } from "@splitsoftware/splitio-react";
import { Col, notification, Row } from "antd";
import Axios from "axios";
import Dinero from "dinero.js";
@@ -19,7 +20,7 @@ import { logImEXEvent } from "../../firebase/firebase.utils";
import {
DELETE_AVAILABLE_JOB,
QUERY_AVAILABLE_JOBS,
QUERY_AVAILABLE_NEW_JOBS_EST_DATA_BY_PK,
QUERY_AVAILABLE_NEW_JOBS_EST_DATA_BY_PK
} from "../../graphql/available-jobs.queries";
import { INSERT_NEW_JOB, UPDATE_JOB } from "../../graphql/jobs.queries";
import { INSERT_NEW_NOTE } from "../../graphql/notes.queries";
@@ -27,10 +28,11 @@ import { SEARCH_VEHICLE_BY_VIN } from "../../graphql/vehicles.queries";
import { insertAuditTrail } from "../../redux/application/application.actions";
import {
selectBodyshop,
selectCurrentUser,
selectCurrentUser
} from "../../redux/user/user.selectors";
import confirmDialog from "../../utils/asyncConfirm";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import CriticalPartsScan from "../../utils/criticalPartsScan";
import AlertComponent from "../alert/alert.component";
import JobsAvailableScan from "../jobs-available-scan/jobs-available-scan.component";
import JobsFindModalContainer from "../jobs-find-modal/jobs-find-modal.container";
@@ -53,6 +55,11 @@ export function JobsAvailableContainer({
currentUser,
insertAuditTrail,
}) {
const { CriticalPartsScanning } = useTreatments(
["CriticalPartsScanning"],
{},
bodyshop.imexshopid
);
const { loading, error, data, refetch } = useQuery(QUERY_AVAILABLE_JOBS, {
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
@@ -155,6 +162,9 @@ export function JobsAvailableContainer({
},
})
.then((r) => {
if (CriticalPartsScanning.treatment === "on") {
CriticalPartsScan(r.data.insert_jobs.returning[0].id);
}
notification["success"]({
message: t("jobs.successes.created"),
onClick: () => {
@@ -241,7 +251,9 @@ export function JobsAvailableContainer({
},
},
});
if (CriticalPartsScanning.treatment === "on") {
CriticalPartsScan(updateResult.data.update_jobs.returning[0].id);
}
if (updateResult.errors) {
//error while inserting
notification["error"]({

View File

@@ -43,7 +43,7 @@ export function JobsConvertButton({
const { t } = useTranslation();
const [form] = Form.useForm();
const handleConvert = async ({ employee_csr, ...values }) => {
const handleConvert = async ({ employee_csr, category, ...values }) => {
if (parentFormIsFieldsTouched()) {
alert(t("jobs.labels.savebeforeconversion"));
return;
@@ -55,6 +55,7 @@ export function JobsConvertButton({
job: {
converted: true,
...(bodyshop.enforce_conversion_csr ? { employee_csr } : {}),
...(bodyshop.enforce_conversion_category ? { category } : {}),
...values,
},
},
@@ -94,6 +95,7 @@ export function JobsConvertButton({
driveable: true,
towin: false,
employee_csr: job.employee_csr,
category: job.category,
}}
>
<Form.Item
@@ -197,6 +199,26 @@ export function JobsConvertButton({
</Select>
</Form.Item>
)}
{bodyshop.enforce_conversion_category && (
<Form.Item
name={"category"}
label={t("jobs.fields.category")}
rules={[
{
required: bodyshop.enforce_conversion_category,
//message: t("general.validation.required"),
},
]}
>
<Select allowClear>
{bodyshop.md_categories.map((s) => (
<Select.Option key={s} value={s}>
{s}
</Select.Option>
))}
</Select>
</Form.Item>
)}
<Form.Item
label={t("jobs.fields.ca_gst_registrant")}
name="ca_gst_registrant"

View File

@@ -9,7 +9,11 @@ const colSpan = {
lg: { span: 12 },
};
export default function JobsCreateVehicleInfoComponent({ loading, vehicles }) {
export default function JobsCreateVehicleInfoComponent({
loading,
vehicles,
form,
}) {
const [state, setState] = useContext(JobCreateContext);
const { t } = useTranslation();
return (
@@ -58,7 +62,7 @@ export default function JobsCreateVehicleInfoComponent({ loading, vehicles }) {
/>
</Col>
<Col {...colSpan}>
<JobsCreateVehicleInfoNewComponent />
<JobsCreateVehicleInfoNewComponent form={form}/>
</Col>
</Row>
</div>

View File

@@ -20,6 +20,7 @@ export default function JobsCreateVehicleInfoContainer({ form }) {
<JobsCreateVehicleInfoComponent
loading={loading}
vehicles={data ? data.search_vehicles : null}
form={form}
/>
);
}

View File

@@ -4,8 +4,9 @@ import { useTranslation } from "react-i18next";
import JobCreateContext from "../../pages/jobs-create/jobs-create.context";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import JobsCreateVehicleInfoPredefined from "./jobs-create-vehicle-info.predefined.component";
export default function JobsCreateVehicleInfoNewComponent() {
export default function JobsCreateVehicleInfoNewComponent({ form }) {
const [state] = useContext(JobCreateContext);
const { t } = useTranslation();
@@ -25,7 +26,7 @@ export default function JobsCreateVehicleInfoNewComponent() {
<Input disabled={!state.vehicle.new} />
</Form.Item>
</LayoutFormRow>
<LayoutFormRow grow>
<LayoutFormRow grow noDivider>
<Form.Item
label={t("vehicles.fields.v_color")}
name={["vehicle", "data", "v_color"]}
@@ -52,8 +53,9 @@ export default function JobsCreateVehicleInfoNewComponent() {
</Form.Item>
</LayoutFormRow>
<LayoutFormRow grow>
<LayoutFormRow grow noDivider>
<Form.Item
span={10}
label={t("vehicles.fields.v_make_desc")}
name={["vehicle", "data", "v_make_desc"]}
rules={[
@@ -66,6 +68,7 @@ export default function JobsCreateVehicleInfoNewComponent() {
<Input disabled={!state.vehicle.new} />
</Form.Item>
<Form.Item
span={11}
label={t("vehicles.fields.v_model_desc")}
name={["vehicle", "data", "v_model_desc"]}
rules={[
@@ -77,6 +80,11 @@ export default function JobsCreateVehicleInfoNewComponent() {
>
<Input disabled={!state.vehicle.new} />
</Form.Item>
<JobsCreateVehicleInfoPredefined
disabled={!state.vehicle.new}
form={form}
span={1}
/>
</LayoutFormRow>
<LayoutFormRow header={t("vehicles.forms.registration")} grow>

View File

@@ -0,0 +1,81 @@
import { PlusOutlined, SearchOutlined } from "@ant-design/icons";
import { Button, Input, Popover, Table } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import PredefinedVehicles from "./predefined-vehicles.js";
export default function JobsCreateVehicleInfoPredefined({ disabled, form }) {
const [open, setOpen] = useState(false);
const [search, setSearch] = useState("");
const { t } = useTranslation();
const handleOpenChange = (newOpen) => {
setOpen(newOpen);
setSearch("");
};
const filteredPredefinedVehicles =
search === ""
? PredefinedVehicles
: PredefinedVehicles.filter(
(v) =>
v.make.toLowerCase().includes(search.toLowerCase()) ||
v.model.toLowerCase().includes(search.toLowerCase())
);
const popContent = () => (
<div>
<Table
size="small"
title={() => <Input.Search onSearch={(value) => setSearch(value)} />}
dataSource={filteredPredefinedVehicles}
columns={[
{
dataIndex: "make",
key: "make",
title: t("vehicles.fields.v_make_desc"),
},
{
dataIndex: "model",
key: "model",
title: t("vehicles.fields.v_model_desc"),
},
{
dataIndex: "select",
key: "select",
title: t("general.labels.actions"),
render: (value, record) => (
<Button
disabled={disabled}
onClick={() => {
form.setFieldsValue({
vehicle: {
data: {
v_make_desc: record.make,
v_model_desc: record.model,
},
},
});
setOpen(false);
setSearch("");
}}
>
<PlusOutlined />
</Button>
),
},
]}
/>
</div>
);
return (
<Popover
content={popContent}
trigger="click"
open={open}
placement="left"
onOpenChange={handleOpenChange}
destroyTooltipOnHide
>
<SearchOutlined style={{ cursor: "pointer" }} />
</Popover>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -289,6 +289,12 @@ export function JobsDetailGeneral({ bodyshop, jobRO, job, form }) {
>
<Input disabled={jobRO} />
</Form.Item>
<Form.Item
label={t("jobs.fields.lost_sale_reason")}
name="lost_sale_reason"
>
<Input disabled={jobRO} allowClear />
</Form.Item>
</FormRow>
</div>
);

View File

@@ -1,6 +1,15 @@
import { DownCircleFilled } from "@ant-design/icons";
import { useApolloClient, useMutation } from "@apollo/client";
import { Button, Dropdown, Menu, notification, Popconfirm } from "antd";
import {
Button,
Dropdown,
Form,
Menu,
notification,
Popconfirm,
Popover,
Select,
} from "antd";
import React, { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
@@ -127,35 +136,63 @@ export function JobsDetailHeaderActions({
<Menu.Item
disabled={job.status !== bodyshop.md_ro_statuses.default_scheduled}
>
<Popconfirm
title={t("general.labels.areyousure")}
okText="Yes"
cancelText="No"
onClick={(e) => e.stopPropagation()}
<Popover
trigger="click"
disabled={job.status !== bodyshop.md_ro_statuses.default_scheduled}
onConfirm={async () => {
const jobUpdate = await cancelAllAppointments({
variables: {
jobid: job.id,
job: {
date_scheduled: null,
scheduled_in: null,
scheduled_completion: null,
status: bodyshop.md_ro_statuses.default_imported,
},
},
});
if (!jobUpdate.errors) {
notification["success"]({
message: t("appointments.successes.canceled"),
});
return;
}
}}
getPopupContainer={(trigger) => trigger.parentNode}
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"),
},
]}
>
<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")}
</Popconfirm>
</Popover>
</Menu.Item>
<Menu.Item
disabled={

View File

@@ -5,7 +5,7 @@ import { useTranslation } from "react-i18next";
import { logImEXEvent } from "../../firebase/firebase.utils";
import cleanAxios from "../../utils/CleanAxios";
import formatBytes from "../../utils/formatbytes";
import yauzl from "yauzl";
//import yauzl from "yauzl";
import { useTreatments } from "@splitsoftware/splitio-react";
import { connect } from "react-redux";
@@ -69,44 +69,44 @@ export function JobsDocumentsDownloadButton({
setDownload(null);
if (Direct_Media_Download.treatment === "on") {
try {
const parentDir = await window.showDirectoryPicker({
id: "media",
startIn: "downloads",
});
// const parentDir = await window.showDirectoryPicker({
// id: "media",
// startIn: "downloads",
// });
const directory = await parentDir.getDirectoryHandle(identifier, {
create: true,
});
// const directory = await parentDir.getDirectoryHandle(identifier, {
// create: true,
// });
yauzl.fromBuffer(
Buffer.from(theDownloadedZip.data),
{},
(err, zipFile) => {
if (err) throw err;
zipFile.on("entry", (entry) => {
zipFile.openReadStream(entry, async (readErr, readStream) => {
if (readErr) {
zipFile.close();
throw readErr;
}
if (err) throw err;
let fileSystemHandle = await directory.getFileHandle(
entry.fileName,
{
create: true,
}
);
const writable = await fileSystemHandle.createWritable();
readStream.on("data", async function (chunk) {
await writable.write(chunk);
});
readStream.on("end", async function () {
await writable.close();
});
});
});
}
);
// yauzl.fromBuffer(
// Buffer.from(theDownloadedZip.data),
// {},
// (err, zipFile) => {
// if (err) throw err;
// zipFile.on("entry", (entry) => {
// zipFile.openReadStream(entry, async (readErr, readStream) => {
// if (readErr) {
// zipFile.close();
// throw readErr;
// }
// if (err) throw err;
// let fileSystemHandle = await directory.getFileHandle(
// entry.fileName,
// {
// create: true,
// }
// );
// const writable = await fileSystemHandle.createWritable();
// readStream.on("data", async function (chunk) {
// await writable.write(chunk);
// });
// readStream.on("end", async function () {
// await writable.close();
// });
// });
// });
// }
// );
} catch (e) {
console.log(e);
standardMediaDownload(theDownloadedZip.data);

View File

@@ -1,7 +1,7 @@
import { EditFilled, FileExcelFilled, SyncOutlined } from "@ant-design/icons";
import { Button, Card, Col, Row, Space } from "antd";
import React, { useEffect, useState } from "react";
import Gallery from "react-grid-gallery";
import { Gallery } from "react-grid-gallery";
import { useTranslation } from "react-i18next";
import DocumentsUploadComponent from "../documents-upload/documents-upload.component";
import { DetermineFileType } from "../documents-upload/documents-upload.utility";
@@ -11,6 +11,9 @@ import JobsDocumentsGalleryReassign from "./jobs-document-gallery.reassign.compo
import JobsDocumentsDeleteButton from "./jobs-documents-gallery.delete.component";
import JobsDocumentsGallerySelectAllComponent from "./jobs-documents-gallery.selectall.component";
import Lightbox from "react-image-lightbox";
import "react-image-lightbox/style.css";
function JobsDocumentsComponent({
data,
jobId,
@@ -23,11 +26,7 @@ function JobsDocumentsComponent({
}) {
const [galleryImages, setgalleryImages] = useState({ images: [], other: [] });
const { t } = useTranslation();
const [index, setIndex] = useState(0);
const onCurrentImageChange = (index) => {
setIndex(index);
};
const [modalState, setModalState] = useState({ open: false, index: 0 });
useEffect(() => {
let documents = data.reduce(
@@ -37,12 +36,14 @@ function JobsDocumentsComponent({
acc.images.push({
src: GenerateSrcUrl(value),
thumbnail: GenerateThumbUrl(value),
thumbnailHeight: 225,
thumbnailWidth: 225,
// src: GenerateSrcUrl(value),
// thumbnail: GenerateThumbUrl(value),
fullsize: GenerateSrcUrl(value),
height: 225,
width: 225,
isSelected: false,
key: value.key,
extension: value.extension,
id: value.id,
type: value.type,
size: value.size,
@@ -62,7 +63,7 @@ function JobsDocumentsComponent({
const fileName = value.key.split("/").pop();
acc.other.push({
source: GenerateSrcUrl(value),
src: "",
src: thumb,
thumbnail: thumb,
tags: [
{
@@ -85,10 +86,9 @@ function JobsDocumentsComponent({
]
: []),
],
thumbnailHeight: 225,
thumbnailWidth: 225,
height: 225,
width: 225,
isSelected: false,
extension: value.extension,
key: value.key,
id: value.id,
@@ -148,35 +148,15 @@ function JobsDocumentsComponent({
<Card title={t("jobs.labels.documents-images")}>
<Gallery
images={galleryImages.images}
backdropClosesModal={true}
currentImageWillChange={onCurrentImageChange}
customControls={[
<Button
key="edit-button"
style={{
float: "right",
zIndex: "5",
}}
onClick={() => {
const newWindow = window.open(
`${window.location.protocol}//${window.location.host}/edit?documentId=${galleryImages.images[index].id}`,
"_blank",
"noopener,noreferrer"
);
if (newWindow) newWindow.opener = null;
}}
>
<EditFilled />
</Button>,
]}
onClickImage={(props) => {
window.open(
props.target.src,
"_blank",
"toolbar=0,location=0,menubar=0"
);
onClick={(index, item) => {
setModalState({ open: true, index: index });
// window.open(
// item.fullsize,
// "_blank",
// "toolbar=0,location=0,menubar=0"
// );
}}
onSelectImage={(index, image) => {
onSelect={(index, image) => {
setgalleryImages({
...galleryImages,
images: galleryImages.images.map((g, idx) =>
@@ -191,8 +171,6 @@ function JobsDocumentsComponent({
<Card title={t("jobs.labels.documents-other")}>
<Gallery
images={galleryImages.other}
backdropClosesModal={true}
enableLightbox={false}
thumbnailStyle={() => {
return {
backgroundImage: <FileExcelFilled />,
@@ -201,14 +179,14 @@ function JobsDocumentsComponent({
cursor: "pointer",
};
}}
onClickThumbnail={(index) => {
onClick={(index) => {
window.open(
galleryImages.other[index].source,
"_blank",
"toolbar=0,location=0,menubar=0"
);
}}
onSelectImage={(index) => {
onSelect={(index) => {
setgalleryImages({
...galleryImages,
other: galleryImages.other.map((g, idx) =>
@@ -219,6 +197,53 @@ function JobsDocumentsComponent({
/>
</Card>
</Col>
{modalState.open && (
<Lightbox
toolbarButtons={[
<EditFilled
onClick={() => {
const newWindow = window.open(
`${window.location.protocol}//${
window.location.host
}/edit?documentId=${
galleryImages.images[modalState.index].id
}`,
"_blank",
"noopener,noreferrer"
);
if (newWindow) newWindow.opener = null;
}}
/>,
]}
mainSrc={galleryImages.images[modalState.index].fullsize}
nextSrc={
galleryImages.images[
(modalState.index + 1) % galleryImages.images.length
].fullsize
}
prevSrc={
galleryImages.images[
(modalState.index + galleryImages.images.length - 1) %
galleryImages.images.length
].fullsize
}
onCloseRequest={() => setModalState({ open: false, index: 0 })}
onMovePrevRequest={() =>
setModalState({
...modalState,
index:
(modalState.index + galleryImages.images.length - 1) %
galleryImages.images.length,
})
}
onMoveNextRequest={() =>
setModalState({
...modalState,
index: (modalState.index + 1) % galleryImages.images.length,
})
}
/>
)}
</Row>
</div>
);

View File

@@ -1,5 +1,5 @@
import React, { useEffect } from "react";
import Gallery from "react-grid-gallery";
import { Gallery } from "react-grid-gallery";
import { useTranslation } from "react-i18next";
import { GenerateSrcUrl, GenerateThumbUrl } from "./job-documents.utility";
@@ -39,7 +39,7 @@ function JobsDocumentGalleryExternal({
<Gallery
images={galleryImages}
backdropClosesModal={true}
onSelectImage={(index, image) => {
onSelect={(index, image) => {
setgalleryImages(
galleryImages.map((g, idx) =>
index === idx ? { ...g, isSelected: !g.isSelected } : g

View File

@@ -1,7 +1,7 @@
import { SyncOutlined, FileExcelFilled } from "@ant-design/icons";
import { Alert, Button, Card, Space } from "antd";
import React, { useEffect } from "react";
import Gallery from "react-grid-gallery";
import React, { useEffect, useState } from "react";
import { Gallery } from "react-grid-gallery";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
@@ -19,6 +19,9 @@ import JobsLocalGalleryDownloadButton from "./jobs-documents-local-gallery.downl
import JobsDocumentsLocalGalleryReassign from "./jobs-documents-local-gallery.reassign.component";
import JobsDocumentsLocalGallerySelectAllComponent from "./jobs-documents-local-gallery.selectall.component";
import Lightbox from "react-image-lightbox";
import "react-image-lightbox/style.css";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
allMedia: selectAllMedia,
@@ -49,6 +52,7 @@ export function JobsDocumentsLocalGallery({
vendorid,
}) {
const { t } = useTranslation();
const [modalState, setModalState] = useState({ open: false, index: 0 });
useEffect(() => {
if (job) {
if (invoice_number) {
@@ -70,12 +74,20 @@ export function JobsDocumentsLocalGallery({
) {
acc.images.push({
...val,
fullsize: val.src,
src: val.thumbnail,
height: val.thumbnailHeight,
width: val.thumbnailWidth,
...(val.optimized && { src: val.optimized, fullsize: val.src }),
});
if (val.optimized) optimized = true;
} else {
acc.other.push({
...val,
fullsize: val.src,
src: val.thumbnail,
height: val.thumbnailHeight,
width: val.thumbnailWidth,
tags: [{ value: val.filename, title: val.filename }],
});
}
@@ -120,8 +132,7 @@ export function JobsDocumentsLocalGallery({
<Card title={t("jobs.labels.documents-images")}>
<Gallery
images={jobMedia.images}
backdropClosesModal={true}
onSelectImage={(index, image) => {
onSelect={(index, image) => {
toggleMediaSelected({ jobid: job.id, filename: image.filename });
}}
{...(optimized && {
@@ -133,24 +144,23 @@ export function JobsDocumentsLocalGallery({
/>,
],
})}
onClickImage={(props) => {
const media = allMedia[job.id].find(
(m) => m.optimized === props.target.src
);
onClick={(index) => {
setModalState({ open: true, index: index });
// const media = allMedia[job.id].find(
// (m) => m.optimized === item.src
// );
window.open(
media ? media.src : props.target.src,
"_blank",
"toolbar=0,location=0,menubar=0"
);
// window.open(
// media ? media.fullsize : item.fullsize,
// "_blank",
// "toolbar=0,location=0,menubar=0"
// );
}}
/>
</Card>
<Card title={t("jobs.labels.documents-other")}>
<Gallery
images={jobMedia.other}
backdropClosesModal={true}
enableLightbox={false}
thumbnailStyle={() => {
return {
backgroundImage: <FileExcelFilled />,
@@ -159,18 +169,48 @@ export function JobsDocumentsLocalGallery({
cursor: "pointer",
};
}}
onClickThumbnail={(index) => {
onClick={(index) => {
window.open(
jobMedia.other[index].src,
jobMedia.other[index].fullsize,
"_blank",
"toolbar=0,location=0,menubar=0"
);
}}
onSelectImage={(index, image) => {
onSelect={(index, image) => {
toggleMediaSelected({ jobid: job.id, filename: image.filename });
}}
/>
</Card>
{modalState.open && (
<Lightbox
mainSrc={jobMedia.images[modalState.index].fullsize}
nextSrc={
jobMedia.images[(modalState.index + 1) % jobMedia.images.length]
.fullsize
}
prevSrc={
jobMedia.images[
(modalState.index + jobMedia.images.length - 1) %
jobMedia.images.length
].fullsize
}
onCloseRequest={() => setModalState({ open: false, index: 0 })}
onMovePrevRequest={() =>
setModalState({
...modalState,
index:
(modalState.index + jobMedia.images.length - 1) %
jobMedia.images.length,
})
}
onMoveNextRequest={() =>
setModalState({
...modalState,
index: (modalState.index + 1) % jobMedia.images.length,
})
}
/>
)}
</div>
);
}

View File

@@ -1,5 +1,5 @@
import React, { useEffect } from "react";
import Gallery from "react-grid-gallery";
import { Gallery } from "react-grid-gallery";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
@@ -38,7 +38,7 @@ function JobDocumentsLocalGalleryExternal({
const { t } = useTranslation();
useEffect(() => {
if ( jobId) {
if (jobId) {
getJobMedia(jobId);
}
}, [jobId, getJobMedia]);
@@ -65,8 +65,7 @@ function JobDocumentsLocalGalleryExternal({
<div className="clearfix">
<Gallery
images={galleryImages}
backdropClosesModal={true}
onSelectImage={(index, image) => {
onSelect={(index, image) => {
setgalleryImages(
galleryImages.map((g, idx) =>
index === idx ? { ...g, isSelected: !g.isSelected } : g

View File

@@ -260,6 +260,19 @@ export function JobsList({ bodyshop }) {
dataIndex: "ins_co_nm",
key: "ins_co_nm",
ellipsis: true,
filters:
(jobs &&
jobs
.map((j) => j.ins_co_nm)
.filter(onlyUnique)
.map((s) => {
return {
text: s,
value: [s],
};
})) ||
[],
onFilter: (value, record) => value.includes(record.ins_co_nm),
responsive: ["md"],
},
{

View File

@@ -272,6 +272,19 @@ export function JobsReadyList({ bodyshop }) {
dataIndex: "ins_co_nm",
key: "ins_co_nm",
ellipsis: true,
filters:
(jobs &&
jobs
.map((j) => j.ins_co_nm)
.filter(onlyUnique)
.map((s) => {
return {
text: s,
value: [s],
};
})) ||
[],
onFilter: (value, record) => value.includes(record.ins_co_nm),
responsive: ["md"],
},
{

View File

@@ -1,15 +1,40 @@
import { Button, Form, notification, PageHeader } from "antd";
import { Button, Form, notification, PageHeader, Popconfirm } from "antd";
import React, { useState } from "react";
import { useHistory } from "react-router-dom";
import { useMutation } from "@apollo/client";
import { useTranslation } from "react-i18next";
import { UPDATE_OWNER } from "../../graphql/owners.queries";
import { DELETE_OWNER, UPDATE_OWNER } from "../../graphql/owners.queries";
import OwnerDetailFormComponent from "./owner-detail-form.component";
function OwnerDetailFormContainer({ owner, refetch }) {
const { t } = useTranslation();
const [form] = Form.useForm();
const history = useHistory();
const [loading, setLoading] = useState(false);
const [updateOwner] = useMutation(UPDATE_OWNER);
const [deleteOwner] = useMutation(DELETE_OWNER);
const handleDelete = async () => {
setLoading(true);
const result = await deleteOwner({
variables: { id: owner.id },
});
console.log(result);
if (result.errors) {
notification["error"]({
message: t("owners.errors.deleting", {
error: JSON.stringify(result.errors),
}),
});
setLoading(false);
} else {
notification["success"]({
message: t("owners.successes.delete"),
});
setLoading(false);
history.push(`/manage/owners`);
}
};
const handleFinish = async (values) => {
setLoading(true);
@@ -41,15 +66,29 @@ function OwnerDetailFormContainer({ owner, refetch }) {
<>
<PageHeader
title={t("menus.header.owners")}
extra={
extra={[
<Popconfirm
trigger="click"
onConfirm={handleDelete}
disabled={owner.jobs.length !== 0}
title={t("owners.labels.deleteconfirm")}
>
<Button
type="danger"
loading={loading}
disabled={owner.jobs.length !== 0}
>
{t("general.actions.delete")}
</Button>
</Popconfirm>,
<Button
type="primary"
loading={loading}
onClick={() => form.submit()}
>
{t("general.actions.save")}
</Button>
}
</Button>,
]}
/>
<Form
form={form}

View File

@@ -7,14 +7,14 @@ import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import {
INSERT_NEW_PAYMENT,
UPDATE_PAYMENT
UPDATE_PAYMENT,
} from "../../graphql/payments.queries";
import { setEmailOptions } from "../../redux/email/email.actions";
import { toggleModalVisible } from "../../redux/modals/modals.actions";
import { selectPayment } from "../../redux/modals/modals.selectors";
import {
selectBodyshop,
selectCurrentUser
selectCurrentUser,
} from "../../redux/user/user.selectors";
import { GenerateDocument } from "../../utils/RenderTemplate";
import { TemplateList } from "../../utils/TemplateConstants";
@@ -124,7 +124,11 @@ function PaymentModalContainer({
};
useEffect(() => {
if (visible) form.resetFields();
if (visible) {
form.resetFields();
form.resetFields();
form.setFieldsValue(context);
}
}, [visible, form, context]);
useEffect(() => {
@@ -139,6 +143,7 @@ function PaymentModalContainer({
: t("payments.labels.edit")
}
visible={visible}
destroyOnClose
okText={t("general.actions.save")}
onOk={() => form.submit()}
width="50%"

View File

@@ -55,6 +55,7 @@ export function ProductionListTable({
const assoc = bodyshop.associations.find(
(a) => a.useremail === currentUser.email
);
if (assoc) {
await updateDefaultProdView({
variables: { assocId: assoc.id, view: value },

View File

@@ -81,7 +81,7 @@ export function ProductionListTable({
state,
activeStatuses: bodyshop.md_ro_statuses.active_statuses,
}).find((e) => e.key === k.key),
width: k.width,
width: k.width ?? 100,
};
})) ||
[]
@@ -267,6 +267,8 @@ export function ProductionListTable({
sortOrder:
state.sortedInfo.columnKey === c.key && state.sortedInfo.order,
title: headerItem(c),
ellipsis: true,
width: c.width ?? 100,
onHeaderCell: (column) => ({
width: column.width,
onResize: handleResize(index),
@@ -276,11 +278,12 @@ export function ProductionListTable({
rowKey="id"
loading={loading}
dataSource={dataSource}
// scroll={{ x: true }}
scroll={{ x: 1000 }}
onChange={handleTableChange}
/>
</ReactDragListView.DragColumn>
</div>
);
}
export default connect(mapStateToProps, null)(ProductionListTable);

View File

@@ -3,8 +3,18 @@ import { Resizable } from "react-resizable";
export default function ResizableComponent(props) {
const { onResize, width, ...restProps } = props;
if (!width) {
return <th {...restProps} />;
}
return (
<Resizable width={width || 200} height={0} onResize={onResize}>
<Resizable
width={width || 200}
height={0}
onResize={onResize}
draggableOpts={{ enableUserSelectHack: false }}
>
<th {...restProps} />
</Resizable>
);

View File

@@ -39,7 +39,7 @@ export default function ProfileShopsComponent({
),
},
];
console.log("🚀 ~ file: profile-shops.component.jsx:45 ~ data", data);
const filteredData =
search === ""
? data

View File

@@ -3,7 +3,7 @@ import React from "react";
import { logImEXEvent } from "../../firebase/firebase.utils";
import {
QUERY_ALL_ASSOCIATIONS,
UPDATE_ASSOCIATION,
UPDATE_ACTIVE_ASSOCIATION,
} from "../../graphql/associations.queries";
import AlertComponent from "../alert/alert.component";
import ProfileShopsComponent from "./profile-shops.component";
@@ -13,9 +13,13 @@ import { getToken } from "firebase/messaging";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import {
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
currentUser: selectCurrentUser,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
@@ -25,14 +29,18 @@ export default connect(
mapDispatchToProps
)(ProfileShopsContainer);
export function ProfileShopsContainer({ bodyshop }) {
export function ProfileShopsContainer({ bodyshop, currentUser }) {
const { loading, error, data } = useQuery(QUERY_ALL_ASSOCIATIONS, {
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
variables: {
email: currentUser.email,
},
skip: !currentUser,
});
const [updateAssocation] = useMutation(UPDATE_ASSOCIATION);
const [updateActiveAssociation] = useMutation(UPDATE_ACTIVE_ASSOCIATION);
const updateActiveShop = async (activeShopId) => {
const updateActiveShop = async (newActiveAssocId) => {
logImEXEvent("profile_change_active_shop");
try {
@@ -46,16 +54,12 @@ export function ProfileShopsContainer({ bodyshop }) {
} catch (error) {
console.log("No FCM token. Skipping unsubscribe.");
}
await Promise.all(
data.associations.map(async (record) => {
await updateAssocation({
variables: {
assocId: record.id,
assocActive: record.id === activeShopId ? true : false,
},
});
})
);
await updateActiveAssociation({
variables: {
newActiveAssocId: newActiveAssocId,
},
});
//Force window refresh.

View File

@@ -1,5 +1,14 @@
import { SyncOutlined } from "@ant-design/icons";
import { Button, Card, Checkbox, Col, PageHeader, Row, Space } from "antd";
import {
Button,
Card,
Checkbox,
Col,
PageHeader,
Row,
Select,
Space,
} from "antd";
import { t } from "i18next";
import React, { useMemo } from "react";
import useLocalStorage from "../../utils/useLocalStorage";
@@ -9,22 +18,39 @@ import ScheduleModal from "../schedule-job-modal/schedule-job-modal.container";
import ScheduleManualEvent from "../schedule-manual-event/schedule-manual-event.component";
import ScheduleProductionList from "../schedule-production-list/schedule-production-list.component";
import ScheduleVerifyIntegrity from "../schedule-verify-integrity/schedule-verify-integrity.component";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(ScheduleCalendarComponent);
export default function ScheduleCalendarComponent({ data, refetch }) {
export function ScheduleCalendarComponent({ data, refetch, bodyshop }) {
const [filter, setFilter] = useLocalStorage("filter_events", {
intake: true,
manual: true,
employeevacation: true,
ins_co_nm: null,
});
const filteredData = useMemo(() => {
return data.filter(
(d) =>
d.block ||
(filter.intake && d.isintake) ||
(filter.manual && !d.isintake && d.block === false) ||
(d.__typename === "employee_vacation" &&
filter.employeevacation &&
!!d.employee)
(d.block ||
(filter.intake && d.isintake) ||
(filter.manual && !d.isintake && d.block === false) ||
(d.__typename === "employee_vacation" &&
filter.employeevacation &&
!!d.employee)) &&
(filter.ins_co_nm && filter.ins_co_nm.length > 0
? filter.ins_co_nm.includes(d.job?.ins_co_nm)
: true)
);
}, [data, filter]);
@@ -37,6 +63,21 @@ export default function ScheduleCalendarComponent({ data, refetch }) {
extra={
<Space wrap>
<ScheduleAtsSummary appointments={filteredData} />
<Select
style={{ minWidth: "15rem" }}
mode="multiple"
placeholder={t("schedule.labels.ins_co_nm_filter")}
allowClear
onClear={() => setFilter({ ...filter, ins_co_nm: [] })}
value={filter?.ins_co_nm ? filter.ins_co_nm : []}
onChange={(e) => {
setFilter({ ...filter, ins_co_nm: e });
}}
options={bodyshop.md_ins_cos.map((i) => ({
label: i.name,
value: i.name,
}))}
/>
<Checkbox
checked={filter?.intake}
onChange={(e) => {

View File

@@ -148,6 +148,7 @@ export function ScheduleJobModalContainer({
date_scheduled: new Date(),
scheduled_in: values.start,
scheduled_completion: values.scheduled_completion,
lost_sale_reason: null,
},
},
});

View File

@@ -1,3 +1,4 @@
import { useTreatments } from "@splitsoftware/splitio-react";
import { Button, Card, Tabs } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
@@ -8,6 +9,7 @@ import ShopInfoGeneral from "./shop-info.general.component";
import ShopInfoIntakeChecklistComponent from "./shop-info.intake.component";
import ShopInfoLaborRates from "./shop-info.laborrates.component";
import ShopInfoOrderStatusComponent from "./shop-info.orderstatus.component";
import ShopInfoPartsScan from "./shop-info.parts-scan";
import ShopInfoRbacComponent from "./shop-info.rbac.component";
import ShopInfoResponsibilityCenterComponent from "./shop-info.responsibilitycenters.component";
import ShopInfoROStatusComponent from "./shop-info.rostatus.component";
@@ -23,6 +25,11 @@ const mapDispatchToProps = (dispatch) => ({
export default connect(mapStateToProps, mapDispatchToProps)(ShopInfoComponent);
export function ShopInfoComponent({ bodyshop, form, saveLoading }) {
const { CriticalPartsScanning } = useTreatments(
["CriticalPartsScanning"],
{},
bodyshop.imexshopid
);
const { t } = useTranslation();
return (
<Card
@@ -71,6 +78,11 @@ export function ShopInfoComponent({ bodyshop, form, saveLoading }) {
<Tabs.TabPane key="laborrates" tab={t("bodyshop.labels.laborrates")}>
<ShopInfoLaborRates form={form} />
</Tabs.TabPane>
{CriticalPartsScanning.treatment === "on" && (
<Tabs.TabPane key="partsscan" tab={t("bodyshop.labels.partsscan")}>
<ShopInfoPartsScan form={form} />
</Tabs.TabPane>
)}
</Tabs>
</Card>
);

View File

@@ -473,6 +473,13 @@ export default function ShopInfoGeneral({ form }) {
>
<Switch />
</Form.Item>
<Form.Item
name={["enforce_conversion_category"]}
label={t("bodyshop.fields.enforce_conversion_category")}
valuePropName="checked"
>
<Switch />
</Form.Item>
<Form.Item
name={["target_touchtime"]}
label={t("bodyshop.fields.target_touchtime")}

View File

@@ -0,0 +1,81 @@
import { DeleteFilled } from "@ant-design/icons";
import { Button, Form, Input, Space } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
export default function ShopInfoPartsScan({ form }) {
const { t } = useTranslation();
return (
<div>
<LayoutFormRow header={t("bodyshop.labels.md_parts_scan")}>
<Form.List name={["md_parts_scan"]}>
{(fields, { add, remove, move }) => {
return (
<div>
{fields.map((field, index) => (
<Form.Item key={field.key}>
<LayoutFormRow noDivider>
<Form.Item
label={t("bodyshop.fields.md_parts_scan.expression")}
key={`${index}expression`}
name={[field.name, "expression"]}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<Input />
</Form.Item>
<Form.Item
label={t("bodyshop.fields.md_parts_scan.flags")}
key={`${index}flags`}
name={[field.name, "flags"]}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<Input />
</Form.Item>
<Space wrap>
<DeleteFilled
onClick={() => {
remove(field.name);
}}
/>
<FormListMoveArrows
move={move}
index={index}
total={fields.length}
/>
</Space>
</LayoutFormRow>
</Form.Item>
))}
<Form.Item>
<Button
type="dashed"
onClick={() => {
add();
}}
style={{ width: "100%" }}
>
{t("bodyshop.actions.addpartsrule")}
</Button>
</Form.Item>
</div>
);
}}
</Form.List>
</LayoutFormRow>
</div>
);
}

View File

@@ -217,7 +217,9 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) {
{t("jobs.fields.ponumber")}
</Select.Option>
<Select.Option value="account_number">
{t("jobs.fields.dms.control_type.account_number")}
{t(
"jobs.fields.dms.control_type.account_number"
)}
</Select.Option>
</Select>
</Form.Item>
@@ -423,6 +425,15 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) {
<Input onBlur={handleBlur} />
</Form.Item>
)}
{bodyshop.cdk_dealerid && (
<Form.Item
label={t("bodyshop.fields.dms.dms_control_override")}
key={`${index}dms_control_override`}
name={[field.name, "dms_control_override"]}
>
<Input onBlur={handleBlur} />
</Form.Item>
)}
<DeleteFilled
onClick={() => {
@@ -546,6 +557,15 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) {
<Input onBlur={handleBlur} />
</Form.Item>
)}
{bodyshop.cdk_dealerid && (
<Form.Item
label={t("bodyshop.fields.dms.dms_control_override")}
key={`${index}dms_control_override`}
name={[field.name, "dms_control_override"]}
>
<Input onBlur={handleBlur} />
</Form.Item>
)}
<DeleteFilled
onClick={() => {
remove(field.name);

View File

@@ -76,6 +76,19 @@ export default function ShopInfoSchedulingComponent({ form }) {
>
<InputNumber min={0} />
</Form.Item>
<Form.Item
name={["md_lost_sale_reasons"]}
label={t("bodyshop.fields.md_lost_sale_reasons")}
rules={[
{
// required: true,
//message: t("general.validation.required"),
type: "array",
},
]}
>
<Select mode="tags" />
</Form.Item>
</LayoutFormRow>
<Divider orientation="left">{t("bodyshop.labels.workingdays")}</Divider>
<Space wrap size="large">

View File

@@ -42,7 +42,9 @@ export default function ShopUsersAuthEdit({ association }) {
</div>
)}
{!visible && (
<div style={{ cursor: "pointer" }} onClick={() => setVisible(true)}>
<div
style={{ cursor: "pointer" }} //onClick={() => setVisible(true)}
>
{association.authlevel || t("general.labels.na")}
</div>
)}

View File

@@ -26,6 +26,10 @@ export function TechClockInContainer({
technician,
bodyshop,
}) {
console.log(
"🚀 ~ file: tech-job-clock-in-form.container.jsx:29 ~ technician:",
technician
);
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const [insertTimeTicket] = useMutation(INSERT_NEW_TIME_TICKET, {
@@ -33,6 +37,10 @@ export function TechClockInContainer({
});
const { t } = useTranslation();
const emps = bodyshop.employees.filter(
(e) => e.id === (technician && technician.id)
)[0];
const handleFinish = async (values) => {
setLoading(true);
const theTime = (await axios.post("/utils/time")).data;
@@ -87,7 +95,12 @@ export function TechClockInContainer({
onClick={() => {
setTimeTicketContext({
actions: {},
context: { timeticket: { employeeid: technician.id } },
context: {
timeticket: {
employeeid: technician.id,
flat_rate: emps.flat_rate,
},
},
});
}}
>

View File

@@ -59,6 +59,7 @@ export function TechClockOffButton({
emps &&
emps.rates.filter((r) => r.cost_center === values.cost_center)[0]
?.rate,
flat_rate: emps && emps.flat_rate,
ciecacode:
bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber
? values.cost_center

View File

@@ -82,9 +82,10 @@ export function TimeTicketModalComponent({
label={t("timetickets.fields.ro_number")}
rules={[
{
required:
!form.getFieldValue("cost_center") ===
"timetickets.labels.shift",
required: !(
form.getFieldValue("cost_center") ===
"timetickets.labels.shift"
),
//message: t("general.validation.required"),
},
]}

View File

@@ -1,16 +1,41 @@
import React, { useState } from "react";
import { Button, Form, notification, PageHeader } from "antd";
import { Button, Form, notification, PageHeader, Popconfirm } from "antd";
import { useMutation } from "@apollo/client";
import VehicleDetailFormComponent from "./vehicle-detail-form.component";
import { useTranslation } from "react-i18next";
import moment from "moment";
import { UPDATE_VEHICLE } from "../../graphql/vehicles.queries";
import { DELETE_VEHICLE, UPDATE_VEHICLE } from "../../graphql/vehicles.queries";
import { useHistory } from "react-router-dom";
function VehicleDetailFormContainer({ vehicle, refetch }) {
const { t } = useTranslation();
const [updateVehicle] = useMutation(UPDATE_VEHICLE);
const [deleteVehicle] = useMutation(DELETE_VEHICLE);
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const history = useHistory();
const handleDelete = async () => {
setLoading(true);
const result = await deleteVehicle({
variables: { id: vehicle.id },
});
console.log(result);
if (result.errors) {
notification["error"]({
message: t("vehicles.errors.deleting", {
error: JSON.stringify(result.errors),
}),
});
setLoading(false);
} else {
notification["success"]({
message: t("vehicles.successes.delete"),
});
setLoading(false);
history.push(`/manage/vehicles`);
}
};
const handleFinish = async (values) => {
setLoading(true);
@@ -40,15 +65,29 @@ function VehicleDetailFormContainer({ vehicle, refetch }) {
<>
<PageHeader
title={t("menus.header.vehicles")}
extra={
extra={[
<Popconfirm
trigger="click"
onConfirm={handleDelete}
disabled={vehicle.jobs.length !== 0}
title={t("vehicles.labels.deleteconfirm")}
>
<Button
type="danger"
loading={loading}
disabled={vehicle.jobs.length !== 0}
>
{t("general.actions.delete")}
</Button>
</Popconfirm>,
<Button
type="primary"
loading={loading}
onClick={() => form.submit()}
>
{t("general.actions.save")}
</Button>
}
</Button>,
]}
/>
<Form
onFinish={handleFinish}

View File

@@ -268,6 +268,7 @@ export const CANCEL_APPOINTMENTS_BY_JOB_ID = gql`
scheduled_in
scheduled_completion
status
lost_sale_reason
}
}
`;

View File

@@ -1,8 +1,11 @@
import { gql } from "@apollo/client";
export const QUERY_ALL_ASSOCIATIONS = gql`
query QUERY_ALL_ASSOCIATIONS {
associations(order_by: { bodyshop: { shopname: asc } }) {
query QUERY_ALL_ASSOCIATIONS($email: String) {
associations(
where: { useremail: { _eq: $email } }
order_by: { bodyshop: { shopname: asc } }
) {
id
active
bodyshop {
@@ -27,6 +30,30 @@ export const UPDATE_ASSOCIATION = gql`
}
}
`;
export const UPDATE_ACTIVE_ASSOCIATION = gql`
mutation UPDATE_ACTIVE_ASSOCIATION($newActiveAssocId: uuid) {
nweActive: update_associations(
where: { id: { _eq: $newActiveAssocId } }
_set: { active: true }
) {
returning {
id
shopid
active
}
}
inactive: update_associations(
where: { id: { _neq: $newActiveAssocId } }
_set: { active: false }
) {
returning {
id
shopid
active
}
}
}
`;
export const UPDATE_ACTIVE_PROD_LIST_VIEW = gql`
mutation UPDATE_ACTIVE_PROD_LIST_VIEW($assocId: uuid, $view: String) {

View File

@@ -12,6 +12,7 @@ export const QUERY_BODYSHOP = gql`
query QUERY_BODYSHOP {
bodyshops(where: { associations: { active: { _eq: true } } }) {
associations {
id
authlevel
useremail
default_prod_list_view
@@ -112,6 +113,9 @@ export const QUERY_BODYSHOP = gql`
localmediaservernetwork
localmediatoken
enforce_conversion_csr
md_lost_sale_reasons
md_parts_scan
enforce_conversion_category
employees {
user_email
id
@@ -121,6 +125,7 @@ export const QUERY_BODYSHOP = gql`
employee_number
rates
external_id
flat_rate
}
}
}
@@ -222,6 +227,9 @@ export const UPDATE_SHOP = gql`
localmediaservernetwork
localmediatoken
enforce_conversion_csr
md_lost_sale_reasons
md_parts_scan
enforce_conversion_category
employees {
id
first_name

View File

@@ -31,6 +31,18 @@ import { gql } from "@apollo/client";
// }
// `;
export const UNREAD_CONVERSATION_COUNT = gql`
query UNREAD_CONVERSATION_COUNT {
messages_aggregate(
where: { read: { _eq: false }, isoutbound: { _eq: false } }
) {
aggregate {
count
}
}
}
`;
export const CONVERSATION_LIST_QUERY = gql`
query CONVERSATION_LIST_QUERY {
conversations(

View File

@@ -536,6 +536,7 @@ export const GET_JOB_BY_PK = gql`
driveable
towin
loss_of_use
lost_sale_reason
vehicle {
id
plate_no
@@ -721,6 +722,7 @@ export const GET_JOB_BY_PK = gql`
ioucreated
convertedtolbr
ah_detail_line
critical
billlines(limit: 1, order_by: { bill: { date: desc } }) {
id
quantity
@@ -2060,6 +2062,7 @@ export const QUERY_JOB_EXPORT_DMS = gql`
ownr_fn
ownr_ln
ownr_co_nm
ins_co_nm
kmin
kmout
v_make_desc

View File

@@ -94,6 +94,14 @@ export const UPDATE_OWNER = gql`
}
`;
export const DELETE_OWNER = gql`
mutation DELETE_OWNER($id: uuid!) {
delete_owners_by_pk(id: $id) {
id
}
}
`;
export const QUERY_ALL_OWNERS = gql`
query QUERY_ALL_OWNERS {
owners {

View File

@@ -55,6 +55,14 @@ export const UPDATE_VEHICLE = gql`
}
`;
export const DELETE_VEHICLE = gql`
mutation DELETE_VEHICLE($id: uuid!) {
delete_vehicles_by_pk(id: $id) {
id
}
}
`;
export const QUERY_ALL_VEHICLES = gql`
query QUERY_ALL_VEHICLES {
vehicles {

View File

@@ -101,7 +101,10 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader }) {
];
});
});
socket.on("connect_error", (err) => {
console.log(`connect_error due to ${err}`, err);
notification.error({ message: err.message });
});
socket.on("log-event", (payload) => {
setLogs((logs) => {
return [...logs, payload];

View File

@@ -17,7 +17,7 @@ export default function JobsCreateComponent({ form }) {
const steps = [
{
title: t("jobs.labels.create.vehicleinfo"),
content: <JobsCreateVehicleInfoContainer />,
content: <JobsCreateVehicleInfoContainer form={form} />,
validation:
!!state.vehicle.new ||
!!state.vehicle.selectedid ||

View File

@@ -193,9 +193,8 @@ export function Manage({ match, conflict, bodyshop }) {
<Suspense
fallback={<LoadingSpinner message={t("general.labels.loadingapp")} />}
>
<PaymentModalContainer />
<PaymentModalContainer />
<BreadCrumbs />
<BillEnterModalContainer />
<JobCostingModal />

View File

@@ -230,6 +230,7 @@
"addapptcolor": "Add Appointment Color",
"addbucket": "Add Definition",
"addpartslocation": "Add Parts Location",
"addpartsrule": "Add Parts Scan Rule",
"addspeedprint": "Add Speed Print",
"addtemplate": "Add Template",
"newlaborrate": "New Labor Rate",
@@ -270,6 +271,7 @@
"disablebillwip": "Disable bill WIP for A/P Posting",
"disablecontactvehiclecreation": "Disable Contact & Vehicle Updates/Creation",
"dms_acctnumber": "DMS Account #",
"dms_control_override": "Static Control # Override",
"dms_wip_acctnumber": "DMS W.I.P. Account #",
"generic_customer_number": "Generic Customer Number",
"itc_federal": "Federal Tax is ITC?",
@@ -281,6 +283,7 @@
},
"email": "General Shop Email",
"enforce_class": "Enforce Class on Conversion?",
"enforce_conversion_category": "Enforce Category on Conversion?",
"enforce_conversion_csr": "Enforce CSR on Conversion?",
"enforce_referral": "Enforce Referrals",
"federal_tax_id": "Federal Tax ID (GST/HST)",
@@ -328,7 +331,12 @@
"zip": "Zip/Postal Code"
},
"md_jobline_presets": "Jobline Presets",
"md_lost_sale_reasons": "Lost Sale Reasons",
"md_parts_order_comment": "Parts Orders Comments",
"md_parts_scan": {
"expression": "RegEX Expression",
"flags": "Flags"
},
"md_payment_types": "Payment Types",
"md_referral_sources": "Referral Sources",
"messaginglabel": "Messaging Preset Label",
@@ -579,6 +587,7 @@
"notespresets": "Notes Presets",
"orderstatuses": "Order Statuses",
"partslocations": "Parts Locations",
"partsscan": "Critical Parts Scanning",
"printlater": "Print Later",
"qbo": "Use QuickBooks Online?",
"qbo_departmentid": "QBO Department ID",
@@ -1272,6 +1281,7 @@
"removefromproduction": "Remove from Production",
"schedule": "Schedule",
"sendcsi": "Send CSI",
"sendpartspricechange": "",
"sendtodms": "Send to DMS",
"sync": "Sync",
"uninvoice": "Uninvoice",
@@ -1295,6 +1305,7 @@
"nojobselected": "No job is selected.",
"noowner": "No owner associated.",
"novehicle": "No vehicle associated.",
"partspricechange": "",
"saving": "Error encountered while saving record.",
"scanimport": "Error importing job. {{message}}",
"totalscalc": "Error while calculating new job totals.",
@@ -1446,6 +1457,7 @@
"loss_date": "Loss Date",
"loss_desc": "Loss Description",
"loss_of_use": "Loss of Use",
"lost_sale_reason": "Lost Sale Reason",
"ma2s": "2 Stage Paint",
"ma3s": "3 Stage Pain",
"mabl": "MABL?",
@@ -1981,6 +1993,7 @@
"update": "Update Selected Records"
},
"errors": {
"deleting": "Error deleting owner. {{error}}.",
"noaccess": "The record does not exist or you do not have access to it. ",
"saving": "Error saving owner. {{error}}.",
"selectexistingornew": "Select an existing owner record or create a new one. "
@@ -2013,6 +2026,7 @@
},
"labels": {
"create_new": "Create a new owner record.",
"deleteconfirm": "Are you sure you want to delete this owner? This cannot be undone.",
"existing_owners": "Existing Owners",
"fromclaim": "Current Claim",
"fromowner": "Historical Owner Record",
@@ -2020,6 +2034,7 @@
"updateowner": "Update Owner"
},
"successes": {
"delete": "Owner deleted successfully.",
"save": "Owner saved successfully."
}
},
@@ -2419,6 +2434,7 @@
"credits_not_received_date": "Credits not Received by Date",
"credits_not_received_date_vendorid": "Credits not Received by Vendor",
"csi": "CSI Responses",
"customer_list": "Customer List",
"cycle_time_analysis": "Cycle Time Analysis",
"estimates_written_converted": "Estimates Written/Converted",
"estimator_detail": "Jobs by Estimator (Detail)",
@@ -2485,6 +2501,7 @@
"production_by_target_date": "Production by Target Date",
"production_by_technician": "Production by Technician",
"production_by_technician_one": "Production filtered by Technician",
"production_over_time": "Production Level over Time",
"psr_by_make": "Percent of Sales by Vehicle Make",
"purchase_return_ratio_grouped_by_vendor_detail": "Purchase & Return Ratio by Vendor (Detail)",
"purchase_return_ratio_grouped_by_vendor_summary": "Purchase & Return Ratio by Vendor (Summary)",
@@ -2517,6 +2534,7 @@
"labels": {
"atssummary": "ATS Summary",
"employeevacation": "Employee Vacations",
"ins_co_nm_filter": "Filter by Insurance Company",
"intake": "Intake Events",
"manual": "Manual Events",
"manualevent": "Add Manual Event"
@@ -2777,6 +2795,7 @@
},
"vehicles": {
"errors": {
"deleting": "Error deleting vehicle. {{error}}.",
"noaccess": "The vehicle does not exist or you do not have access to it.",
"selectexistingornew": "Select an existing vehicle record or create a new one. ",
"validation": "Please ensure all fields are entered correctly.",
@@ -2812,12 +2831,14 @@
"registration": "Registration"
},
"labels": {
"deleteconfirm": "Are you sure you want to delete this vehicle? This cannot be undone.",
"fromvehicle": "Historical Vehicle Record",
"novehinfo": "No Vehicle Information",
"relatedjobs": "Related Jobs",
"updatevehicle": "Update Vehicle Information"
},
"successes": {
"delete": "Vehicle deleted successfully.",
"save": "Vehicle saved successfully."
}
},

View File

@@ -230,6 +230,7 @@
"addapptcolor": "",
"addbucket": "",
"addpartslocation": "",
"addpartsrule": "",
"addspeedprint": "",
"addtemplate": "",
"newlaborrate": "",
@@ -270,6 +271,7 @@
"disablebillwip": "",
"disablecontactvehiclecreation": "",
"dms_acctnumber": "",
"dms_control_override": "",
"dms_wip_acctnumber": "",
"generic_customer_number": "",
"itc_federal": "",
@@ -281,6 +283,7 @@
},
"email": "",
"enforce_class": "",
"enforce_conversion_category": "",
"enforce_conversion_csr": "",
"enforce_referral": "",
"federal_tax_id": "",
@@ -328,7 +331,12 @@
"zip": ""
},
"md_jobline_presets": "",
"md_lost_sale_reasons": "",
"md_parts_order_comment": "",
"md_parts_scan": {
"expression": "",
"flags": ""
},
"md_payment_types": "",
"md_referral_sources": "",
"messaginglabel": "",
@@ -579,6 +587,7 @@
"notespresets": "",
"orderstatuses": "",
"partslocations": "",
"partsscan": "",
"printlater": "",
"qbo": "",
"qbo_departmentid": "",
@@ -1272,6 +1281,7 @@
"removefromproduction": "",
"schedule": "Programar",
"sendcsi": "",
"sendpartspricechange": "",
"sendtodms": "",
"sync": "",
"uninvoice": "",
@@ -1295,6 +1305,7 @@
"nojobselected": "No hay trabajo seleccionado.",
"noowner": "Ningún propietario asociado.",
"novehicle": "No hay vehículo asociado.",
"partspricechange": "",
"saving": "Se encontró un error al guardar el registro.",
"scanimport": "",
"totalscalc": "",
@@ -1446,6 +1457,7 @@
"loss_date": "Fecha de pérdida",
"loss_desc": "",
"loss_of_use": "",
"lost_sale_reason": "",
"ma2s": "",
"ma3s": "",
"mabl": "",
@@ -1981,6 +1993,7 @@
"update": ""
},
"errors": {
"deleting": "",
"noaccess": "El registro no existe o no tiene acceso a él.",
"saving": "",
"selectexistingornew": ""
@@ -2013,6 +2026,7 @@
},
"labels": {
"create_new": "Crea un nuevo registro de propietario.",
"deleteconfirm": "",
"existing_owners": "Propietarios existentes",
"fromclaim": "",
"fromowner": "",
@@ -2020,6 +2034,7 @@
"updateowner": ""
},
"successes": {
"delete": "",
"save": "Propietario guardado con éxito."
}
},
@@ -2419,6 +2434,7 @@
"credits_not_received_date": "",
"credits_not_received_date_vendorid": "",
"csi": "",
"customer_list": "",
"cycle_time_analysis": "",
"estimates_written_converted": "",
"estimator_detail": "",
@@ -2485,6 +2501,7 @@
"production_by_target_date": "",
"production_by_technician": "",
"production_by_technician_one": "",
"production_over_time": "",
"psr_by_make": "",
"purchase_return_ratio_grouped_by_vendor_detail": "",
"purchase_return_ratio_grouped_by_vendor_summary": "",
@@ -2517,6 +2534,7 @@
"labels": {
"atssummary": "",
"employeevacation": "",
"ins_co_nm_filter": "",
"intake": "",
"manual": "",
"manualevent": ""
@@ -2777,6 +2795,7 @@
},
"vehicles": {
"errors": {
"deleting": "",
"noaccess": "El vehículo no existe o usted no tiene acceso a él.",
"selectexistingornew": "",
"validation": "Asegúrese de que todos los campos se ingresen correctamente.",
@@ -2812,12 +2831,14 @@
"registration": ""
},
"labels": {
"deleteconfirm": "",
"fromvehicle": "",
"novehinfo": "",
"relatedjobs": "",
"updatevehicle": ""
},
"successes": {
"delete": "",
"save": "Vehículo guardado con éxito."
}
},

View File

@@ -230,6 +230,7 @@
"addapptcolor": "",
"addbucket": "",
"addpartslocation": "",
"addpartsrule": "",
"addspeedprint": "",
"addtemplate": "",
"newlaborrate": "",
@@ -270,6 +271,7 @@
"disablebillwip": "",
"disablecontactvehiclecreation": "",
"dms_acctnumber": "",
"dms_control_override": "",
"dms_wip_acctnumber": "",
"generic_customer_number": "",
"itc_federal": "",
@@ -281,6 +283,7 @@
},
"email": "",
"enforce_class": "",
"enforce_conversion_category": "",
"enforce_conversion_csr": "",
"enforce_referral": "",
"federal_tax_id": "",
@@ -328,7 +331,12 @@
"zip": ""
},
"md_jobline_presets": "",
"md_lost_sale_reasons": "",
"md_parts_order_comment": "",
"md_parts_scan": {
"expression": "",
"flags": ""
},
"md_payment_types": "",
"md_referral_sources": "",
"messaginglabel": "",
@@ -579,6 +587,7 @@
"notespresets": "",
"orderstatuses": "",
"partslocations": "",
"partsscan": "",
"printlater": "",
"qbo": "",
"qbo_departmentid": "",
@@ -1272,6 +1281,7 @@
"removefromproduction": "",
"schedule": "Programme",
"sendcsi": "",
"sendpartspricechange": "",
"sendtodms": "",
"sync": "",
"uninvoice": "",
@@ -1295,6 +1305,7 @@
"nojobselected": "Aucun travail n'est sélectionné.",
"noowner": "Aucun propriétaire associé.",
"novehicle": "Aucun véhicule associé.",
"partspricechange": "",
"saving": "Erreur rencontrée lors de la sauvegarde de l'enregistrement.",
"scanimport": "",
"totalscalc": "",
@@ -1446,6 +1457,7 @@
"loss_date": "Date de perte",
"loss_desc": "",
"loss_of_use": "",
"lost_sale_reason": "",
"ma2s": "",
"ma3s": "",
"mabl": "",
@@ -1981,6 +1993,7 @@
"update": ""
},
"errors": {
"deleting": "",
"noaccess": "L'enregistrement n'existe pas ou vous n'y avez pas accès.",
"saving": "",
"selectexistingornew": ""
@@ -2013,6 +2026,7 @@
},
"labels": {
"create_new": "Créez un nouvel enregistrement de propriétaire.",
"deleteconfirm": "",
"existing_owners": "Propriétaires existants",
"fromclaim": "",
"fromowner": "",
@@ -2020,6 +2034,7 @@
"updateowner": ""
},
"successes": {
"delete": "",
"save": "Le propriétaire a bien enregistré."
}
},
@@ -2419,6 +2434,7 @@
"credits_not_received_date": "",
"credits_not_received_date_vendorid": "",
"csi": "",
"customer_list": "",
"cycle_time_analysis": "",
"estimates_written_converted": "",
"estimator_detail": "",
@@ -2485,6 +2501,7 @@
"production_by_target_date": "",
"production_by_technician": "",
"production_by_technician_one": "",
"production_over_time": "",
"psr_by_make": "",
"purchase_return_ratio_grouped_by_vendor_detail": "",
"purchase_return_ratio_grouped_by_vendor_summary": "",
@@ -2517,6 +2534,7 @@
"labels": {
"atssummary": "",
"employeevacation": "",
"ins_co_nm_filter": "",
"intake": "",
"manual": "",
"manualevent": ""
@@ -2777,6 +2795,7 @@
},
"vehicles": {
"errors": {
"deleting": "",
"noaccess": "Le véhicule n'existe pas ou vous n'y avez pas accès.",
"selectexistingornew": "",
"validation": "Veuillez vous assurer que tous les champs sont correctement entrés.",
@@ -2812,12 +2831,14 @@
"registration": ""
},
"labels": {
"deleteconfirm": "",
"fromvehicle": "",
"novehinfo": "",
"relatedjobs": "",
"updatevehicle": ""
},
"successes": {
"delete": "",
"save": "Le véhicule a été enregistré avec succès."
}
},

View File

@@ -1,9 +1,9 @@
import React from "react";
import NumberFormat from "react-number-format";
import { NumericFormat } from "react-number-format";
export default function CurrencyFormatter(props) {
return (
<NumberFormat
<NumericFormat
thousandSeparator={true}
decimalScale={2}
fixedDecimalScale={true}

View File

@@ -142,7 +142,7 @@ middlewares.push(
const cache = new InMemoryCache({});
export default new ApolloClient({
const client = new ApolloClient({
link: ApolloLink.from(middlewares),
cache,
connectToDevTools: process.env.NODE_ENV !== "production",
@@ -161,3 +161,4 @@ export default new ApolloClient({
},
},
});
export default client;

View File

@@ -1841,6 +1841,34 @@ export const TemplateList = (type, context) => {
},
group: "purchases",
},
production_over_time: {
title: i18n.t("reportcenter.templates.production_over_time"),
subject: i18n.t(
"reportcenter.templates.production_over_time"
),
key: "production_over_time",
//idtype: "vendor",
disabled: false,
rangeFilter: {
object: i18n.t("reportcenter.labels.objects.jobs"),
field: i18n.t("jobs.fields.actual_in"),
},
group: "jobs",
},
customer_list: {
title: i18n.t("reportcenter.templates.customer_list"),
subject: i18n.t(
"reportcenter.templates.customer_list"
),
key: "customer_list",
//idtype: "vendor",
disabled: false,
rangeFilter: {
object: i18n.t("reportcenter.labels.objects.jobs"),
field: i18n.t("jobs.fields.date_invoiced"),
},
group: "customers",
},
}
: {}),
...(!type || type === "courtesycarcontract"

View File

@@ -0,0 +1,12 @@
import axios from "axios";
import { notification } from "antd";
async function CriticalPartsScan(jobid) {
try {
await axios.post("/job/partsscan", { jobid });
} catch (error) {
notification.open({ type: "error", message: JSON.stringify(error) });
}
}
export default CriticalPartsScan;

File diff suppressed because it is too large Load Diff

View File

@@ -223,11 +223,9 @@
- kanban_settings
- qbo_realmId
filter:
bodyshop:
associations:
user:
authid:
_eq: X-Hasura-User-Id
user:
authid:
_eq: X-Hasura-User-Id
check: null
- table:
name: audit_trail
@@ -798,6 +796,13 @@
table:
name: owners
schema: public
- name: payment_responses
using:
foreign_key_constraint_on:
column: bodyshopid
table:
name: payment_response
schema: public
- name: phonebooks
using:
foreign_key_constraint_on:
@@ -855,6 +860,7 @@
- deliverchecklist
- email
- enforce_class
- enforce_conversion_category
- enforce_conversion_csr
- enforce_referral
- entegral_configuration
@@ -885,11 +891,13 @@
- md_ins_cos
- md_jobline_presets
- md_labor_rates
- md_lost_sale_reasons
- md_messaging_presets
- md_notes_presets
- md_order_statuses
- md_parts_locations
- md_parts_order_comment
- md_parts_scan
- md_payment_types
- md_rbac
- md_referral_sources
@@ -952,6 +960,7 @@
- deliverchecklist
- email
- enforce_class
- enforce_conversion_category
- enforce_conversion_csr
- enforce_referral
- federal_tax_id
@@ -977,11 +986,13 @@
- md_ins_cos
- md_jobline_presets
- md_labor_rates
- md_lost_sale_reasons
- md_messaging_presets
- md_notes_presets
- md_order_statuses
- md_parts_locations
- md_parts_order_comment
- md_parts_scan
- md_payment_types
- md_rbac
- md_referral_sources
@@ -2562,6 +2573,7 @@
- convertedtolbr
- convertedtolbr_data
- created_at
- critical
- db_hrs
- db_price
- db_ref
@@ -2639,6 +2651,7 @@
- convertedtolbr
- convertedtolbr_data
- created_at
- critical
- db_hrs
- db_price
- db_ref
@@ -2903,6 +2916,13 @@
table:
name: parts_orders
schema: public
- name: payment_responses
using:
foreign_key_constraint_on:
column: jobid
table:
name: payment_response
schema: public
- name: payments
using:
foreign_key_constraint_on:
@@ -3382,6 +3402,7 @@
- loss_desc
- loss_of_use
- loss_type
- lost_sale_reason
- materials
- other_amount_payable
- owner_owing
@@ -3656,6 +3677,7 @@
- loss_desc
- loss_of_use
- loss_type
- lost_sale_reason
- materials
- other_amount_payable
- owner_owing
@@ -4502,6 +4524,63 @@
_eq: X-Hasura-User-Id
- active:
_eq: true
- table:
name: payment_response
schema: public
object_relationships:
- name: bodyshop
using:
foreign_key_constraint_on: bodyshopid
- name: job
using:
foreign_key_constraint_on: jobid
- name: payment
using:
foreign_key_constraint_on: paymentid
insert_permissions:
- role: user
permission:
check:
bodyshop:
associations:
_and:
- user:
authid:
_eq: X-Hasura-User-Id
- active:
_eq: true
columns:
- amount
- bodyshopid
- declinereason
- ext_paymentid
- jobid
- paymentid
- response
- successful
select_permissions:
- role: user
permission:
columns:
- successful
- response
- amount
- declinereason
- ext_paymentid
- bodyshopid
- id
- jobid
- paymentid
filter:
bodyshop:
associations:
_and:
- user:
authid:
_eq: X-Hasura-User-Id
- active:
_eq: true
allow_aggregations: true
- table:
name: payments
schema: public
@@ -4517,6 +4596,13 @@
table:
name: exportlog
schema: public
- name: payment_responses
using:
foreign_key_constraint_on:
column: paymentid
table:
name: payment_response
schema: public
insert_permissions:
- role: user
permission:

View File

@@ -0,0 +1 @@
DROP TABLE "public"."payment_response";

View File

@@ -0,0 +1,2 @@
CREATE TABLE "public"."payment_response" ("id" uuid NOT NULL DEFAULT gen_random_uuid(), "bodyshopid" uuid NOT NULL, "jobid" uuid, "paymentid" uuid, "successful" boolean NOT NULL DEFAULT false, "ext_paymentid" text NOT NULL, "amount" numeric NOT NULL, "declinereason" text, "response" jsonb NOT NULL DEFAULT jsonb_build_object(), PRIMARY KEY ("id") , FOREIGN KEY ("bodyshopid") REFERENCES "public"."bodyshops"("id") ON UPDATE cascade ON DELETE cascade, FOREIGN KEY ("jobid") REFERENCES "public"."jobs"("id") ON UPDATE cascade ON DELETE cascade, FOREIGN KEY ("paymentid") REFERENCES "public"."payments"("id") ON UPDATE cascade ON DELETE cascade);
CREATE EXTENSION IF NOT EXISTS pgcrypto;

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"."bodyshops" add column "md_lost_sale_reasons" jsonb
-- not null default jsonb_build_array();

View File

@@ -0,0 +1,2 @@
alter table "public"."bodyshops" add column "md_lost_sale_reasons" jsonb
not null default jsonb_build_array();

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"."jobs" add column "lost_sale_reason" text
-- null;

View File

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

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"."bodyshops" add column "md_parts_scan" jsonb
-- not null default jsonb_build_array();

View File

@@ -0,0 +1,2 @@
alter table "public"."bodyshops" add column "md_parts_scan" jsonb
not null default jsonb_build_array();

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"."joblines" add column "critical" boolean
-- not null default 'false';

View File

@@ -0,0 +1,2 @@
alter table "public"."joblines" add column "critical" boolean
not null default 'false';

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"."bodyshops" add column "enforce_conversion_category" boolean
-- not null default 'false';

View File

@@ -0,0 +1,2 @@
alter table "public"."bodyshops" add column "enforce_conversion_category" boolean
not null default 'false';

View File

@@ -0,0 +1 @@
alter table "public"."bodyshops" alter column "md_lost_sale_reasons" set default jsonb_build_array();

View File

@@ -0,0 +1 @@
alter table "public"."bodyshops" alter column "md_lost_sale_reasons" set default '["Scheduling Delay", "Backordered Parts", "Price", "Unknown"]';

5634
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -17,41 +17,41 @@
"start": "node server.js"
},
"dependencies": {
"aws-sdk": "^2.1181.0",
"aws-sdk": "^2.1326.0",
"axios": "^0.27.2",
"bluebird": "^3.7.2",
"body-parser": "^1.20.0",
"cloudinary": "^1.30.1",
"body-parser": "^1.20.2",
"cloudinary": "^1.34.0",
"compression": "^1.7.4",
"cookie-parser": "^1.4.6",
"cors": "2.8.5",
"csrf": "^3.1.0",
"dinero.js": "^1.9.1",
"dotenv": "16.0.1",
"express": "^4.18.1",
"firebase-admin": "^11.0.0",
"graphql": "^16.5.0",
"dotenv": "16.0.3",
"express": "^4.18.2",
"firebase-admin": "^11.5.0",
"graphql": "^16.6.0",
"graphql-request": "^4.2.0",
"graylog2": "^0.2.1",
"inline-css": "^4.0.1",
"inline-css": "^4.0.2",
"intuit-oauth": "^4.0.0",
"json-2-csv": "^3.17.1",
"json-2-csv": "^3.19.0",
"lodash": "^4.17.21",
"moment": "^2.29.4",
"moment-timezone": "^0.5.34",
"moment-timezone": "^0.5.41",
"multer": "^1.4.5-lts.1",
"node-mailjet": "^5.1.0",
"node-persist": "^3.1.0",
"node-quickbooks": "^2.0.39",
"nodemailer": "^6.7.7",
"phone": "^3.1.23",
"node-mailjet": "^6.0.2",
"node-persist": "^3.1.3",
"node-quickbooks": "^2.0.41",
"nodemailer": "^6.9.1",
"phone": "^3.1.35",
"query-string": "^7.1.1",
"soap": "^0.45.0",
"socket.io": "^4.5.0",
"ssh2-sftp-client": "^9.0.2",
"soap": "^1.0.0",
"socket.io": "^4.6.1",
"ssh2-sftp-client": "^9.0.4",
"stripe": "^9.15.0",
"twilio": "^3.80.0",
"uuid": "^8.3.2",
"twilio": "^4.8.0",
"uuid": "^9.0.0",
"xml2js": "^0.4.23",
"xmlbuilder2": "^3.0.2"
},

View File

@@ -135,7 +135,8 @@ app.post(
app.post("/job/totalsssu", fb.validateFirebaseIdToken, job.totalsSsu);
app.post("/job/costing", fb.validateFirebaseIdToken, job.costing);
app.post("/job/costingmulti", fb.validateFirebaseIdToken, job.costingmulti);
var partsScan = require("./server/parts-scan/parts-scan");
app.post("/job/partsscan", fb.validateFirebaseIdToken, partsScan.partsScan);
//Scheduling
var scheduling = require("./server/scheduling/scheduling-job");
app.post("/scheduling/job", fb.validateFirebaseIdToken, scheduling.job);

View File

@@ -1092,7 +1092,13 @@ async function GenerateTransWips(socket) {
if (alloc.sale.getAmount() > 0 && !alloc.tax) {
const item = {
acct: alloc.profitCenter.dms_acctnumber,
cntl: socket.JobData.ro_number,
cntl:
alloc.profitCenter.dms_control_override &&
alloc.profitCenter.dms_control_override !== null &&
alloc.profitCenter.dms_control_override !== undefined &&
alloc.profitCenter.dms_control_override?.trim() !== ""
? alloc.profitCenter.dms_control_override
: socket.JobData.ro_number,
cntl2: null,
credtMemoNo: null,
postAmt: alloc.sale.multiply(-1).getAmount(),
@@ -1109,7 +1115,13 @@ async function GenerateTransWips(socket) {
if (alloc.cost.getAmount() > 0 && !alloc.tax) {
const item = {
acct: alloc.costCenter.dms_acctnumber,
cntl: socket.JobData.ro_number,
cntl:
alloc.costCenter.dms_control_override &&
alloc.costCenter.dms_control_override !== null &&
alloc.costCenter.dms_control_override !== undefined &&
alloc.costCenter.dms_control_override?.trim() !== ""
? alloc.costCenter.dms_control_override
: socket.JobData.ro_number,
cntl2: null,
credtMemoNo: null,
postAmt: alloc.cost.getAmount(),
@@ -1123,7 +1135,13 @@ async function GenerateTransWips(socket) {
const itemWip = {
acct: alloc.costCenter.dms_wip_acctnumber,
cntl: socket.JobData.ro_number,
cntl:
alloc.costCenter.dms_control_override &&
alloc.costCenter.dms_control_override !== null &&
alloc.costCenter.dms_control_override !== undefined &&
alloc.costCenter.dms_control_override?.trim() !== ""
? alloc.costCenter.dms_control_override
: socket.JobData.ro_number,
cntl2: null,
credtMemoNo: null,
postAmt: alloc.cost.multiply(-1).getAmount(),
@@ -1158,7 +1176,13 @@ async function GenerateTransWips(socket) {
if (alloc.sale.getAmount() > 0) {
const item2 = {
acct: alloc.profitCenter.dms_acctnumber,
cntl: socket.JobData.ro_number,
cntl:
alloc.profitCenter.dms_control_override &&
alloc.profitCenter.dms_control_override !== null &&
alloc.profitCenter.dms_control_override !== undefined &&
alloc.profitCenter.dms_control_override?.trim() !== ""
? alloc.profitCenter.dms_control_override
: socket.JobData.ro_number,
cntl2: null,
credtMemoNo: null,
postAmt: alloc.sale.multiply(-1).getAmount(),

View File

@@ -42,14 +42,16 @@ function pollFunc(fn, timeout, interval) {
pollFunc(getEntegralShopData, 0, 5 * 60 * 1000); //Set the metadata to refresh every 5 minutes.
async function getEntegralShopData() {
await storage.init({ logging: true });
const { bodyshops } = await client.request(queries.GET_ENTEGRAL_SHOPS);
logger.log("set-entegral-shops-local-storage", "DEBUG", "API", null, null);
await storage.setItem("entegralShops", bodyshops);
return true; //Continue execution.
// await storage.init({ logging: true });
// const { bodyshops } = await client.request(queries.GET_ENTEGRAL_SHOPS);
// logger.log("set-entegral-shops-local-storage", "DEBUG", "API", null, null);
// await storage.setItem("entegralShops", bodyshops);
// return true; //Continue execution.
}
exports.default = async (req, res) => {
res.sendStatus(200);
return;
//Query for the List of Bodyshop Clients.
const job = req.body.event.data.new;
logger.log("arms-job-update", "DEBUG", "api", job.id, null);

View File

@@ -814,7 +814,11 @@ const CreateCosts = (job) => {
].add(
Dinero({
amount: Math.round((ticket_val.rate || 0) * 100),
}).multiply(ticket_val.actualhrs || ticket_val.productivehrs || 0)
}).multiply(
(ticket_val.flat_rate
? ticket_val.productivehrs
: ticket_val.actualhrs) || 0
)
);
return ticket_acc;

View File

@@ -775,6 +775,7 @@ exports.AUTOHOUSE_QUERY = `query AUTOHOUSE_EXPORT($start: timestamptz, $bodyshop
cost_center
actualhrs
productivehrs
flat_rate
}
area_of_damage
employee_prep_rel {
@@ -905,7 +906,7 @@ exports.UPDATE_JOB = `
}
`;
exports.GET_JOB_BY_PK = ` query GET_JOB_BY_PK($id: uuid!) {
exports.GET_JOB_BY_PK = `query GET_JOB_BY_PK($id: uuid!) {
jobs_by_pk(id: $id) {
updated_at
alt_transport
@@ -1718,3 +1719,27 @@ query GET_PBS_AP_ALLOCATIONS($billids: [uuid!]) {
}
}
`;
exports.QUERY_PARTS_SCAN = `query QUERY_PARTS_SCAN ($id: uuid!) {
jobs_by_pk(id: $id) {
bodyshop {
id
md_parts_scan
}
joblines(where: {removed: {_eq: false}}) {
id
line_desc
critical
}
}
}
`;
exports.UPDATE_PARTS_CRITICAL = `mutation UPDATE_PARTS_CRITICAL ($IdsToMarkCritical:[uuid!]!, $jobid: uuid!){
critical: update_joblines(where:{id:{_in:$IdsToMarkCritical}}, _set:{critical: true}){
affected_rows
}
notcritical: update_joblines(where:{id:{_nin:$IdsToMarkCritical}, jobid: {_eq: $jobid}}, _set:{critical: false}){
affected_rows
}
}`;

View File

@@ -0,0 +1,58 @@
const Dinero = require("dinero.js");
const queries = require("../graphql-client/queries");
const { job } = require("../scheduling/scheduling-job");
const GraphQLClient = require("graphql-request").GraphQLClient;
const logger = require("../utils/logger");
const _ = require("lodash");
// Dinero.defaultCurrency = "USD";
// Dinero.globalLocale = "en-CA";
exports.partsScan = async function (req, res) {
const BearerToken = req.headers.authorization;
const { jobid } = req.body;
logger.log("job-parts-scan", "DEBUG", req.user?.email, jobid, null);
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {
headers: {
Authorization: BearerToken,
},
});
try {
//Query all jobline data using the user's authorization.
const data = await client
.setHeaders({ Authorization: BearerToken })
.request(queries.QUERY_PARTS_SCAN, {
id: jobid,
});
//Create RegExps once for better performance.
const IdsToMarkCritical = [];
const RegExpressions = data.jobs_by_pk.bodyshop.md_parts_scan.map(
(r) => new RegExp(r.expression, r.flags)
);
//Check each line against each regex rule.
data.jobs_by_pk.joblines.forEach((jobline) => {
RegExpressions.forEach((rExp) => {
if (jobline.line_desc.match(rExp)) {
IdsToMarkCritical.push(jobline);
}
});
});
const result = await client
.setHeaders({ Authorization: BearerToken })
.request(queries.UPDATE_PARTS_CRITICAL, {
IdsToMarkCritical: _.uniqBy(IdsToMarkCritical, "id").map((i) => i.id),
jobid: jobid,
});
res.status(200).json(result);
} catch (error) {
logger.log("job-parts-scan-error", "ERROR", req.user.email, jobid, {
jobid,
error,
});
res.status(400).json(JSON.stringify(error));
}
};

2601
yarn.lock

File diff suppressed because it is too large Load Diff