Merged in test-beta (pull request #1226)

Test beta
This commit is contained in:
Dave Richer
2024-01-27 02:26:07 +00:00
102 changed files with 53976 additions and 3717 deletions

1594
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -11,29 +11,29 @@
"@craco/craco": "^7.1.0",
"@fingerprintjs/fingerprintjs": "^4.2.1",
"@jsreport/browser-client": "^3.1.0",
"@reduxjs/toolkit": "^2.0.1",
"@sentry/react": "^7.93.0",
"@sentry/tracing": "^7.93.0",
"@reduxjs/toolkit": "^2.1.0",
"@sentry/react": "^7.95.0",
"@sentry/tracing": "^7.95.0",
"@splitsoftware/splitio-react": "^1.11.0",
"@tanem/react-nprogress": "^5.0.51",
"antd": "^5.12.8",
"antd": "^5.13.2",
"apollo-link-logger": "^2.0.1",
"axios": "^1.6.5",
"craco-less": "^3.0.1",
"dayjs": "^1.11.10",
"dayjs-business-days2": "^1.2.2",
"dinero.js": "^1.9.1",
"dotenv": "^16.3.1",
"dotenv": "^16.4.0",
"enquire-js": "^0.2.1",
"env-cmd": "^10.1.0",
"exifr": "^7.1.3",
"firebase": "^10.7.2",
"graphql": "^16.6.0",
"i18next": "^23.7.16",
"i18next": "^23.7.18",
"i18next-browser-languagedetector": "^7.0.2",
"jsoneditor": "^10.0.0",
"jsreport-browser-client-dist": "^1.3.0",
"libphonenumber-js": "^1.10.53",
"libphonenumber-js": "^1.10.54",
"logrocket": "^7.0.0",
"markerjs2": "^2.31.4",
"normalize-url": "^8.0.0",
@@ -51,10 +51,11 @@
"react-drag-listview": "^2.0.0",
"react-grid-gallery": "^1.0.0",
"react-grid-layout": "1.3.4",
"react-i18next": "^14.0.0",
"react-i18next": "^14.0.1",
"react-icons": "^5.0.1",
"react-image-lightbox": "^5.1.4",
"react-intersection-observer": "^9.5.3",
"react-markdown": "^9.0.1",
"react-number-format": "^5.1.4",
"react-redux": "^9.1.0",
"react-resizable": "^3.0.5",
@@ -92,6 +93,7 @@
"buildcra": "REACT_APP_GIT_SHA=`git rev-parse --short HEAD` craco build",
"test": "cypress open",
"eject": "react-scripts eject",
"eulaize": "node src/utils/eulaize.js",
"madge": "madge --image ./madge-graph.svg --extensions js,jsx,ts,tsx --circular ."
},
"eslintConfig": {
@@ -118,7 +120,7 @@
},
"devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@sentry/webpack-plugin": "^2.10.2",
"@sentry/webpack-plugin": "^2.10.3",
"@testing-library/cypress": "^10.0.1",
"cypress": "^13.6.3",
"eslint-plugin-cypress": "^2.15.1",

View File

@@ -8,6 +8,7 @@ import {Route, Routes} from "react-router-dom";
import {createStructuredSelector} from "reselect";
import DocumentEditorContainer from "../components/document-editor/document-editor.container";
import ErrorBoundary from "../components/error-boundary/error-boundary.component";
//Component Imports
import LoadingSpinner from "../components/loading-spinner/loading-spinner.component";
import DisclaimerPage from "../pages/disclaimer/disclaimer.page";
@@ -16,10 +17,11 @@ import TechPageContainer from "../pages/tech/tech.page.container";
import {setOnline} from "../redux/application/application.actions";
import {selectOnline} from "../redux/application/application.selectors";
import {checkUserSession} from "../redux/user/user.actions";
import {selectBodyshop, selectCurrentUser,} from "../redux/user/user.selectors";
import {selectBodyshop, selectCurrentEula, selectCurrentUser,} from "../redux/user/user.selectors";
import PrivateRoute from "../components/PrivateRoute";
import "./App.styles.scss";
import handleBeta from "../utils/betaHandler";
import Eula from "../components/eula/eula.component";
const ResetPassword = lazy(() =>
import("../pages/reset-password/reset-password.component")
@@ -35,14 +37,14 @@ const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser,
online: selectOnline,
bodyshop: selectBodyshop,
currentEula: selectCurrentEula
});
const mapDispatchToProps = (dispatch) => ({
checkUserSession: () => dispatch(checkUserSession()),
setOnline: (isOnline) => dispatch(setOnline(isOnline)),
});
export function App({bodyshop, checkUserSession, currentUser, online, setOnline}) {
export function App({bodyshop, checkUserSession, currentUser, online, setOnline, currentEula}) {
const client = useSplitClient().client;
const [listenersAdded, setListenersAdded] = useState(false)
const {t} = useTranslation();
@@ -121,6 +123,10 @@ export function App({bodyshop, checkUserSession, currentUser, online, setOnline}
/>
);
if (currentEula && !currentUser.eulaIsAccepted) {
return <Eula/>
}
// Any route that is not assigned and matched will default to the Landing Page component
return (
<Suspense fallback={<LoadingSpinner message="ImEX Online"/>}>
@@ -131,10 +137,12 @@ export function App({bodyshop, checkUserSession, currentUser, online, setOnline}
<Route path="/csi/:surveyId" element={<ErrorBoundary><CsiPage/></ErrorBoundary>}/>
<Route path="/disclaimer" element={<ErrorBoundary><DisclaimerPage/></ErrorBoundary>}/>
<Route path="/mp/:paymentIs" element={<ErrorBoundary><MobilePaymentContainer/></ErrorBoundary>}/>
<Route path="/manage/*" element={<ErrorBoundary><PrivateRoute isAuthorized={currentUser.authorized}/></ErrorBoundary>}>
<Route path="/manage/*"
element={<ErrorBoundary><PrivateRoute isAuthorized={currentUser.authorized}/></ErrorBoundary>}>
<Route path="*" element={<ManagePage/>}/>
</Route>
<Route path="/tech/*" element={<ErrorBoundary><PrivateRoute isAuthorized={currentUser.authorized}/></ErrorBoundary>}>
<Route path="/tech/*"
element={<ErrorBoundary><PrivateRoute isAuthorized={currentUser.authorized}/></ErrorBoundary>}>
<Route path="*" element={<TechPageContainer/>}/>
</Route>
<Route path="/edit/*" element={<PrivateRoute isAuthorized={currentUser.authorized}/>}>

View File

@@ -147,23 +147,11 @@
}
}
//Update row highlighting on production board.
.ant-table-tbody > tr.ant-table-row:hover > td {
background: #e7f3ff !important;
}
.ant-table-tbody > tr.ant-table-row-selected > td {
background: #e6f7ff !important;
}
.job-line-manual {
color: tomato;
font-style: italic;
}
td.ant-table-column-sort {
background-color: transparent;
}
.ant-table-tbody > tr.ant-table-row:nth-child(2n) > td {
background-color: #f4f4f4;

View File

@@ -1,4 +1,9 @@
import {defaultsDeep} from "lodash";
import {theme} from "antd";
const {defaultAlgorithm, darkAlgorithm} = theme;
let isDarkMode = false;
/**
* Default theme
@@ -6,6 +11,11 @@ import {defaultsDeep} from "lodash";
*/
const defaultTheme = {
components: {
Table: {
rowHoverBg: '#e7f3ff',
rowSelectedBg: '#e6f7ff',
headerSortHoverBg: 'transparent',
},
Menu: {
darkItemHoverBg: '#1677ff',
itemHoverBg: '#1677ff',
@@ -40,7 +50,11 @@ const devTheme = {
*/
const prodTheme = {};
const theme = process.env.NODE_ENV === "development" ? devTheme
const currentTheme = process.env.NODE_ENV === "development" ? devTheme
: prodTheme;
export default defaultsDeep(theme, defaultTheme);
const finaltheme = {
algorithm: isDarkMode ? darkAlgorithm : defaultAlgorithm,
...defaultsDeep(currentTheme, defaultTheme)
}
export default finaltheme;

View File

@@ -0,0 +1,253 @@
import React, {useCallback, useEffect, useRef, useState} from "react";
import {Button, Card, Checkbox, Col, Form, Input, Modal, notification, Row} from "antd";
import Markdown from "react-markdown";
import {createStructuredSelector} from "reselect";
import {selectCurrentEula, selectCurrentUser} from "../../redux/user/user.selectors";
import {connect} from "react-redux";
import {FormDatePicker} from "../form-date-picker/form-date-picker.component";
import {INSERT_EULA_ACCEPTANCE} from "../../graphql/user.queries";
import {useMutation} from "@apollo/client";
import {acceptEula} from "../../redux/user/user.actions";
import {useTranslation} from "react-i18next";
import day from '../../utils/day';
import './eula.styles.scss';
const Eula = ({currentEula, currentUser, acceptEula}) => {
const [formReady, setFormReady] = useState(false);
const [hasEverScrolledToBottom, setHasEverScrolledToBottom] = useState(false);
const [insertEulaAcceptance] = useMutation(INSERT_EULA_ACCEPTANCE);
const [form] = Form.useForm();
const markdownCardRef = useRef(null);
const {t} = useTranslation();
const [api, contextHolder] = notification.useNotification();
const handleScroll = useCallback((e) => {
const bottom = e.target.scrollHeight - 100 <= e.target.scrollTop + e.target.clientHeight;
if (bottom && !hasEverScrolledToBottom) {
setHasEverScrolledToBottom(true);
} else if (e.target.scrollHeight <= e.target.clientHeight && !hasEverScrolledToBottom) {
setHasEverScrolledToBottom(true);
}
}, [hasEverScrolledToBottom, setHasEverScrolledToBottom]);
useEffect(() => {
handleScroll({target: markdownCardRef.current});
}, [handleScroll]);
const handleChange = useCallback(() => {
form.validateFields({validateOnly: true})
.then(() => setFormReady(hasEverScrolledToBottom))
.catch(() => setFormReady(false));
}, [form, hasEverScrolledToBottom]);
useEffect(() => {
handleChange();
}, [handleChange, hasEverScrolledToBottom, form]);
const onFinish = async ({acceptTerms, ...formValues}) => {
const eulaId = currentEula.id;
const useremail = currentUser.email;
try {
const {accepted_terms, ...otherFormValues} = formValues;
// Trim the values of the fields before submitting
const trimmedFormValues = {
first_name: otherFormValues.first_name.trim(),
last_name: otherFormValues.last_name.trim(),
business_name: otherFormValues.business_name.trim(),
address: otherFormValues.address.trim(),
phone_number: otherFormValues.phone_number.trim(),
};
await insertEulaAcceptance({
variables: {
eulaAcceptance: {
eulaid: eulaId,
useremail,
...otherFormValues,
...trimmedFormValues,
date_accepted: new Date(),
}
}
});
acceptEula();
} catch (err) {
api.error({
message: t('eula.errors.acceptance.message'),
description: t('eula.errors.acceptance.description'),
placement: 'bottomRight',
duration: 5000,
});
console.log(`${t('eula.errors.acceptance.message')}`);
console.dir({
message: err.message,
stack: err.stack,
});
}
};
return (
<>
{contextHolder}
<Modal
title={t('eula.titles.modal')}
className='eula-modal'
width={'100vh'}
open={currentEula}
footer={() => (
<Button
className='eula-accept-button'
form='tosForm'
type="primary"
size='large'
htmlType="submit"
disabled={!formReady}
children={t('eula.buttons.accept')}
/>
)}
closable={false}
>
<Card type='inner' className='eula-markdown-card' onScroll={handleScroll} ref={markdownCardRef}>
<div id='markdowndiv' className='eula-markdown-div'>
<Markdown children={currentEula?.content?.replace(/\\n|\\r|\\n\\r|\\r\\n/g, '\n')}/>
</div>
</Card>
<EulaFormComponent form={form} handleChange={handleChange} onFinish={onFinish} t={t}/>
{!hasEverScrolledToBottom && (
<Card className='eula-never-scrolled' type='inner'>
<h3>{t('eula.content.never_scrolled')}</h3>
</Card>
)}
</Modal>
</>
)
}
const EulaFormComponent = ({form, handleChange, onFinish, t}) => (
<Card type='inner' title={t('eula.titles.upper_card')} style={{marginTop: '10px'}}>
<Form id='tosForm' onChange={handleChange} onFinish={onFinish} form={form}>
<Row gutter={24}>
<Col span={12}>
<Form.Item
label={t('eula.labels.first_name')}
name="first_name"
rules={[{
required: true,
validator: (_, value) =>
value.trim() !== '' ? Promise.resolve() : Promise.reject(new Error(t('eula.messages.first_name'))),
},]}
>
<Input placeholder={t('eula.labels.first_name')}
aria-label={t('eula.labels.first_name')}/>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
label={t('eula.labels.last_name')}
name="last_name"
rules={[{
required: true,
validator: (_, value) =>
value.trim() !== '' ? Promise.resolve() : Promise.reject(new Error(t('eula.messages.last_name'))),
}]}
>
<Input placeholder={t('eula.labels.last_name')}
aria-label={t('eula.labels.last_name')}/>
</Form.Item>
</Col>
</Row>
<Row gutter={24}>
<Col span={12}>
<Form.Item
label={t('eula.labels.business_name')}
name="business_name"
rules={[{
required: true,
validator: (_, value) =>
value.trim() !== '' ? Promise.resolve() : Promise.reject(new Error(t('eula.messages.business_name'))),
}]}
>
<Input placeholder={t('eula.labels.business_name')} aria-label={t('eula.labels.business_name')}/>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
label={t('eula.labels.phone_number')}
name="phone_number"
rules={[
{
pattern: /^(\+\d{1,2}\s?)?1?-?\.?\s?\(?\d{3}\)?[\s.-]?\d{3}[\s.-]?\d{4}$/,
message: t('eula.messages.phone_number'),
}
]}
>
<Input placeholder={t('eula.labels.phone_number')}
aria-label={t('eula.labels.phone_number')}/>
</Form.Item>
</Col>
</Row>
<Row gutter={24}>
<Col span={12}>
<Form.Item
label={t('eula.labels.address')}
name="address"
>
<Input placeholder={t('eula.labels.address')} aria-label={t('eula.labels.address')}/>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
label={t('eula.labels.date_accepted')}
name="date_accepted"
rules={[
{
required: true,
validator: (_, value) => {
if (day(value).isSame(day(), 'day')) {
return Promise.resolve();
}
return Promise.reject(new Error(t('eula.messages.date_accepted')));
}
},
]}
>
<FormDatePicker onChange={handleChange} onlyToday aria-label={t('eula.labels.date_accepted')}/>
</Form.Item>
</Col>
</Row>
<Row gutter={24}>
<Col span={24}>
<Form.Item
name="accepted_terms"
valuePropName="checked"
rules={[
{
required: true,
validator: (_, value) =>
value ? Promise.resolve() : Promise.reject(new Error(t('eula.messages.accepted_terms'))),
},
]}
>
<Checkbox aria-label={t('eula.labels.accepted_terms')}>{t('eula.labels.accepted_terms')}</Checkbox>
</Form.Item>
</Col>
</Row>
</Form>
</Card>
);
const mapStateToProps = createStructuredSelector({
currentEula: selectCurrentEula,
currentUser: selectCurrentUser,
});
const mapDispatchToProps = (dispatch) => ({
acceptEula: () => dispatch(acceptEula()),
});
export default connect(mapStateToProps, mapDispatchToProps)(Eula);

View File

@@ -0,0 +1,21 @@
.eula-modal {
top: 20px;
}
.eula-markdown-card {
max-height: 50vh;
overflow-y: auto;
background-color: lightgray;
}
.eula-markdown-div {
padding: 0 10px 0 10px;
}
.eula-never-scrolled {
text-align: center;
}
.eula-accept-button {
width: 100%;
}

View File

@@ -16,7 +16,16 @@ export default connect(mapStateToProps, mapDispatchToProps)(FormDatePicker);
const dateFormat = "MM/DD/YYYY";
export function FormDatePicker({bodyshop, value, onChange, onBlur, onlyFuture, isDateOnly = true, ...restProps }) {
export function FormDatePicker({
bodyshop,
value,
onChange,
onBlur,
onlyFuture,
onlyToday,
isDateOnly = true,
...restProps
}) {
const ref = useRef();
const handleChange = (newDate) => {
@@ -87,9 +96,13 @@ export function FormDatePicker({bodyshop, value, onChange, onBlur, onlyFuture, i
onBlur={onBlur || handleBlur}
showToday={false}
disabledTime
{...(onlyFuture && {
disabledDate: (d) => dayjs().subtract(1, "day").isAfter(d),
})}
disabledDate={(d) => {
if (onlyToday) {
return !dayjs().isSame(d, 'day');
} else if (onlyFuture) {
return dayjs().subtract(1, "day").isAfter(d);
}
}}
{...restProps}
/>
</div>

View File

@@ -0,0 +1,246 @@
import React, {useCallback, useEffect, useState} from 'react';
import day from '../../utils/day';
import axios from 'axios';
import {Badge, Card, Space, Table, Tag} from 'antd';
import {gql, useQuery} from "@apollo/client";
import {DateTimeFormatterFunction} from "../../utils/DateFormatter";
import {isEmpty} from "lodash";
import {useTranslation} from "react-i18next";
require('./job-lifecycle.styles.scss');
// show text on bar if text can fit
export function JobLifecycleComponent({job, statuses, ...rest}) {
const [loading, setLoading] = useState(true);
const [lifecycleData, setLifecycleData] = useState(null);
const {t} = useTranslation(); // Used for tracking external state changes.
const {data} = useQuery(gql`
query get_job_test($id: uuid!){
jobs_by_pk(id:$id){
id
status
}
}
`, {
variables: {
id: job.id
},
fetchPolicy: 'cache-only'
});
/**
* Gets the lifecycle data for the job.
* @returns {Promise<void>}
*/
const getLifecycleData = useCallback(async () => {
if (job && job.id && statuses && statuses.statuses) {
try {
setLoading(true);
const response = await axios.post("/job/lifecycle", {
jobids: job.id,
statuses: statuses.statuses
});
const data = response.data.transition[job.id];
setLifecycleData(data);
} catch (err) {
console.error(`${t('job_lifecycle.errors.fetch')}: ${err.message}`);
} finally {
setLoading(false);
}
}
}, [job, statuses, t]);
useEffect(() => {
if (!data) return;
setTimeout(() => {
getLifecycleData().catch(err => console.error(`${t('job_lifecycle.errors.fetch')}: ${err.message}`));
}, 500);
}, [data, getLifecycleData, t]);
const columns = [
{
title: t('job_lifecycle.columns.value'),
dataIndex: 'value',
key: 'value',
},
{
title: t('job_lifecycle.columns.start'),
dataIndex: 'start',
key: 'start',
render: (text) => DateTimeFormatterFunction(text),
sorter: (a, b) => day(a.start).unix() - day(b.start).unix(),
},
{
title: t('job_lifecycle.columns.relative_start'),
dataIndex: 'start_readable',
key: 'start_readable',
},
{
title: t('job_lifecycle.columns.end'),
dataIndex: 'end',
key: 'end',
sorter: (a, b) => {
if (isEmpty(a.end) || isEmpty(b.end)) {
if (isEmpty(a.end) && isEmpty(b.end)) {
return 0;
}
return isEmpty(a.end) ? 1 : -1;
}
return day(a.end).unix() - day(b.end).unix();
},
render: (text) => isEmpty(text) ? t('job_lifecycle.content.not_available') : DateTimeFormatterFunction(text)
},
{
title: t('job_lifecycle.columns.relative_end'),
dataIndex: 'end_readable',
key: 'end_readable',
},
{
title: t('job_lifecycle.columns.duration'),
dataIndex: 'duration_readable',
key: 'duration_readable',
sorter: (a, b) => a.duration - b.duration,
},
];
return (
<Card loading={loading} title={t('job_lifecycle.content.title')}>
{!loading ? (
lifecycleData && lifecycleData.lifecycle && lifecycleData.durations ? (
<Space direction='vertical' style={{width: '100%'}}>
<Card
type='inner'
title={(
<Space direction='horizontal' size='small'>
<Badge status='processing' count={lifecycleData.durations.totalStatuses}/>
{t('job_lifecycle.content.title_durations')}
</Space>
)}
style={{width: '100%'}}
>
<div id="bar-container" style={{
display: 'flex',
width: '100%',
height: '100px',
textAlign: 'center',
borderRadius: '5px',
borderWidth: '5px',
borderStyle: 'solid',
borderColor: '#f0f2f5',
margin: 0,
padding: 0
}}>
{lifecycleData.durations.summations.map((key, index, array) => {
const isFirst = index === 0;
const isLast = index === array.length - 1;
return (
<div key={key.status} style={{
overflow: 'hidden',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
margin: 0,
padding: 0,
borderTop: '1px solid #f0f2f5',
borderBottom: '1px solid #f0f2f5',
borderLeft: isFirst ? '1px solid #f0f2f5' : undefined,
borderRight: isLast ? '1px solid #f0f2f5' : undefined,
backgroundColor: key.color,
width: `${key.percentage}%`
}}
aria-label={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`}
title={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`}
>
{key.percentage > 15 ?
<>
<div>{key.roundedPercentage}</div>
<div style={{
backgroundColor: '#f0f2f5',
borderRadius: '5px',
paddingRight: '2px',
paddingLeft: '2px',
fontSize: '0.8rem',
}}>
{key.status}
</div>
</>
: null}
</div>
);
})}
</div>
<Card type='inner' title={t('job_lifecycle.content.legend_title')}
style={{marginTop: '10px'}}>
<div>
{lifecycleData.durations.summations.map((key) => (
<Tag key={key.status} color={key.color} style={{width: '13vh', padding: '4px', margin: '4px'}}>
<div
aria-label={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`}
title={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`}
style={{
backgroundColor: '#f0f2f5',
color: '#000',
padding: '4px',
textAlign: 'center'
}}>
{key.status} ({key.roundedPercentage})
</div>
</Tag>
))}
</div>
</Card>
{(lifecycleData?.durations?.humanReadableTotal) ||
(lifecycleData.lifecycle[0] && lifecycleData.lifecycle[0].value && lifecycleData?.durations?.totalCurrentStatusDuration?.humanReadable) ?
<Card style={{marginTop: '10px'}}>
<ul>
{lifecycleData.durations && lifecycleData.durations.humanReadableTotal &&
<li>
<span
style={{fontWeight: 'bold'}}>{t('job_lifecycle.content.previous_status_accumulated_time')}:</span> {lifecycleData.durations.humanReadableTotal}
</li>
}
{lifecycleData.lifecycle[0] && lifecycleData.lifecycle[0].value && lifecycleData?.durations?.totalCurrentStatusDuration?.humanReadable &&
<li>
<span
style={{fontWeight: 'bold'}}>{t('job_lifecycle.content.current_status_accumulated_time')} ({lifecycleData.lifecycle[0].value}):</span> {lifecycleData.durations.totalCurrentStatusDuration.humanReadable}
</li>
}
</ul>
</Card>
: null}
</Card>
<Card type='inner' title={(
<>
<Space direction="horizontal" size="small">
<Badge status='processing' count={lifecycleData.lifecycle.length}/>
{t('job_lifecycle.content.title_transitions')}
</Space>
</>
)}>
<Table style={{
overflow: 'auto',
width: '100%',
}} columns={columns} dataSource={lifecycleData.lifecycle}/>
</Card>
</Space>
) : (
<Card type='inner' style={{textAlign: 'center'}}>
{t('job_lifecycle.content.data_unavailable')}
</Card>
)
) : (
<Card type='inner' title={t('job_lifecycle.content.title_loading')}>
{t('job_lifecycle.content.loading')}
</Card>
)}
</Card>
);
}
export default JobLifecycleComponent;

View File

@@ -51,12 +51,14 @@ export function JobsAdminStatus({ insertAuditTrail, bodyshop, job }) {
}
return (
<Dropdown menu={statusMenu} trigger={["click"]} key="changestatus">
<Button shape="round">
<span>{job.status}</span>
<>
<Dropdown menu={statusMenu} trigger={["click"]} key="changestatus">
<Button shape="round">
<span>{job.status}</span>
<DownCircleFilled />
</Button>
</Dropdown>
<DownCircleFilled />
</Button>
</Dropdown>
</>
);
}

View File

@@ -1,34 +1,18 @@
import { useMutation } from "@apollo/client";
import { Button, notification } from "antd";
import { gql } from "@apollo/client";
import { Button, Space, notification } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import {
DELETE_DELIVERY_CHECKLIST,
DELETE_INTAKE_CHECKLIST,
} from "../../graphql/jobs.queries";
export default function JobAdminDeleteIntake({ job }) {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const [deleteIntake] = useMutation(gql`
mutation DELETE_INTAKE($jobId: uuid!) {
update_jobs_by_pk(
pk_columns: { id: $jobId }
_set: { intakechecklist: null }
) {
id
intakechecklist
}
}
`);
const [DELETE_DELIVERY] = useMutation(gql`
mutation DELETE_DELIVERY($jobId: uuid!) {
update_jobs_by_pk(
pk_columns: { id: $jobId }
_set: { deliverchecklist: null }
) {
id
deliverchecklist
}
}
`);
const [deleteIntake] = useMutation(DELETE_INTAKE_CHECKLIST);
const [deleteDelivery] = useMutation(DELETE_DELIVERY_CHECKLIST);
const handleDelete = async (values) => {
setLoading(true);
@@ -50,7 +34,7 @@ export default function JobAdminDeleteIntake({ job }) {
const handleDeleteDelivery = async (values) => {
setLoading(true);
const result = await DELETE_DELIVERY({
const result = await deleteDelivery({
variables: { jobId: job.id },
});
@@ -68,12 +52,22 @@ export default function JobAdminDeleteIntake({ job }) {
return (
<>
<Button loading={loading} onClick={handleDelete}>
{t("jobs.labels.deleteintake")}
</Button>
<Button loading={loading} onClick={handleDeleteDelivery}>
{t("jobs.labels.deletedelivery")}
</Button>
<Space wrap>
<Button
loading={loading}
onClick={handleDelete}
disabled={!job.intakechecklist}
>
{t("jobs.labels.deleteintake")}
</Button>
<Button
loading={loading}
onClick={handleDeleteDelivery}
disabled={!job.deliverychecklist}
>
{t("jobs.labels.deletedelivery")}
</Button>
</Space>
</>
);
}

View File

@@ -1,5 +1,5 @@
import { gql, useMutation } from "@apollo/client";
import { Button, notification } from "antd";
import { useMutation } from "@apollo/client";
import { Button, Space, notification } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
@@ -7,6 +7,11 @@ import dayjs from "../../utils/day";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
import {
MARK_JOB_AS_EXPORTED,
MARK_JOB_AS_UNINVOICED,
MARK_JOB_FOR_REEXPORT,
} from "../../graphql/jobs.queries";
import { insertAuditTrail } from "../../redux/application/application.actions";
import {
selectBodyshop,
@@ -35,58 +40,18 @@ export function JobAdminMarkReexport({
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const [insertExportLog] = useMutation(INSERT_EXPORT_LOG);
const [markJobForReexport] = useMutation(gql`
mutation MARK_JOB_FOR_REEXPORT($jobId: uuid!) {
update_jobs_by_pk(
pk_columns: { id: $jobId }
_set: { date_exported: null
status: "${bodyshop.md_ro_statuses.default_invoiced}"
}
) {
id
date_exported
status
date_invoiced
}
}
`);
const [markJobExported] = useMutation(gql`
mutation MARK_JOB_AS_EXPORTED($jobId: uuid!, $date_exported: timestamptz!) {
update_jobs_by_pk(
pk_columns: { id: $jobId }
_set: { date_exported: $date_exported
status: "${bodyshop.md_ro_statuses.default_exported}"
}
) {
id
date_exported
date_invoiced
status
}
}
`);
const [markJobUninvoiced] = useMutation(gql`
mutation MARK_JOB_AS_UNINVOICED($jobId: uuid!, ) {
update_jobs_by_pk(
pk_columns: { id: $jobId }
_set: { date_exported: null
date_invoiced: null
status: "${bodyshop.md_ro_statuses.default_delivered}"
}
) {
id
date_exported
date_invoiced
status
}
}
`);
const [markJobForReexport] = useMutation(MARK_JOB_FOR_REEXPORT);
const [markJobExported] = useMutation(MARK_JOB_AS_EXPORTED);
const [markJobUninvoiced] = useMutation(MARK_JOB_AS_UNINVOICED);
const handleMarkForExport = async () => {
setLoading(true);
const result = await markJobForReexport({
variables: { jobId: job.id },
variables: {
jobId: job.id,
default_invoiced: bodyshop.md_ro_statuses.default_invoiced,
},
});
if (!result.errors) {
@@ -108,7 +73,11 @@ export function JobAdminMarkReexport({
const handleMarkExported = async () => {
setLoading(true);
const result = await markJobExported({
variables: { jobId: job.id, date_exported: dayjs() },
variables: {
jobId: job.id,
date_exported: dayjs(),
default_exported: bodyshop.md_ro_statuses.default_exported,
},
});
await insertExportLog({
@@ -144,7 +113,10 @@ export function JobAdminMarkReexport({
const handleUninvoice = async () => {
setLoading(true);
const result = await markJobUninvoiced({
variables: { jobId: job.id },
variables: {
jobId: job.id,
default_delivered: bodyshop.md_ro_statuses.default_delivered,
},
});
if (!result.errors) {
@@ -165,27 +137,29 @@ export function JobAdminMarkReexport({
return (
<>
<Button
loading={loading}
disabled={!job.date_exported}
onClick={handleMarkForExport}
>
{t("jobs.labels.markforreexport")}
</Button>
<Button
loading={loading}
disabled={job.date_exported}
onClick={handleMarkExported}
>
{t("jobs.actions.markasexported")}
</Button>
<Button
loading={loading}
disabled={!job.date_invoiced || job.date_exported}
onClick={handleUninvoice}
>
{t("jobs.actions.uninvoice")}
</Button>
<Space wrap>
<Button
loading={loading}
disabled={!job.date_exported}
onClick={handleMarkForExport}
>
{t("jobs.labels.markforreexport")}
</Button>
<Button
loading={loading}
disabled={job.date_exported}
onClick={handleMarkExported}
>
{t("jobs.actions.markasexported")}
</Button>
<Button
loading={loading}
disabled={!job.date_invoiced || job.date_exported}
onClick={handleUninvoice}
>
{t("jobs.actions.uninvoice")}
</Button>
</Space>
</>
);
}

View File

@@ -0,0 +1,65 @@
import { useMutation } from "@apollo/client";
import { Switch, notification } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { UPDATE_REMOVE_FROM_AR } from "../../graphql/jobs.queries";
import { insertAuditTrail } from "../../redux/application/application.actions";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
const mapStateToProps = createStructuredSelector({});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation })),
});
export default connect(mapStateToProps, mapDispatchToProps)(JobsAdminRemoveAR);
export function JobsAdminRemoveAR({ insertAuditTrail, job }) {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const [switchValue, setSwitchValue] = useState(job.remove_from_ar);
const [mutationUpdateRemoveFromAR] = useMutation(UPDATE_REMOVE_FROM_AR);
const handleChange = async (value) => {
setLoading(true);
const result = await mutationUpdateRemoveFromAR({
variables: { jobId: job.id, remove_from_ar: value },
});
if (!result.errors) {
notification["success"]({ message: t("jobs.successes.save") });
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.admin_job_remove_from_ar(value),
});
setSwitchValue(value);
} else {
notification["error"]({
message: t("jobs.errors.saving", {
error: JSON.stringify(result.errors),
}),
});
}
setLoading(false);
};
return (
<>
<div style={{ display: "flex", alignItems: "center" }}>
<div style={{ marginRight: "10px" }}>
{t("jobs.labels.remove_from_ar")}:
</div>
<div>
<Switch
checked={switchValue}
loading={loading}
onChange={handleChange}
/>
</div>
</div>
</>
);
}

View File

@@ -1,9 +1,10 @@
import { gql, useMutation } from "@apollo/client";
import { useMutation } from "@apollo/client";
import { Button, notification } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { UNVOID_JOB } from "../../graphql/jobs.queries";
import { insertAuditTrail } from "../../redux/application/application.actions";
import {
selectBodyshop,
@@ -29,66 +30,17 @@ export function JobsAdminUnvoid({
}) {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const [updateJob] = useMutation(gql`
mutation UNVOID_JOB($jobId: uuid!) {
update_jobs_by_pk(pk_columns: {id: $jobId}, _set: {voided: false, status: "${
bodyshop.md_ro_statuses.default_imported
}", date_void: null}) {
id
date_void
voided
status
}
insert_notes(objects: {jobid: $jobId, audit: true, created_by: "${
currentUser.email
}", text: "${t("jobs.labels.unvoidnote")}"}) {
returning {
id
}
}
}
`);
// const result = await voidJob({
// variables: {
// jobId: job.id,
// job: {
// status: bodyshop.md_ro_statuses.default_void,
// voided: true,
// },
// note: [
// {
// jobid: job.id,
// created_by: currentUser.email,
// audit: true,
// text: t("jobs.labels.voidnote", {
// date: dayjs().format("MM/DD/yyy"),
// time: dayjs().format("hh:mm a"),
// }),
// },
// ],
// },
// });
// if (!!!result.errors) {
// notification["success"]({
// message: t("jobs.successes.voided"),
// });
// //go back to jobs list.
// history.push(`/manage/`);
// } else {
// notification["error"]({
// message: t("jobs.errors.voiding", {
// error: JSON.stringify(result.errors),
// }),
// });
// }
const [mutationUnvoidJob] = useMutation(UNVOID_JOB);
const handleUpdate = async (values) => {
setLoading(true);
const result = await updateJob({
variables: { jobId: job.id },
const result = await mutationUnvoidJob({
variables: {
jobId: job.id,
default_imported: bodyshop.md_ro_statuses.default_imported,
currentUserEmail: currentUser.email,
text: t("jobs.labels.unvoidnote"),
},
});
if (!result.errors) {
@@ -110,8 +62,10 @@ mutation UNVOID_JOB($jobId: uuid!) {
};
return (
<Button loading={loading} disabled={!job.voided} onClick={handleUpdate}>
{t("jobs.actions.unvoid")}
</Button>
<>
<Button loading={loading} disabled={!job.voided} onClick={handleUpdate}>
{t("jobs.actions.unvoid")}
</Button>
</>
);
}

View File

@@ -624,7 +624,7 @@ export function JobsDetailHeaderActions({
{
key: 'cancelallappointments',
onClick: () => {
if ( job.status !== bodyshop.md_ro_statuses.default_scheduled) {
if (job.status !== bodyshop.md_ro_statuses.default_scheduled) {
return;
}
showCancelScheduleModal()
@@ -713,12 +713,7 @@ export function JobsDetailHeaderActions({
menuItems.push({
key: 'cccontract',
disabled: jobRO || !job.converted,
label: <Link
to={{
pathname: "/manage/courtesycars/contracts/new",
state: {jobId: job.id},
}}
>
label: <Link state={{jobId: job.id}} to='/manage/courtesycars/contracts/new'>
{t("menus.jobsactions.newcccontract")}
</Link>
});
@@ -993,10 +988,11 @@ export function JobsDetailHeaderActions({
onOk={handleCancelScheduleOK}
onCancel={handleCancelScheduleModalCancel}
footer={[
<Button form="cancelScheduleForm" key="back" onClick={handleCancelScheduleModalCancel}>
<Button form="cancelScheduleForm" key="back" onClick={handleCancelScheduleModalCancel}>
{t("general.actions.cancel")}
</Button>,
<Button form="cancelScheduleForm" htmlType="submit" key="submit" type="primary" loading={loading} onClick={handleCancelScheduleOK}>
<Button form="cancelScheduleForm" htmlType="submit" key="submit" type="primary" loading={loading}
onClick={handleCancelScheduleOK}>
{t("appointments.actions.cancel")}
</Button>,
]}
@@ -1004,7 +1000,7 @@ export function JobsDetailHeaderActions({
<Form
layout="vertical"
id="cancelScheduleForm"
onFinish={s =>{
onFinish={s => {
console.log(s);
handleLostSaleFinish(s);
}}

View File

@@ -225,7 +225,7 @@ export function PayableExportAll({
if (bodyshop.pbs_serialnumber)
return (
<Link to={{ state: { billids }, pathname: `/manage/dmsap` }}>
<Link to='/manage/dmsap' state={{ billids }}>
<Button>{t("jobs.actions.export")}</Button>
</Link>
);

View File

@@ -218,7 +218,7 @@ export function PayableExportButton({
if (bodyshop.pbs_serialnumber)
return (
<Link to={{ state: { billids: [billId] }, pathname: `/manage/dmsap` }}>
<Link to='/manage/dmsap' state={{billids: [billId]}}>
<Button>{t("jobs.actions.export")}</Button>
</Link>
);

View File

@@ -68,8 +68,8 @@ export function ReportCenterModalComponent({ reportCenterModal }) {
const handleFinish = async (values) => {
setLoading(true);
const start = values.dates[0];
const end = values.dates[1];
const start = values.dates ? values.dates[0] : null;
const end = values.dates ? values.dates[1] : null;
const { id } = values;
await GenerateDocument(
@@ -239,20 +239,30 @@ export function ReportCenterModalComponent({ reportCenterModal }) {
else return null;
}}
</Form.Item>
<Form.Item
name="dates"
label={t("reportcenter.labels.dates")}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<DatePicker.RangePicker
format="MM/DD/YYYY"
presets={DatePIckerRanges}
/>
<Form.Item style={{ margin: 0, padding: 0 }} dependencies={["key"]}>
{() => {
const key = form.getFieldValue("key");
const datedisable = Templates[key] && Templates[key].datedisable;
if (datedisable !== true) {
return (
<Form.Item
name="dates"
label={t("reportcenter.labels.dates")}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<DatePicker.RangePicker
format="MM/DD/YYYY"
ranges={DatePIckerRanges}
/>
</Form.Item>
);
} else return null;
}}
</Form.Item>
<Form.Item style={{ margin: 0, padding: 0 }} dependencies={["key"]}>
{() => {

View File

@@ -1,13 +1,13 @@
import { useQuery } from "@apollo/client";
import { Col, Row } from "antd";
import {useQuery} from "@apollo/client";
import {Col, Row} from "antd";
import _ from "lodash";
import dayjs from "../../utils/day";
import React, { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { QUERY_TIME_TICKETS_IN_RANGE_SB } from "../../graphql/timetickets.queries";
import { selectBodyshop } from "../../redux/user/user.selectors";
import React, {useMemo} from "react";
import {useTranslation} from "react-i18next";
import {connect} from "react-redux";
import {createStructuredSelector} from "reselect";
import {QUERY_TIME_TICKETS_IN_RANGE_SB} from "../../graphql/timetickets.queries";
import {selectBodyshop} from "../../redux/user/user.selectors";
import AlertComponent from "../alert/alert.component";
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
import * as Utils from "../scoreboard-targets-table/scoreboard-targets-table.util";
@@ -16,396 +16,397 @@ import ScoreboardTicketsStats from "./scoreboard-timetickets.stats.component";
import ScoreboardTimeticketsTargetsTable from "./scoreboard-timetickets.targets-table.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(
mapStateToProps,
mapDispatchToProps
mapStateToProps,
mapDispatchToProps
)(ScoreboardTimeTicketsStats);
export function ScoreboardTimeTicketsStats({ bodyshop }) {
const { t } = useTranslation();
const startDate = dayjs().startOf("month")
const endDate = dayjs().endOf("month");
export function ScoreboardTimeTicketsStats({bodyshop}) {
const {t} = useTranslation();
const startDate = dayjs().startOf("month")
const endDate = dayjs().endOf("month");
const fixedPeriods = useMemo(() => {
const endOfThisMonth = dayjs().endOf("month");
const startofthisMonth = dayjs().startOf("month");
const fixedPeriods = useMemo(() => {
const endOfThisMonth = dayjs().endOf("month");
const startofthisMonth = dayjs().startOf("month");
const endOfLastmonth = dayjs().subtract(1, "month").endOf("month");
const startOfLastmonth = dayjs().subtract(1, "month").startOf("month");
const endOfLastmonth = dayjs().subtract(1, "month").endOf("month");
const startOfLastmonth = dayjs().subtract(1, "month").startOf("month");
const endOfThisWeek = dayjs().endOf("week");
const startOfThisWeek = dayjs().startOf("week");
const endOfThisWeek = dayjs().endOf("week");
const startOfThisWeek = dayjs().startOf("week");
const endOfLastWeek = dayjs().subtract(1, "week").endOf("week");
const startOfLastWeek = dayjs().subtract(1, "week").startOf("week");
const endOfLastWeek = dayjs().subtract(1, "week").endOf("week");
const startOfLastWeek = dayjs().subtract(1, "week").startOf("week");
const endOfPriorWeek = dayjs().subtract(2, "week").endOf("week");
const startOfPriorWeek = dayjs().subtract(2, "week").startOf("week");
const endOfPriorWeek = dayjs().subtract(2, "week").endOf("week");
const startOfPriorWeek = dayjs().subtract(2, "week").startOf("week");
const allDates = [
endOfThisMonth,
startofthisMonth,
endOfLastmonth,
startOfLastmonth,
endOfThisWeek,
startOfThisWeek,
endOfLastWeek,
startOfLastWeek,
endOfPriorWeek,
startOfPriorWeek,
];
const start = dayjs.min(allDates);
const end = dayjs.max(allDates);
return {
start,
end,
endOfThisMonth,
startofthisMonth,
endOfLastmonth,
startOfLastmonth,
endOfThisWeek,
startOfThisWeek,
endOfLastWeek,
startOfLastWeek,
endOfPriorWeek,
startOfPriorWeek,
};
}, []);
const allDates = [
endOfThisMonth,
startofthisMonth,
endOfLastmonth,
startOfLastmonth,
endOfThisWeek,
startOfThisWeek,
endOfLastWeek,
startOfLastWeek,
endOfPriorWeek,
startOfPriorWeek,
];
const start = dayjs.min(allDates);
const end = dayjs.max(allDates);
return {
start,
end,
endOfThisMonth,
startofthisMonth,
endOfLastmonth,
startOfLastmonth,
endOfThisWeek,
startOfThisWeek,
endOfLastWeek,
startOfLastWeek,
endOfPriorWeek,
startOfPriorWeek,
};
}, []);
const { loading, error, data } = useQuery(QUERY_TIME_TICKETS_IN_RANGE_SB, {
variables: {
start: startDate.format("YYYY-MM-DD"),
end: endDate.format("YYYY-MM-DD"),
fixedStart: fixedPeriods.start.format("YYYY-MM-DD"),
fixedEnd: fixedPeriods.end.format("YYYY-MM-DD"),
jobStart: startDate,
jobEnd: endDate,
},
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
pollInterval: 60000,
skip: !fixedPeriods,
});
const calculatedData = useMemo(() => {
if (!data) return [];
const ret = {
totalThisWeek: 0,
totalThisWeekLAB: 0,
totalThisWeekLAR: 0,
totalLastWeek: 0,
totalLastWeekLAB: 0,
totalLastWeekLAR: 0,
totalPriorWeek: 0,
totalPriorWeekLAB: 0,
totalPriorWeekLAR: 0,
totalThisMonth: 0,
totalThisMonthLAB: 0,
totalThisMonthLAR: 0,
totalLastMonth: 0,
totalLastMonthLAB: 0,
totalLastMonthLAR: 0,
actualTotalOverPeriod: 0,
actualTotalOverPeriodLAB: 0,
actualTotalOverPeriodLAR: 0,
totalEffieciencyOverPeriod: 0,
totalEffieciencyOverPeriodLAB: 0,
totalEffieciencyOverPeriodLAR: 0,
seperatedThisWeek: {
sunday: {
total: 0,
lab: 0,
lar: 0,
const {loading, error, data} = useQuery(QUERY_TIME_TICKETS_IN_RANGE_SB, {
variables: {
start: startDate.format("YYYY-MM-DD"),
end: endDate.format("YYYY-MM-DD"),
fixedStart: fixedPeriods.start.format("YYYY-MM-DD"),
fixedEnd: fixedPeriods.end.format("YYYY-MM-DD"),
jobStart: startDate,
jobEnd: endDate,
},
monday: {
total: 0,
lab: 0,
lar: 0,
},
tuesday: {
total: 0,
lab: 0,
lar: 0,
},
wednesday: {
total: 0,
lab: 0,
lar: 0,
},
thursday: {
total: 0,
lab: 0,
lar: 0,
},
friday: {
total: 0,
lab: 0,
lar: 0,
},
saturday: {
total: 0,
lab: 0,
lar: 0,
},
},
};
data.fixedperiod.forEach((ticket) => {
const ticketDate = dayjs(ticket.date);
if (
ticketDate.isBetween(
fixedPeriods.startOfThisWeek,
fixedPeriods.endOfThisWeek,
undefined,
"[]"
)
) {
ret.totalThisWeek = ret.totalThisWeek + ticket.productivehrs;
if (ticket.ciecacode !== "LAR")
ret.totalThisWeekLAB = ret.totalThisWeekLAB + ticket.productivehrs;
if (ticket.ciecacode === "LAR")
ret.totalThisWeekLAR = ret.totalThisWeekLAR + ticket.productivehrs;
//Seperate out to Day of Week
ret.seperatedThisWeek[
dayjs(ticket.date).format("dddd").toLowerCase()
].total =
ret.seperatedThisWeek[
dayjs(ticket.date).format("dddd").toLowerCase()
].total + ticket.productivehrs;
if (ticket.ciecacode !== "LAR")
ret.seperatedThisWeek[
dayjs(ticket.date).format("dddd").toLowerCase()
].lab =
ret.seperatedThisWeek[
dayjs(ticket.date).format("dddd").toLowerCase()
].lab + ticket.productivehrs;
if (ticket.ciecacode === "LAR")
ret.seperatedThisWeek[
dayjs(ticket.date).format("dddd").toLowerCase()
].lar =
ret.seperatedThisWeek[
dayjs(ticket.date).format("dddd").toLowerCase()
].lar + ticket.productivehrs;
} else if (
ticketDate.isBetween(
fixedPeriods.startOfLastWeek,
fixedPeriods.endOfLastWeek,
undefined,
"[]"
)
) {
ret.totalLastWeek = ret.totalLastWeek + ticket.productivehrs;
if (ticket.ciecacode !== "LAR")
ret.totalLastWeekLAB = ret.totalLastWeekLAB + ticket.productivehrs;
if (ticket.ciecacode === "LAR")
ret.totalLastWeekLAR = ret.totalLastWeekLAR + ticket.productivehrs;
} else if (
ticketDate.isBetween(
fixedPeriods.startOfPriorWeek,
fixedPeriods.endOfPriorWeek,
undefined,
"[]"
)
) {
ret.totalPriorWeek = ret.totalPriorWeek + ticket.productivehrs;
if (ticket.ciecacode !== "LAR")
ret.totalPriorWeekLAB = ret.totalPriorWeekLAB + ticket.productivehrs;
if (ticket.ciecacode === "LAR")
ret.totalPriorWeekLAR = ret.totalPriorWeekLAR + ticket.productivehrs;
}
if (
ticketDate.isBetween(
fixedPeriods.startofthisMonth,
fixedPeriods.endOfThisMonth,
undefined,
"[]"
)
) {
ret.totalThisMonth = ret.totalThisMonth + ticket.productivehrs;
ret.actualTotalOverPeriod =
ret.actualTotalOverPeriod + (ticket.actualhrs || 0);
if (ticket.ciecacode !== "LAR") {
ret.totalThisMonthLAB = ret.totalThisMonthLAB + ticket.productivehrs;
ret.actualTotalOverPeriodLAB =
ret.actualTotalOverPeriodLAB + (ticket.actualhrs || 0);
}
if (ticket.ciecacode === "LAR") {
ret.totalThisMonthLAR = ret.totalThisMonthLAR + ticket.productivehrs;
ret.actualTotalOverPeriodLAR =
ret.actualTotalOverPeriodLAR + (ticket.actualhrs || 0);
}
} else if (
ticketDate.isBetween(
fixedPeriods.startOfLastmonth,
fixedPeriods.endOfLastmonth,
undefined,
"[]"
)
) {
ret.totalLastMonth = ret.totalLastMonth + ticket.productivehrs;
if (ticket.ciecacode !== "LAR")
ret.totalLastMonthLAB = ret.totalLastMonthLAB + ticket.productivehrs;
if (ticket.ciecacode === "LAR")
ret.totalLastMonthLAR = ret.totalLastMonthLAR + ticket.productivehrs;
}
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
pollInterval: 60000,
skip: !fixedPeriods,
});
ret.totalEffieciencyOverPeriod = ret.actualTotalOverPeriod
? (ret.totalThisMonth / ret.actualTotalOverPeriod) * 100
: 0;
ret.totalEffieciencyOverPeriodLAB = ret.actualTotalOverPeriodLAB
? (ret.totalThisMonthLAB / ret.actualTotalOverPeriodLAB) * 100
: 0;
ret.totalEffieciencyOverPeriodLAR = ret.actualTotalOverPeriodLAR
? (ret.totalThisMonthLAR / ret.actualTotalOverPeriodLAR) * 100
: 0;
const calculatedData = useMemo(() => {
if (!data) return [];
const ret = {
totalThisWeek: 0,
totalThisWeekLAB: 0,
totalThisWeekLAR: 0,
totalLastWeek: 0,
totalLastWeekLAB: 0,
totalLastWeekLAR: 0,
totalPriorWeek: 0,
totalPriorWeekLAB: 0,
totalPriorWeekLAR: 0,
totalThisMonth: 0,
totalThisMonthLAB: 0,
totalThisMonthLAR: 0,
totalLastMonth: 0,
totalLastMonthLAB: 0,
totalLastMonthLAR: 0,
actualTotalOverPeriod: 0,
actualTotalOverPeriodLAB: 0,
actualTotalOverPeriodLAR: 0,
totalEffieciencyOverPeriod: 0,
totalEffieciencyOverPeriodLAB: 0,
totalEffieciencyOverPeriodLAR: 0,
seperatedThisWeek: {
sunday: {
total: 0,
lab: 0,
lar: 0,
},
monday: {
total: 0,
lab: 0,
lar: 0,
},
tuesday: {
total: 0,
lab: 0,
lar: 0,
},
wednesday: {
total: 0,
lab: 0,
lar: 0,
},
thursday: {
total: 0,
lab: 0,
lar: 0,
},
friday: {
total: 0,
lab: 0,
lar: 0,
},
saturday: {
total: 0,
lab: 0,
lar: 0,
},
},
};
roundObject(ret);
data.fixedperiod.forEach((ticket) => {
const ticketDate = dayjs(ticket.date);
if (
ticketDate.isBetween(
fixedPeriods.startOfThisWeek,
fixedPeriods.endOfThisWeek,
undefined,
"[]"
)
) {
ret.totalThisWeek = ret.totalThisWeek + ticket.productivehrs;
if (ticket.ciecacode !== "LAR")
ret.totalThisWeekLAB = ret.totalThisWeekLAB + ticket.productivehrs;
if (ticket.ciecacode === "LAR")
ret.totalThisWeekLAR = ret.totalThisWeekLAR + ticket.productivehrs;
const ticketsGroupedByDate = _.groupBy(data.timetickets, "date");
const listOfDays = Utils.ListOfDaysInCurrentMonth();
const combinedData = [],
labData = [],
larData = [];
var acc_comb = 0;
var acc_lab = 0;
var acc_lar = 0;
listOfDays.forEach((day) => {
const r = {
date: dayjs(day).format("MM/DD"),
actualhrs: 0,
productivehrs: 0,
};
const combined = {
accTargetHrs: _.round(
Utils.AsOfDateTargetHours(
bodyshop.scoreboard_target.dailyBodyTarget +
bodyshop.scoreboard_target.dailyPaintTarget,
day
) +
(bodyshop.scoreboard_target.dailyBodyTarget +
bodyshop.scoreboard_target.dailyPaintTarget),
1
),
accHrs: 0,
};
const lab = {
accTargetHrs: _.round(
Utils.AsOfDateTargetHours(
bodyshop.scoreboard_target.dailyBodyTarget,
day
) + bodyshop.scoreboard_target.dailyBodyTarget,
1
),
accHrs: 0,
};
const lar = {
accTargetHrs: _.round(
Utils.AsOfDateTargetHours(
bodyshop.scoreboard_target.dailyPaintTarget,
day
) + bodyshop.scoreboard_target.dailyPaintTarget,
1
),
accHrs: 0,
};
if (ticketsGroupedByDate[day]) {
ticketsGroupedByDate[day].forEach((ticket) => {
r.actualhrs = r.actualhrs + ticket.actualhrs;
r.productivehrs = r.productivehrs + ticket.productivehrs;
acc_comb = acc_comb + ticket.productivehrs;
if (ticket.ciecacode !== "LAR")
acc_lab = acc_lab + ticket.productivehrs;
if (ticket.ciecacode === "LAR")
acc_lar = acc_lar + ticket.productivehrs;
//Seperate out to Day of Week
ret.seperatedThisWeek[
dayjs(ticket.date).format("dddd").toLowerCase()
].total =
ret.seperatedThisWeek[
dayjs(ticket.date).format("dddd").toLowerCase()
].total + ticket.productivehrs;
if (ticket.ciecacode !== "LAR")
ret.seperatedThisWeek[
dayjs(ticket.date).format("dddd").toLowerCase()
].lab =
ret.seperatedThisWeek[
dayjs(ticket.date).format("dddd").toLowerCase()
].lab + ticket.productivehrs;
if (ticket.ciecacode === "LAR")
ret.seperatedThisWeek[
dayjs(ticket.date).format("dddd").toLowerCase()
].lar =
ret.seperatedThisWeek[
dayjs(ticket.date).format("dddd").toLowerCase()
].lar + ticket.productivehrs;
} else if (
ticketDate.isBetween(
fixedPeriods.startOfLastWeek,
fixedPeriods.endOfLastWeek,
undefined,
"[]"
)
) {
ret.totalLastWeek = ret.totalLastWeek + ticket.productivehrs;
if (ticket.ciecacode !== "LAR")
ret.totalLastWeekLAB = ret.totalLastWeekLAB + ticket.productivehrs;
if (ticket.ciecacode === "LAR")
ret.totalLastWeekLAR = ret.totalLastWeekLAR + ticket.productivehrs;
} else if (
ticketDate.isBetween(
fixedPeriods.startOfPriorWeek,
fixedPeriods.endOfPriorWeek,
undefined,
"[]"
)
) {
ret.totalPriorWeek = ret.totalPriorWeek + ticket.productivehrs;
if (ticket.ciecacode !== "LAR")
ret.totalPriorWeekLAB = ret.totalPriorWeekLAB + ticket.productivehrs;
if (ticket.ciecacode === "LAR")
ret.totalPriorWeekLAR = ret.totalPriorWeekLAR + ticket.productivehrs;
}
if (
ticketDate.isBetween(
fixedPeriods.startofthisMonth,
fixedPeriods.endOfThisMonth,
undefined,
"[]"
)
) {
ret.totalThisMonth = ret.totalThisMonth + ticket.productivehrs;
ret.actualTotalOverPeriod =
ret.actualTotalOverPeriod + (ticket.actualhrs || 0);
if (ticket.ciecacode !== "LAR") {
ret.totalThisMonthLAB = ret.totalThisMonthLAB + ticket.productivehrs;
ret.actualTotalOverPeriodLAB =
ret.actualTotalOverPeriodLAB + (ticket.actualhrs || 0);
}
if (ticket.ciecacode === "LAR") {
ret.totalThisMonthLAR = ret.totalThisMonthLAR + ticket.productivehrs;
ret.actualTotalOverPeriodLAR =
ret.actualTotalOverPeriodLAR + (ticket.actualhrs || 0);
}
} else if (
ticketDate.isBetween(
fixedPeriods.startOfLastmonth,
fixedPeriods.endOfLastmonth,
undefined,
"[]"
)
) {
ret.totalLastMonth = ret.totalLastMonth + ticket.productivehrs;
if (ticket.ciecacode !== "LAR")
ret.totalLastMonthLAB = ret.totalLastMonthLAB + ticket.productivehrs;
if (ticket.ciecacode === "LAR")
ret.totalLastMonthLAR = ret.totalLastMonthLAR + ticket.productivehrs;
}
});
}
combined.accHrs = acc_comb;
lab.accHrs = acc_lab;
lar.accHrs = acc_lar;
combinedData.push({ ...r, ...combined });
labData.push({ ...r, ...lab });
larData.push({ ...r, ...lar });
});
ret.totalEffieciencyOverPeriod = ret.actualTotalOverPeriod
? (ret.totalThisMonth / ret.actualTotalOverPeriod) * 100
: 0;
ret.totalEffieciencyOverPeriodLAB = ret.actualTotalOverPeriodLAB
? (ret.totalThisMonthLAB / ret.actualTotalOverPeriodLAB) * 100
: 0;
ret.totalEffieciencyOverPeriodLAR = ret.actualTotalOverPeriodLAR
? (ret.totalThisMonthLAR / ret.actualTotalOverPeriodLAR) * 100
: 0;
const jobData = {};
roundObject(ret);
data.jobs.forEach((job) => {
job.tthrs = job.joblines.reduce((acc, val) => acc + val.mod_lb_hrs, 0);
});
const ticketsGroupedByDate = _.groupBy(data.timetickets, "date");
jobData.tthrs = data.jobs
.reduce((acc, val) => acc + val.tthrs, 0)
.toFixed(1);
const listOfDays = Utils.ListOfDaysInCurrentMonth();
jobData.count = data.jobs.length.toFixed(0);
const combinedData = [],
labData = [],
larData = [];
var acc_comb = 0;
var acc_lab = 0;
var acc_lar = 0;
return {
fixed: ret,
combinedData: combinedData,
labData: labData,
larData: larData,
jobData: jobData,
};
}, [fixedPeriods, data, bodyshop]);
listOfDays.forEach((day) => {
const r = {
date: dayjs(day).format("MM/DD"),
actualhrs: 0,
productivehrs: 0,
};
if (error) return <AlertComponent message={error.message} type="error" />;
if (loading) return <LoadingSpinner />;
return (
<Row gutter={[16, 16]}>
<Col span={24}>
<ScoreboardTimeticketsTargetsTable />
</Col>
<Col span={24}>
<ScoreboardTicketsStats
data={calculatedData.fixed}
jobData={calculatedData.jobData}
/>
</Col>
<Col span={24}>
<ScoreboardTimeTicketsChart
data={calculatedData.combinedData}
chartTitle={t("scoreboard.labels.combinedcharttitle")}
/>
</Col>
<Col span={12}>
<ScoreboardTimeTicketsChart
data={calculatedData.labData}
chartTitle={t("scoreboard.labels.bodycharttitle")}
/>
</Col>
<Col span={12}>
<ScoreboardTimeTicketsChart
data={calculatedData.larData}
chartTitle={t("scoreboard.labels.refinishcharttitle")}
/>
</Col>
</Row>
);
const combined = {
accTargetHrs: _.round(
Utils.AsOfDateTargetHours(
bodyshop.scoreboard_target.dailyBodyTarget +
bodyshop.scoreboard_target.dailyPaintTarget,
day
) +
(bodyshop.scoreboard_target.dailyBodyTarget +
bodyshop.scoreboard_target.dailyPaintTarget),
1
),
accHrs: 0,
};
const lab = {
accTargetHrs: _.round(
Utils.AsOfDateTargetHours(
bodyshop.scoreboard_target.dailyBodyTarget,
day
) + bodyshop.scoreboard_target.dailyBodyTarget,
1
),
accHrs: 0,
};
const lar = {
accTargetHrs: _.round(
Utils.AsOfDateTargetHours(
bodyshop.scoreboard_target.dailyPaintTarget,
day
) + bodyshop.scoreboard_target.dailyPaintTarget,
1
),
accHrs: 0,
};
if (ticketsGroupedByDate[day]) {
ticketsGroupedByDate[day].forEach((ticket) => {
r.actualhrs = r.actualhrs + ticket.actualhrs;
r.productivehrs = r.productivehrs + ticket.productivehrs;
acc_comb = acc_comb + ticket.productivehrs;
if (ticket.ciecacode !== "LAR")
acc_lab = acc_lab + ticket.productivehrs;
if (ticket.ciecacode === "LAR")
acc_lar = acc_lar + ticket.productivehrs;
});
}
combined.accHrs = acc_comb;
lab.accHrs = acc_lab;
lar.accHrs = acc_lar;
combinedData.push({...r, ...combined});
labData.push({...r, ...lab});
larData.push({...r, ...lar});
});
const jobData = {};
const dataJobs = data.jobs.map((job) => ({
...job,
tthrs: job.joblines.reduce((acc, val) => acc + val.mod_lb_hrs, 0)
}));
jobData.tthrs = dataJobs
.reduce((acc, val) => acc + val.tthrs, 0)
.toFixed(1);
jobData.count = dataJobs.length.toFixed(0);
return {
fixed: ret,
combinedData: combinedData,
labData: labData,
larData: larData,
jobData: jobData,
};
}, [fixedPeriods, data, bodyshop]);
if (error) return <AlertComponent message={error.message} type="error"/>;
if (loading) return <LoadingSpinner/>;
return (
<Row gutter={[16, 16]}>
<Col span={24}>
<ScoreboardTimeticketsTargetsTable/>
</Col>
<Col span={24}>
<ScoreboardTicketsStats
data={calculatedData.fixed}
jobData={calculatedData.jobData}
/>
</Col>
<Col span={24}>
<ScoreboardTimeTicketsChart
data={calculatedData.combinedData}
chartTitle={t("scoreboard.labels.combinedcharttitle")}
/>
</Col>
<Col span={12}>
<ScoreboardTimeTicketsChart
data={calculatedData.labData}
chartTitle={t("scoreboard.labels.bodycharttitle")}
/>
</Col>
<Col span={12}>
<ScoreboardTimeTicketsChart
data={calculatedData.larData}
chartTitle={t("scoreboard.labels.refinishcharttitle")}
/>
</Col>
</Row>
);
}
function roundObject(inputObj) {
for (var key of Object.keys(inputObj)) {
if (typeof inputObj[key] === "number") {
inputObj[key] = inputObj[key].toFixed(1);
} else if (Array.isArray(inputObj[key])) {
inputObj[key].forEach((item) => roundObject(item));
} else if (typeof inputObj[key] === "object") {
roundObject(inputObj[key]);
for (var key of Object.keys(inputObj)) {
if (typeof inputObj[key] === "number") {
inputObj[key] = inputObj[key].toFixed(1);
} else if (Array.isArray(inputObj[key])) {
inputObj[key].forEach((item) => roundObject(item));
} else if (typeof inputObj[key] === "object") {
roundObject(inputObj[key]);
}
}
}
}

View File

@@ -8,6 +8,21 @@ export const INTROSPECTION = gql`
}
}
`;
export const QUERY_EULA = gql`
query QUERY_EULA($now: timestamptz!) {
eulas(where: {effective_date: {_lte: $now}, _or: [{end_date: {_is_null: true}}, {end_date: {_gt: $now}}]}) {
id
content
eula_acceptances {
id
date_accepted
}
}
}
`;
export const QUERY_BODYSHOP = gql`
query QUERY_BODYSHOP {
bodyshops(where: { associations: { active: { _eq: true } } }) {

View File

@@ -545,147 +545,166 @@ export const QUERY_JOB_COSTING_DETAILS = gql`
export const GET_JOB_BY_PK = gql`
query GET_JOB_BY_PK($id: uuid!) {
jobs_by_pk(id: $id) {
updated_at
actual_completion
actual_delivery
actual_in
adjustment_bottom_line
area_of_damage
auto_add_ats
available_jobs {
id
}
alt_transport
ca_bc_pvrt
ca_customer_gst
ca_gst_registrant
category
cccontracts {
agreementnumber
courtesycar {
fleetnumber
id
make
model
plate
year
}
id
scheduledreturn
start
status
}
cieca_ttl
class
clm_no
clm_total
comment
converted
csiinvites {
completedon
id
}
date_estimated
date_exported
date_invoiced
date_last_contacted
date_lost_sale
date_next_contact
date_open
date_rentalresp
date_repairstarted
date_scheduled
date_towin
date_void
ded_amt
ded_note
ded_status
deliverchecklist
depreciation_taxes
driveable
employee_body
employee_body_rel {
id
first_name
last_name
}
employee_refinish_rel {
id
first_name
last_name
}
employee_prep_rel {
id
first_name
last_name
}
employee_csr
employee_csr_rel {
id
first_name
last_name
}
employee_csr
employee_prep
employee_prep_rel {
id
first_name
last_name
}
employee_refinish
employee_body
alt_transport
intakechecklist
invoice_final_note
comment
loss_desc
kmin
kmout
referral_source
referral_source_extra
unit_number
po_number
special_coverage_policy
scheduled_delivery
converted
lbr_adjustments
ro_number
po_number
clm_total
employee_refinish_rel {
id
first_name
last_name
}
est_co_nm
est_ct_fn
est_ct_ln
est_ea
est_ph1
federal_tax_rate
id
inproduction
vehicleid
plate_no
plate_st
v_vin
v_model_yr
v_model_desc
v_make_desc
v_color
vehicleid
driveable
towin
loss_of_use
lost_sale_reason
vehicle {
id
plate_no
plate_st
v_vin
v_model_yr
v_model_desc
v_make_desc
v_color
notes
v_paint_codes
jobs {
id
ro_number
status
clm_no
}
}
available_jobs {
id
}
ins_co_id
policy_no
loss_date
clm_no
area_of_damage
ins_co_nm
ins_addr1
ins_city
ins_co_id
ins_co_nm
ins_ct_ln
ins_ct_fn
ins_ea
ins_ph1
est_co_nm
est_ct_fn
est_ct_ln
est_ph1
est_ea
selling_dealer
servicing_dealer
selling_dealer_contact
servicing_dealer_contact
regie_number
scheduled_completion
id
ded_amt
ded_status
depreciation_taxes
other_amount_payable
towing_payable
storage_payable
adjustment_bottom_line
federal_tax_rate
state_tax_rate
local_tax_rate
tax_tow_rt
tax_str_rt
tax_paint_mat_rt
tax_shop_mat_rt
tax_sub_rt
tax_lbr_rt
tax_levies_rt
parts_tax_rates
job_totals
ownr_fn
ownr_ln
ownr_co_nm
ownr_ea
ownr_addr1
ownr_addr2
ownr_city
ownr_st
ownr_zip
ownr_ctry
ownr_ph1
ownr_ph2
production_vars
ca_gst_registrant
ownerid
ded_note
materials
auto_add_ats
rate_ats
intakechecklist
invoice_final_note
iouparent
job_totals
joblines(where: { removed: { _eq: false } }, order_by: { line_no: asc }) {
act_price
ah_detail_line
alt_partm
alt_partno
billlines(limit: 1, order_by: { bill: { date: desc } }) {
actual_cost
actual_price
bill {
id
invoice_number
vendor {
id
name
}
}
joblineid
id
quantity
}
convertedtolbr
critical
db_hrs
db_price
db_ref
id
ioucreated
lbr_amt
lbr_op
line_desc
line_ind
line_no
line_ref
location
manual_line
mod_lb_hrs
mod_lbr_ty
notes
oem_partno
op_code_desc
part_qty
part_type
prt_dsmk_m
prt_dsmk_p
status
tax_part
unq_seq
}
kmin
kmout
labor_rate_desc
lbr_adjustments
local_tax_rate
loss_date
loss_desc
loss_of_use
lost_sale_reason
materials
other_amount_payable
owner {
id
ownr_fn
@@ -702,7 +721,40 @@ export const GET_JOB_BY_PK = gql`
ownr_ph2
tax_number
}
labor_rate_desc
owner_owing
ownerid
ownr_addr1
ownr_addr2
ownr_ctry
ownr_city
ownr_co_nm
ownr_ea
ownr_fn
ownr_ln
ownr_ph1
ownr_ph2
ownr_st
ownr_zip
parts_tax_rates
payments {
amount
created_at
date
exportedat
id
jobid
memo
payer
paymentnum
transactionid
type
}
plate_no
plate_st
po_number
policy_no
production_vars
rate_ats
rate_la1
rate_la2
rate_la3
@@ -726,121 +778,64 @@ export const GET_JOB_BY_PK = gql`
rate_mapa
rate_mash
rate_matd
actual_in
federal_tax_rate
local_tax_rate
state_tax_rate
regie_number
referral_source
referral_source_extra
remove_from_ar
ro_number
scheduled_completion
scheduled_in
actual_completion
scheduled_delivery
actual_delivery
date_estimated
date_open
date_scheduled
date_invoiced
date_last_contacted
date_lost_sale
date_next_contact
date_towin
date_rentalresp
date_exported
date_repairstarted
date_void
scheduled_in
selling_dealer
servicing_dealer
selling_dealer_contact
servicing_dealer_contact
special_coverage_policy
state_tax_rate
status
owner_owing
tax_registration_number
class
category
deliverchecklist
voided
ca_bc_pvrt
ca_customer_gst
storage_payable
suspended
joblines(where: { removed: { _eq: false } }, order_by: { line_no: asc }) {
tax_lbr_rt
tax_levies_rt
tax_paint_mat_rt
tax_registration_number
tax_shop_mat_rt
tax_str_rt
tax_sub_rt
tax_tow_rt
towin
towing_payable
unit_number
updated_at
v_vin
v_model_yr
v_model_desc
v_make_desc
v_color
vehicleid
vehicle {
id
alt_partm
line_no
unq_seq
line_ind
line_desc
line_ref
part_type
oem_partno
alt_partno
db_price
act_price
part_qty
mod_lbr_ty
db_hrs
mod_lb_hrs
lbr_op
lbr_amt
op_code_desc
status
jobs {
clm_no
id
ro_number
status
}
notes
location
tax_part
db_ref
manual_line
prt_dsmk_p
prt_dsmk_m
ioucreated
convertedtolbr
ah_detail_line
critical
billlines(limit: 1, order_by: { bill: { date: desc } }) {
id
quantity
actual_cost
actual_price
joblineid
bill {
id
invoice_number
vendor {
id
name
}
}
}
}
payments {
id
jobid
amount
payer
paymentnum
created_at
transactionid
memo
date
type
exportedat
}
cccontracts {
id
status
start
scheduledreturn
agreementnumber
courtesycar {
id
make
model
year
plate
fleetnumber
}
}
cieca_ttl
csiinvites {
id
completedon
plate_no
plate_st
v_color
v_make_desc
v_model_desc
v_model_yr
v_paint_codes
v_vin
}
voided
}
}
`;
export const GET_JOB_RECONCILIATION_BY_PK = gql`
query GET_JOB_RECONCILIATION_BY_PK($id: uuid!) {
bills(where: { jobid: { _eq: $id } }) {
@@ -905,6 +900,7 @@ export const GET_JOB_RECONCILIATION_BY_PK = gql`
}
}
`;
export const QUERY_JOB_CARD_DETAILS = gql`
query QUERY_JOB_CARD_DETAILS($id: uuid!) {
jobs_by_pk(id: $id) {
@@ -2225,3 +2221,120 @@ export const GET_JOB_LINE_ORDERS = gql`
}
}
`;
export const UPDATE_REMOVE_FROM_AR = gql`
mutation UPDATE_REMOVE_FROM_AR($jobId: uuid!, $remove_from_ar: Boolean!) {
update_jobs_by_pk(
pk_columns: { id: $jobId }
_set: { remove_from_ar: $remove_from_ar }
) {
id
remove_from_ar
}
}
`;
export const UNVOID_JOB = gql`
mutation UNVOID_JOB(
$jobId: uuid!
$default_imported: String!
$currentUserEmail: String!
$text: String!
) {
update_jobs_by_pk(
pk_columns: { id: $jobId }
_set: { voided: false, status: $default_imported, date_void: null }
) {
id
date_void
voided
status
}
insert_notes(
objects: {
jobid: $jobId
audit: true
created_by: $currentUserEmail
text: $text
}
) {
returning {
id
}
}
}
`;
export const DELETE_INTAKE_CHECKLIST = gql`
mutation DELETE_INTAKE($jobId: uuid!) {
update_jobs_by_pk(
pk_columns: { id: $jobId }
_set: { intakechecklist: null }
) {
id
intakechecklist
}
}
`;
export const DELETE_DELIVERY_CHECKLIST = gql`
mutation DELETE_DELIVERY($jobId: uuid!) {
update_jobs_by_pk(
pk_columns: { id: $jobId }
_set: { deliverchecklist: null }
) {
id
deliverchecklist
}
}
`;
export const MARK_JOB_FOR_REEXPORT = gql`
mutation MARK_JOB_FOR_REEXPORT($jobId: uuid!, $default_invoiced: String!) {
update_jobs_by_pk(
pk_columns: { id: $jobId }
_set: { date_exported: null, status: $default_invoiced }
) {
id
date_exported
status
date_invoiced
}
}
`;
export const MARK_JOB_AS_EXPORTED = gql`
mutation MARK_JOB_AS_EXPORTED(
$jobId: uuid!
$date_exported: timestamptz!
$default_exported: String!
) {
update_jobs_by_pk(
pk_columns: { id: $jobId }
_set: { date_exported: $date_exported, status: $default_exported }
) {
id
date_exported
date_invoiced
status
}
}
`;
export const MARK_JOB_AS_UNINVOICED = gql`
mutation MARK_JOB_AS_UNINVOICED($jobId: uuid!, $default_delivered: String!) {
update_jobs_by_pk(
pk_columns: { id: $jobId }
_set: {
date_exported: null
date_invoiced: null
status: $default_delivered
}
) {
id
date_exported
date_invoiced
status
}
}
`;

View File

@@ -31,6 +31,14 @@ export const UPDATE_ASSOCIATION = gql`
}
`;
export const INSERT_EULA_ACCEPTANCE = gql`
mutation INSERT_EULA_ACCEPTANCE($eulaAcceptance:eula_acceptances_insert_input!) {
insert_eula_acceptances_one(object: $eulaAcceptance){
id
}
}
`;
export const UPSERT_USER = gql`
mutation UPSERT_USER($authEmail: String!, $authToken: String!) {
insert_users(

View File

@@ -7,16 +7,16 @@ import { useParams } from "react-router-dom";
import AlertComponent from "../../components/alert/alert.component";
import JobCalculateTotals from "../../components/job-calculate-totals/job-calculate-totals.component";
import ScoreboardAddButton from "../../components/job-scoreboard-add-button/job-scoreboard-add-button.component";
import JobsAdminStatus from "../../components/jobs-admin-change-status/jobs-admin-change.status.component";
import JobsAdminClass from "../../components/jobs-admin-class/jobs-admin-class.component";
import JobsAdminDatesChange from "../../components/jobs-admin-dates/jobs-admin-dates.component";
import JobsAdminDeleteIntake from "../../components/jobs-admin-delete-intake/jobs-admin-delete-intake.component";
import JobsAdminMarkReexport from "../../components/jobs-admin-mark-reexport/jobs-admin-mark-reexport.component";
import JobAdminOwnerReassociate from "../../components/jobs-admin-owner-reassociate/jobs-admin-owner-reassociate.component";
import JobsAdminRemoveAR from "../../components/jobs-admin-remove-ar/jobs-admin-remove-ar.component";
import JobsAdminUnvoid from "../../components/jobs-admin-unvoid/jobs-admin-unvoid.component";
import JobAdminVehicleReassociate from "../../components/jobs-admin-vehicle-reassociate/jobs-admin-vehicle-reassociate.component";
import LoadingSpinner from "../../components/loading-spinner/loading-spinner.component";
import JobsAdminStatus from "../../components/jobs-admin-change-status/jobs-admin-change.status.component";
import NotFound from "../../components/not-found/not-found.component";
import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component";
import { GET_JOB_BY_PK } from "../../graphql/jobs.queries";
@@ -104,6 +104,7 @@ export function JobsCloseContainer({ setBreadcrumbs, setSelectedHeader }) {
<JobsAdminMarkReexport job={data ? data.jobs_by_pk : {}} />
<JobsAdminUnvoid job={data ? data.jobs_by_pk : {}} />
<JobsAdminStatus job={data ? data.jobs_by_pk : {}} />
<JobsAdminRemoveAR job={data ? data.jobs_by_pk : {}} />
</Space>
</Card>
</Col>

View File

@@ -55,6 +55,7 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import UndefinedToNull from "../../utils/undefinedtonull";
import { DateTimeFormat } from "./../../utils/DateFormatter";
import JobLifecycleComponent from "../../components/job-lifecycle/job-lifecycle.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -305,6 +306,12 @@ export function JobsDetailPage({
label: t("menus.jobsdetail.labor"),
children: <JobsDetailLaborContainer job={job} jobId={job.id} />,
},
{
key: 'lifecycle',
icon: <BarsOutlined />,
label: t('menus.jobsdetail.lifecycle'),
children: <JobLifecycleComponent job={job} statuses={bodyshop.md_ro_statuses} />,
},
{
key: "dates",
icon: <CalendarFilled />,

View File

@@ -109,3 +109,13 @@ export const setAuthlevel = (authlevel) => ({
type: UserActionTypes.SET_AUTH_LEVEL,
payload: authlevel,
});
export const setCurrentEula = (eula) => ({
type: UserActionTypes.SET_CURRENT_EULA,
payload: eula,
});
export const acceptEula = () => ({
type: UserActionTypes.EULA_ACCEPTED,
});

View File

@@ -3,6 +3,7 @@ import UserActionTypes from "./user.types";
const INITIAL_STATE = {
currentUser: {
authorized: null,
eulaIsAccepted: false,
//language: "en-US"
},
bodyshop: null,
@@ -17,6 +18,7 @@ const INITIAL_STATE = {
loading: false,
},
authLevel: 0,
currentEula: null,
};
const userReducer = (state = INITIAL_STATE, action) => {
@@ -63,11 +65,19 @@ const userReducer = (state = INITIAL_STATE, action) => {
loading: false,
},
};
case UserActionTypes.EULA_ACCEPTED:
return {
...state,
currentUser:{...state.currentUser, eulaIsAccepted: true},
currentEula: null,
};
case UserActionTypes.SIGN_IN_SUCCESS:
const{ currentEula,...currentUser} = action.payload
return {
...state,
loginLoading: false,
currentUser: action.payload,
currentUser: currentUser,
currentEula,
error: null,
};
case UserActionTypes.SIGN_OUT_SUCCESS:

View File

@@ -43,6 +43,9 @@ import {
validatePasswordResetSuccess,
} from "./user.actions";
import UserActionTypes from "./user.types";
import client from "../../utils/GraphQLClient";
import {QUERY_EULA} from "../../graphql/bodyshop.queries";
import day from "../../utils/day";
const fpPromise = FingerprintJS.load();
@@ -73,6 +76,8 @@ export function* signInWithEmail({ payload: { email, password } }) {
export function* onCheckUserSession() {
yield takeLatest(UserActionTypes.CHECK_USER_SESSION, isUserAuthenticated);
}
export function* isUserAuthenticated() {
try {
logImEXEvent("redux_auth_check");
@@ -85,6 +90,15 @@ export function* isUserAuthenticated() {
LogRocket.identify(user.email);
const eulaQuery = yield client.query({
query: QUERY_EULA,
variables: {
now: day()
},
});
const eulaIsAccepted = eulaQuery.data.eulas.length > 0 && eulaQuery.data.eulas[0].eula_acceptances.length > 0;
yield put(
signInSuccess({
uid: user.uid,
@@ -92,6 +106,8 @@ export function* isUserAuthenticated() {
displayName: user.displayName,
photoURL: user.photoURL,
authorized: true,
eulaIsAccepted,
currentEula: eulaIsAccepted ? null : eulaQuery.data.eulas[0],
})
);
} catch (error) {

View File

@@ -36,3 +36,8 @@ export const selectLoginLoading = createSelector(
[selectUser],
(user) => user.loginLoading
);
export const selectCurrentEula = createSelector(
[selectUser],
(user) => user.currentEula
);

View File

@@ -32,5 +32,7 @@ const UserActionTypes = {
CHECK_ACTION_CODE_START: "CHECK_ACTION_CODE_START",
CHECK_ACTION_CODE_SUCCESS: "CHECK_ACTION_CODE_SUCCESS",
CHECK_ACTION_CODE_FAILURE: "CHECK_ACTION_CODE_FAILURE",
SET_CURRENT_EULA: "SET_CURRENT_EULA",
EULA_ACCEPTED : "EULA_ACCEPTED",
};
export default UserActionTypes;

View File

@@ -99,6 +99,7 @@
},
"audit_trail": {
"messages": {
"admin_job_remove_from_ar": "ADMIN: Remove from AR updated to: {{status}}",
"admin_jobmarkexported": "ADMIN: Job marked as exported.",
"admin_jobmarkforreexport": "ADMIN: Job marked for re-export.",
"admin_jobuninvoice": "ADMIN: Job has been uninvoiced.",
@@ -112,11 +113,11 @@
"jobassignmentchange": "Employee {{name}} assigned to {{operation}}",
"jobassignmentremoved": "Employee assignment removed for {{operation}}",
"jobchecklist": "Checklist type \"{{type}}\" completed. In production set to {{inproduction}}. Status set to {{status}}.",
"jobinvoiced": "Job has been invoiced.",
"jobconverted": "Job converted and assigned number {{ro_number}}.",
"jobfieldchanged": "Job field $t(jobs.fields.{{field}}) changed to {{value}}.",
"jobimported": "Job imported.",
"jobinproductionchange": "Job production status set to {{inproduction}}",
"jobinvoiced": "Job has been invoiced.",
"jobioucreated": "IOU Created.",
"jobmodifylbradj": "Labor adjustments modified {{mod_lbr_ty}} / {{hours}}.",
"jobnoteadded": "Note added to Job.",
@@ -254,7 +255,6 @@
"saving": "Error encountered while saving. {{message}}"
},
"fields": {
"ReceivableCustomField": "QBO Receivable Custom Field {{number}}",
"address1": "Address 1",
"address2": "Address 2",
"appt_alt_transport": "Appointment Alternative Transportation Options",
@@ -331,6 +331,9 @@
"md_ded_notes": "Deductible Notes",
"md_email_cc": "Auto Email CC: $t(printcenter.subjects.jobs.{{template}})",
"md_from_emails": "Additional From Emails",
"md_functionality_toggles": {
"parts_queue_toggle": "Auto Add Imported/Supplemented Jobs to Parts Queue"
},
"md_hour_split": {
"paint": "Paint Hour Split",
"prep": "Prep Hour Split"
@@ -353,9 +356,6 @@
},
"md_payment_types": "Payment Types",
"md_referral_sources": "Referral Sources",
"md_functionality_toggles": {
"parts_queue_toggle": "Auto Add Imported/Supplemented Jobs to Parts Queue"
},
"md_tasks_presets": {
"hourstype": "",
"memo": "",
@@ -473,6 +473,7 @@
"editaccess": "Users -> Edit access"
}
},
"ReceivableCustomField": "QBO Receivable Custom Field {{number}}",
"responsibilitycenter": "Responsibility Center",
"responsibilitycenter_accountdesc": "Account Description",
"responsibilitycenter_accountitem": "Item",
@@ -814,6 +815,10 @@
"usage": "Usage",
"vehicle": "Vehicle Description"
},
"readiness": {
"notready": "Not Ready",
"ready": "Ready"
},
"status": {
"in": "Available",
"inservice": "In Service",
@@ -823,10 +828,6 @@
},
"successes": {
"saved": "Courtesy Car saved successfully."
},
"readiness": {
"notready": "Not Ready",
"ready": "Ready"
}
},
"csi": {
@@ -933,6 +934,41 @@
"updated": "Document updated successfully. "
}
},
"eula": {
"titles": {
"modal": "Terms and Conditions",
"upper_card": "Acknowledgement"
},
"messages": {
"first_name": "Please enter your first name.",
"last_name": "Please enter your last name.",
"business_name": "Please enter your legal business name.",
"phone_number": "Please enter your phone number.",
"date_accepted": "Please enter Today's Date.",
"accepted_terms": "Please accept the terms and conditions of this agreement."
},
"buttons": {
"accept": "Accept EULA"
},
"labels": {
"first_name": "First Name",
"last_name": "Last Name",
"business_name": "Legal Business Name",
"phone_number": "Phone Number",
"address": "Address",
"date_accepted": "Date Accepted",
"accepted_terms": "I accept the terms and conditions of this agreement."
},
"content": {
"never_scrolled": "You must scroll to the bottom of the Terms and Conditions before accepting."
},
"errors": {
"acceptance": {
"message": "Eula Acceptance Error",
"description": "Something went wrong while accepting the EULA. Please try again."
}
}
},
"emails": {
"errors": {
"notsent": "Email not sent. Error encountered while sending {{message}}"
@@ -1213,6 +1249,31 @@
"updated": "Inventory line updated."
}
},
"job_lifecycle": {
"columns": {
"duration": "Duration",
"end": "End",
"relative_end": "Relative End",
"relative_start": "Relative Start",
"start": "Start",
"value": "Value"
},
"content": {
"current_status_accumulated_time": "Current Status Accumulated Time",
"data_unavailable": " There is currently no Lifecycle data for this Job.",
"legend_title": "Legend",
"loading": "Loading Job Timelines....",
"not_available": "N/A",
"previous_status_accumulated_time": "Previous Status Accumulated Time",
"title": "Job Lifecycle Component",
"title_durations": "Historical Status Durations",
"title_loading": "Loading",
"title_transitions": "Transitions"
},
"errors": {
"fetch": "Error getting Job Lifecycle Data"
}
},
"job_payments": {
"buttons": {
"goback": "Go Back",
@@ -1833,6 +1894,7 @@
},
"reconciliationheader": "Parts & Sublet Reconciliation",
"relatedros": "Related ROs",
"remove_from_ar": "Remove from AR",
"returntotals": "Return Totals",
"rosaletotal": "RO Parts Total",
"sale_additional": "Sales - Additional",
@@ -2012,6 +2074,7 @@
"general": "General",
"insurance": "Insurance Information",
"labor": "Labor",
"lifecycle": "Lifecycle",
"partssublet": "Parts & Bills",
"rates": "Rates",
"repairdata": "Repair Data",
@@ -2563,6 +2626,7 @@
},
"templates": {
"anticipated_revenue": "Anticipated Revenue",
"ar_aging": "AR Aging",
"attendance_detail": "Attendance (All Employees)",
"attendance_employee": "Employee Attendance",
"attendance_summary": "Attendance Summary (All Employees)",
@@ -2919,8 +2983,8 @@
"shop-templates": "Shop Templates | $t(titles.app)",
"shop_vendors": "Vendors | $t(titles.app)",
"techconsole": "Technician Console | $t(titles.app)",
"techjoblookup": "Technician Job Lookup | $t(titles.app)",
"techjobclock": "Technician Job Clock | $t(titles.app)",
"techjoblookup": "Technician Job Lookup | $t(titles.app)",
"techshiftclock": "Technician Shift Clock | $t(titles.app)",
"temporarydocs": "Temporary Documents | $t(titles.app)",
"timetickets": "Time Tickets | $t(titles.app)",

View File

@@ -99,6 +99,7 @@
},
"audit_trail": {
"messages": {
"admin_job_remove_from_ar": "",
"admin_jobmarkexported": "",
"admin_jobmarkforreexport": "",
"admin_jobuninvoice": "",
@@ -112,11 +113,11 @@
"jobassignmentchange": "",
"jobassignmentremoved": "",
"jobchecklist": "",
"jobinvoiced": "",
"jobconverted": "",
"jobfieldchanged": "",
"jobimported": "",
"jobinproductionchange": "",
"jobinvoiced": "",
"jobioucreated": "",
"jobmodifylbradj": "",
"jobnoteadded": "",
@@ -254,13 +255,9 @@
"saving": ""
},
"fields": {
"ReceivableCustomField": "",
"address1": "",
"address2": "",
"appt_alt_transport": "",
"md_functionality_toggles": {
"parts_queue_toggle": ""
},
"appt_colors": {
"color": "",
"label": ""
@@ -334,6 +331,9 @@
"md_ded_notes": "",
"md_email_cc": "",
"md_from_emails": "",
"md_functionality_toggles": {
"parts_queue_toggle": ""
},
"md_hour_split": {
"paint": "",
"prep": ""
@@ -473,6 +473,7 @@
"editaccess": ""
}
},
"ReceivableCustomField": "",
"responsibilitycenter": "",
"responsibilitycenter_accountdesc": "",
"responsibilitycenter_accountitem": "",
@@ -814,6 +815,10 @@
"usage": "",
"vehicle": ""
},
"readiness": {
"notready": "",
"ready": ""
},
"status": {
"in": "",
"inservice": "",
@@ -823,10 +828,6 @@
},
"successes": {
"saved": ""
},
"readiness": {
"notready": "",
"ready": ""
}
},
"csi": {
@@ -933,6 +934,41 @@
"updated": ""
}
},
"eula": {
"titles": {
"modal": "Terms and Conditions",
"upper_card": "Acknowledgement"
},
"messages": {
"first_name": "Please enter your first name.",
"last_name": "Please enter your last name.",
"business_name": "Please enter your legal business name.",
"phone_number": "Please enter your phone number.",
"date_accepted": "Please enter Today's Date.",
"accepted_terms": "Please accept the terms and conditions of this agreement."
},
"buttons": {
"accept": "Accept EULA"
},
"labels": {
"first_name": "First Name",
"last_name": "Last Name",
"business_name": "Legal Business Name",
"phone_number": "Phone Number",
"address": "Address",
"date_accepted": "Date Accepted",
"accepted_terms": "I accept the terms and conditions of this agreement."
},
"content": {
"never_scrolled": "You must scroll to the bottom of the Terms and Conditions before accepting."
},
"errors": {
"acceptance": {
"message": "Eula Acceptance Error",
"description": "Something went wrong while accepting the EULA. Please try again."
}
}
},
"emails": {
"errors": {
"notsent": "Correo electrónico no enviado Se encontró un error al enviar {{message}}"
@@ -1213,6 +1249,31 @@
"updated": ""
}
},
"job_lifecycle": {
"columns": {
"duration": "",
"end": "",
"relative_end": "",
"relative_start": "",
"start": "",
"value": ""
},
"content": {
"current_status_accumulated_time": "",
"data_unavailable": "",
"legend_title": "",
"loading": "",
"not_available": "",
"previous_status_accumulated_time": "",
"title": "",
"title_durations": "",
"title_loading": "",
"title_transitions": ""
},
"errors": {
"fetch": "Error al obtener los datos del ciclo de vida del trabajo"
}
},
"job_payments": {
"buttons": {
"goback": "",
@@ -1833,6 +1894,7 @@
},
"reconciliationheader": "",
"relatedros": "",
"remove_from_ar": "",
"returntotals": "",
"rosaletotal": "",
"sale_additional": "",
@@ -2012,6 +2074,7 @@
"general": "",
"insurance": "",
"labor": "Labor",
"lifecycle": "",
"partssublet": "Piezas / Subarrendamiento",
"rates": "",
"repairdata": "Datos de reparación",
@@ -2563,6 +2626,7 @@
},
"templates": {
"anticipated_revenue": "",
"ar_aging": "",
"attendance_detail": "",
"attendance_employee": "",
"attendance_summary": "",
@@ -2919,8 +2983,8 @@
"shop-templates": "",
"shop_vendors": "Vendedores | $t(titles.app)",
"techconsole": "$t(titles.app)",
"techjoblookup": "$t(titles.app)",
"techjobclock": "$t(titles.app)",
"techjoblookup": "$t(titles.app)",
"techshiftclock": "$t(titles.app)",
"temporarydocs": "",
"timetickets": "",

View File

@@ -99,6 +99,7 @@
},
"audit_trail": {
"messages": {
"admin_job_remove_from_ar": "",
"admin_jobmarkexported": "",
"admin_jobmarkforreexport": "",
"admin_jobuninvoice": "",
@@ -112,11 +113,11 @@
"jobassignmentchange": "",
"jobassignmentremoved": "",
"jobchecklist": "",
"jobinvoiced": "",
"jobconverted": "",
"jobfieldchanged": "",
"jobimported": "",
"jobinproductionchange": "",
"jobinvoiced": "",
"jobioucreated": "",
"jobmodifylbradj": "",
"jobnoteadded": "",
@@ -254,7 +255,6 @@
"saving": ""
},
"fields": {
"ReceivableCustomField": "",
"address1": "",
"address2": "",
"appt_alt_transport": "",
@@ -331,13 +331,13 @@
"md_ded_notes": "",
"md_email_cc": "",
"md_from_emails": "",
"md_functionality_toggles": {
"parts_queue_toggle": ""
},
"md_hour_split": {
"paint": "",
"prep": ""
},
"md_functionality_toggles": {
"parts_queue_toggle": ""
},
"md_ins_co": {
"city": "",
"name": "",
@@ -473,6 +473,7 @@
"editaccess": ""
}
},
"ReceivableCustomField": "",
"responsibilitycenter": "",
"responsibilitycenter_accountdesc": "",
"responsibilitycenter_accountitem": "",
@@ -814,6 +815,10 @@
"usage": "",
"vehicle": ""
},
"readiness": {
"notready": "",
"ready": ""
},
"status": {
"in": "",
"inservice": "",
@@ -823,10 +828,6 @@
},
"successes": {
"saved": ""
},
"readiness": {
"notready": "",
"ready": ""
}
},
"csi": {
@@ -933,6 +934,41 @@
"updated": ""
}
},
"eula": {
"titles": {
"modal": "Terms and Conditions",
"upper_card": "Acknowledgement"
},
"messages": {
"first_name": "Please enter your first name.",
"last_name": "Please enter your last name.",
"business_name": "Please enter your legal business name.",
"phone_number": "Please enter your phone number.",
"date_accepted": "Please enter Today's Date.",
"accepted_terms": "Please accept the terms and conditions of this agreement."
},
"buttons": {
"accept": "Accept EULA"
},
"labels": {
"first_name": "First Name",
"last_name": "Last Name",
"business_name": "Legal Business Name",
"phone_number": "Phone Number",
"address": "Address",
"date_accepted": "Date Accepted",
"accepted_terms": "I accept the terms and conditions of this agreement."
},
"content": {
"never_scrolled": "You must scroll to the bottom of the Terms and Conditions before accepting."
},
"errors": {
"acceptance": {
"message": "Eula Acceptance Error",
"description": "Something went wrong while accepting the EULA. Please try again."
}
}
},
"emails": {
"errors": {
"notsent": "Courriel non envoyé. Erreur rencontrée lors de l'envoi de {{message}}"
@@ -1213,6 +1249,31 @@
"updated": ""
}
},
"job_lifecycle": {
"columns": {
"duration": "",
"end": "",
"relative_end": "",
"relative_start": "",
"start": "",
"value": ""
},
"content": {
"current_status_accumulated_time": "",
"data_unavailable": "",
"legend_title": "",
"loading": "",
"not_available": "",
"previous_status_accumulated_time": "",
"title": "",
"title_durations": "",
"title_loading": "",
"title_transitions": ""
},
"errors": {
"fetch": "Erreur lors de l'obtention des données du cycle de vie des tâches"
}
},
"job_payments": {
"buttons": {
"goback": "",
@@ -1833,6 +1894,7 @@
},
"reconciliationheader": "",
"relatedros": "",
"remove_from_ar": "",
"returntotals": "",
"rosaletotal": "",
"sale_additional": "",
@@ -2012,6 +2074,7 @@
"general": "",
"insurance": "",
"labor": "La main d'oeuvre",
"lifecycle": "",
"partssublet": "Pièces / Sous-location",
"rates": "",
"repairdata": "Données de réparation",
@@ -2563,6 +2626,7 @@
},
"templates": {
"anticipated_revenue": "",
"ar_aging": "",
"attendance_detail": "",
"attendance_employee": "",
"attendance_summary": "",
@@ -2919,8 +2983,8 @@
"shop-templates": "",
"shop_vendors": "Vendeurs | $t(titles.app)",
"techconsole": "$t(titles.app)",
"techjoblookup": "$t(titles.app)",
"techjobclock": "$t(titles.app)",
"techjoblookup": "$t(titles.app)",
"techshiftclock": "$t(titles.app)",
"temporarydocs": "",
"timetickets": "",

View File

@@ -1,54 +1,56 @@
import i18n from "i18next";
const AuditTrailMapping = {
alertToggle: (status) => i18n.t("audit_trail.messages.alerttoggle", { status }),
admin_job_remove_from_ar: (status) =>
i18n.t("audit_trail.messages.admin_job_remove_from_ar", { status }),
admin_jobfieldchange: (field, value) =>
"ADMIN: " +
i18n.t("audit_trail.messages.jobfieldchanged", { field, value }),
admin_jobmarkexported: () =>
i18n.t("audit_trail.messages.admin_jobmarkexported"),
admin_jobmarkforreexport: () =>
i18n.t("audit_trail.messages.admin_jobmarkforreexport"),
admin_jobstatuschange: (status) =>
"ADMIN: " + i18n.t("audit_trail.messages.jobstatuschange", { status }),
admin_jobuninvoice: () => i18n.t("audit_trail.messages.admin_jobuninvoice"),
admin_jobunvoid: () => i18n.t("audit_trail.messages.admin_jobunvoid"),
alertToggle: (status) =>
i18n.t("audit_trail.messages.alerttoggle", { status }),
appointmentcancel: (lost_sale_reason) =>
i18n.t("audit_trail.messages.appointmentcancel", { lost_sale_reason }),
appointmentinsert: (start) =>
i18n.t("audit_trail.messages.appointmentinsert", { start }),
jobstatuschange: (status) =>
i18n.t("audit_trail.messages.jobstatuschange", { status }),
admin_jobstatuschange: (status) =>
"ADMIN: " + i18n.t("audit_trail.messages.jobstatuschange", { status }),
jobsupplement: () => i18n.t("audit_trail.messages.jobsupplement"),
jobimported: () => i18n.t("audit_trail.messages.jobimported"),
jobinvoiced: () =>
i18n.t("audit_trail.messages.jobinvoiced"),
jobconverted: (ro_number) =>
i18n.t("audit_trail.messages.jobconverted", { ro_number }),
jobfieldchange: (field, value) =>
i18n.t("audit_trail.messages.jobfieldchanged", { field, value }),
admin_jobfieldchange: (field, value) =>
"ADMIN: " +
i18n.t("audit_trail.messages.jobfieldchanged", { field, value }),
jobspartsorder: (order_number) =>
i18n.t("audit_trail.messages.jobspartsorder", { order_number }),
jobspartsreturn: (order_number) =>
i18n.t("audit_trail.messages.jobspartsreturn", { order_number }),
jobmodifylbradj: ({ mod_lbr_ty, hours }) =>
i18n.t("audit_trail.messages.jobmodifylbradj", { mod_lbr_ty, hours }),
billposted: (invoice_number) =>
i18n.t("audit_trail.messages.billposted", { invoice_number }),
billupdated: (invoice_number) =>
i18n.t("audit_trail.messages.billupdated", { invoice_number }),
failedpayment: () => i18n.t("audit_trail.messages.failedpayment"),
jobassignmentchange: (operation, name) =>
i18n.t("audit_trail.messages.jobassignmentchange", { operation, name }),
jobassignmentremoved: (operation) =>
i18n.t("audit_trail.messages.jobassignmentremoved", { operation }),
jobinproductionchange: (inproduction) =>
i18n.t("audit_trail.messages.jobinproductionchange", { inproduction }),
jobchecklist: (type, inproduction, status) =>
i18n.t("audit_trail.messages.jobchecklist", { type, inproduction, status }),
jobconverted: (ro_number) =>
i18n.t("audit_trail.messages.jobconverted", { ro_number }),
jobfieldchange: (field, value) =>
i18n.t("audit_trail.messages.jobfieldchanged", { field, value }),
jobimported: () => i18n.t("audit_trail.messages.jobimported"),
jobinproductionchange: (inproduction) =>
i18n.t("audit_trail.messages.jobinproductionchange", { inproduction }),
jobinvoiced: () => i18n.t("audit_trail.messages.jobinvoiced"),
jobmodifylbradj: ({ mod_lbr_ty, hours }) =>
i18n.t("audit_trail.messages.jobmodifylbradj", { mod_lbr_ty, hours }),
jobnoteadded: () => i18n.t("audit_trail.messages.jobnoteadded"),
jobnoteupdated: () => i18n.t("audit_trail.messages.jobnoteupdated"),
jobnotedeleted: () => i18n.t("audit_trail.messages.jobnotedeleted"),
admin_jobunvoid: () => i18n.t("audit_trail.messages.admin_jobunvoid"),
admin_jobuninvoice: () => i18n.t("audit_trail.messages.admin_jobuninvoice"),
admin_jobmarkforreexport: () =>
i18n.t("audit_trail.messages.admin_jobmarkforreexport"),
admin_jobmarkexported: () =>
i18n.t("audit_trail.messages.admin_jobmarkexported"),
failedpayment: () => i18n.t("audit_trail.messages.failedpayment"),
jobnoteupdated: () => i18n.t("audit_trail.messages.jobnoteupdated"),
jobspartsorder: (order_number) =>
i18n.t("audit_trail.messages.jobspartsorder", { order_number }),
jobspartsreturn: (order_number) =>
i18n.t("audit_trail.messages.jobspartsreturn", { order_number }),
jobstatuschange: (status) =>
i18n.t("audit_trail.messages.jobstatuschange", { status }),
jobsupplement: () => i18n.t("audit_trail.messages.jobsupplement"),
};
export default AuditTrailMapping;

View File

@@ -17,6 +17,9 @@ export function DateTimeFormatter(props) {
)
: null;
}
export function DateTimeFormatterFunction(date) {
return dayjs(date).format("MM/DD/YYYY hh:mm a");
}
export function TimeFormatter(props) {
return props.children
? dayjs(props.children).format(props.format ? props.format : "hh:mm a")

View File

@@ -2039,6 +2039,15 @@ export const TemplateList = (type, context) => {
},
group: "jobs",
},
ar_aging: {
title: i18n.t("reportcenter.templates.ar_aging"),
subject: i18n.t("reportcenter.templates.ar_aging"),
key: "ar_aging",
//idtype: "vendor",
disabled: false,
datedisable: true,
group: "customers",
},
}
: {}),
...(!type || type === "courtesycarcontract"

View File

@@ -32,11 +32,6 @@ import objectSupport from 'dayjs/plugin/objectSupport';
import toArray from 'dayjs/plugin/toArray';
import toObject from 'dayjs/plugin/toObject';
// import badMutable from 'dayjs/plugin/badMutable';
// import preParsePostFormat from 'dayjs/plugin/preParsePostFormat';
// dayjs.extend(badMutable); // TODO: Client Update - This is not advised, scoreboard page
dayjs.extend(toObject);
dayjs.extend(toArray);
dayjs.extend(objectSupport);
@@ -46,7 +41,6 @@ dayjs.extend(isToday);
dayjs.extend(localeData);
dayjs.extend(quarterOfYear);
dayjs.extend(localizedFormat);
// dayjs.extend(preParsePostFormat); // TODO: This should not be needed
dayjs.extend(isLeapYear);
dayjs.extend(isoWeeksInYear);
dayjs.extend(isoWeek);

View File

@@ -0,0 +1,16 @@
const fs = require('fs');
const filename = process.argv[2];
fs.readFile(filename, 'utf8', (err, data) => {
if (err) {
console.error(`Error reading file ${filename}:`, err);
return;
}
const filteredData = JSON.stringify(data);
console.log('Select the content between the quotes below and paste it into the EULA Content field in the EULA Content table in the database.')
console.log('--------------------------------------------------')
console.log(filteredData);
console.log('--------------------------------------------------')
});

View File

@@ -2442,7 +2442,7 @@
_eq: X-Hasura-User-Id
columns:
- address
- buisness_name
- business_name
- date_accepted
- eulaid
- first_name
@@ -2454,7 +2454,7 @@
permission:
columns:
- address
- buisness_name
- business_name
- first_name
- last_name
- phone_number
@@ -2676,6 +2676,31 @@
- table:
name: job_ar_schema
schema: public
object_relationships:
- name: job
using:
foreign_key_constraint_on: id
select_permissions:
- role: user
permission:
columns:
- id
- ro_number
- clm_total
- total_payments
- balance
- date_invoiced
- shopid
filter:
job:
bodyshop:
associations:
_and:
- active:
_eq: true
- user:
authid:
_eq: X-Hasura-User-Id
- table:
name: job_conversations
schema: public
@@ -4164,6 +4189,11 @@
- name: job_status_transition
definition:
enable_manual: true
insert:
columns: '*'
update:
columns:
- status
retry_conf:
interval_sec: 10
num_retries: 0

View File

@@ -0,0 +1 @@
alter table "public"."eula_acceptances" rename column "business_name" to "buisness_name";

View File

@@ -0,0 +1 @@
alter table "public"."eula_acceptances" rename column "buisness_name" to "business_name";

View File

@@ -0,0 +1 @@
alter table "public"."eula_acceptances" alter column "phone_number" set not null;

View File

@@ -0,0 +1 @@
alter table "public"."eula_acceptances" alter column "phone_number" drop not null;

View File

@@ -0,0 +1 @@
alter table "public"."eula_acceptances" alter column "address" set not null;

View File

@@ -0,0 +1 @@
alter table "public"."eula_acceptances" alter column "address" drop not 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"."job_ar_schema" add column "shopid" uuid
-- null;

View File

@@ -0,0 +1,2 @@
alter table "public"."job_ar_schema" add column "shopid" uuid
null;

View File

@@ -0,0 +1 @@
alter table "public"."job_ar_schema" drop constraint "job_ar_schema_id_fkey";

View File

@@ -0,0 +1,5 @@
alter table "public"."job_ar_schema"
add constraint "job_ar_schema_id_fkey"
foreign key ("id")
references "public"."jobs"
("id") on update restrict on delete restrict;

View File

@@ -0,0 +1,35 @@
-- Could not auto-generate a down migration.
-- Please write an appropriate down migration for the SQL below:
-- CREATE OR REPLACE FUNCTION public.jobs_ar_summary ()
-- RETURNS SETOF job_ar_schema
-- LANGUAGE plpgsql
-- STABLE
-- AS $function$
-- BEGIN
--
-- RETURN query
-- select
-- j.id,
-- j.ro_number,
-- j.clm_total,
-- coalesce (p.total_payments,0) as total_payments,
-- j.clm_total - coalesce (p.total_payments,0) as balance,
-- j.date_invoiced,
-- j.shopid
-- from
-- jobs j
-- left join (
-- select
-- p.jobid,
-- coalesce (sum(p.amount),0) as total_payments
-- from
-- payments p
-- group by
-- p.jobid
-- ) p on
-- j.id = p.jobid
-- where j.remove_from_ar = false and j.date_invoiced is not null and j.clm_total - coalesce (p.total_payments,0) > 0;
--
--
-- END
-- $function$;

View File

@@ -0,0 +1,33 @@
CREATE OR REPLACE FUNCTION public.jobs_ar_summary ()
RETURNS SETOF job_ar_schema
LANGUAGE plpgsql
STABLE
AS $function$
BEGIN
RETURN query
select
j.id,
j.ro_number,
j.clm_total,
coalesce (p.total_payments,0) as total_payments,
j.clm_total - coalesce (p.total_payments,0) as balance,
j.date_invoiced,
j.shopid
from
jobs j
left join (
select
p.jobid,
coalesce (sum(p.amount),0) as total_payments
from
payments p
group by
p.jobid
) p on
j.id = p.jobid
where j.remove_from_ar = false and j.date_invoiced is not null and j.clm_total - coalesce (p.total_payments,0) > 0;
END
$function$;

View File

@@ -0,0 +1,35 @@
-- Could not auto-generate a down migration.
-- Please write an appropriate down migration for the SQL below:
-- CREATE OR REPLACE FUNCTION public.jobs_ar_summary ()
-- RETURNS SETOF job_ar_schema
-- LANGUAGE plpgsql
-- STABLE
-- AS $function$
-- BEGIN
--
-- RETURN query
-- select
-- j.id,
-- j.ro_number,
-- j.clm_total,
-- coalesce (p.total_payments,0) as total_payments,
-- j.clm_total - coalesce (p.total_payments,0) as balance,
-- j.date_invoiced,
-- j.shopid
-- from
-- jobs j
-- left join (
-- select
-- p.jobid,
-- coalesce (sum(p.amount),0) as total_payments
-- from
-- payments p
-- group by
-- p.jobid
-- ) p on
-- j.id = p.jobid
-- where j.remove_from_ar = false and j.date_invoiced is not null and j.clm_total - coalesce (p.total_payments,0) != 0;
--
--
-- END
-- $function$;

View File

@@ -0,0 +1,33 @@
CREATE OR REPLACE FUNCTION public.jobs_ar_summary ()
RETURNS SETOF job_ar_schema
LANGUAGE plpgsql
STABLE
AS $function$
BEGIN
RETURN query
select
j.id,
j.ro_number,
j.clm_total,
coalesce (p.total_payments,0) as total_payments,
j.clm_total - coalesce (p.total_payments,0) as balance,
j.date_invoiced,
j.shopid
from
jobs j
left join (
select
p.jobid,
coalesce (sum(p.amount),0) as total_payments
from
payments p
group by
p.jobid
) p on
j.id = p.jobid
where j.remove_from_ar = false and j.date_invoiced is not null and j.clm_total - coalesce (p.total_payments,0) != 0;
END
$function$;

View File

@@ -0,0 +1,35 @@
-- Could not auto-generate a down migration.
-- Please write an appropriate down migration for the SQL below:
-- CREATE OR REPLACE FUNCTION public.jobs_ar_summary ()
-- RETURNS SETOF job_ar_schema
-- LANGUAGE plpgsql
-- STABLE
-- AS $function$
-- BEGIN
--
-- RETURN query
-- select
-- j.id,
-- j.ro_number,
-- j.clm_total,
-- coalesce (p.total_payments,0) as total_payments,
-- j.clm_total - coalesce (p.total_payments,0) as balance,
-- j.date_invoiced,
-- j.shopid
-- from
-- jobs j
-- left join (
-- select
-- p.jobid,
-- coalesce (sum(p.amount),0) as total_payments
-- from
-- payments p
-- group by
-- p.jobid
-- ) p on
-- j.id = p.jobid
-- where j.remove_from_ar = false and j.date_invoiced is not null and j.clm_total - coalesce (p.total_payments,0) != 0;
--
--
-- END
-- $function$;

View File

@@ -0,0 +1,33 @@
CREATE OR REPLACE FUNCTION public.jobs_ar_summary ()
RETURNS SETOF job_ar_schema
LANGUAGE plpgsql
STABLE
AS $function$
BEGIN
RETURN query
select
j.id,
j.ro_number,
j.clm_total,
coalesce (p.total_payments,0) as total_payments,
j.clm_total - coalesce (p.total_payments,0) as balance,
j.date_invoiced,
j.shopid
from
jobs j
left join (
select
p.jobid,
coalesce (sum(p.amount),0) as total_payments
from
payments p
group by
p.jobid
) p on
j.id = p.jobid
where j.remove_from_ar = false and j.date_invoiced is not null and j.clm_total - coalesce (p.total_payments,0) != 0;
END
$function$;

File diff suppressed because it is too large Load Diff

1599
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -8,6 +8,7 @@
},
"scripts": {
"setup": "rm -rf node_modules && npm i && cd client && rm -rf node_modules && npm i",
"setup:win": "rimraf node_modules && npm i && cd client && rimraf node_modules && npm i",
"admin": "cd admin && npm start",
"client": "cd client && npm start",
"server": "nodemon server.js",
@@ -47,8 +48,9 @@
"node-mailjet": "^6.0.5",
"node-persist": "^4.0.1",
"node-quickbooks": "^2.0.43",
"nodemailer": "^6.9.8",
"phone": "^3.1.42",
"nodemailer": "^6.9.7",
"phone": "^3.1.41",
"rimraf": "^5.0.5",
"soap": "^1.0.0",
"socket.io": "^4.7.4",
"ssh2-sftp-client": "^9.1.0",

343
server.js
View File

@@ -1,299 +1,94 @@
// Import core modules
const express = require("express");
const cors = require("cors");
const bodyParser = require("body-parser");
const path = require("path");
const compression = require("compression");
const twilio = require("twilio");
const logger = require("./server/utils/logger");
const fb = require("./server/firebase/firebase-handler");
const cookieParser = require("cookie-parser");
const multer = require("multer");
const upload = multer();
//var enforce = require("express-sslify");
const http = require("http");
const {Server} = require("socket.io");
// Load environment variables
require("dotenv").config({
path: path.resolve(
process.cwd(),
`.env.${process.env.NODE_ENV || "development"}`
),
path: path.resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`)
});
// Import custom utilities and handlers
const logger = require("./server/utils/logger");
// Express app and server setup
const app = express();
const port = process.env.PORT || 5000;
//const port = 5000;
const http = require("http");
const server = http.createServer(app);
const { Server } = require("socket.io");
const io = new Server(server, {
path: "/ws",
cors: {
origin: [
"https://test.imex.online",
"https://www.test.imex.online",
"http://localhost:3000",
"https://imex.online",
"https://www.imex.online",
"https://beta.test.imex.online",
"https://www.beta.test.imex.online",
"https://beta.imex.online",
"https://www.beta.imex.online",
],
methods: ["GET", "POST"],
credentials: true,
exposedHeaders: ["set-cookie"],
},
path: "/ws",
cors: {
origin: [
"https://test.imex.online",
"https://www.test.imex.online",
"http://localhost:3000",
"https://imex.online",
"https://www.imex.online",
"https://beta.test.imex.online",
"https://www.beta.test.imex.online",
"https://beta.imex.online",
"https://www.beta.imex.online",
],
methods: ["GET", "POST"],
credentials: true,
exposedHeaders: ["set-cookie"],
},
});
exports.io = io;
require("./server/web-sockets/web-socket");
// app.set('trust proxy', true)
// app.use(fb.validateFirebaseIdToken);
// Middleware
app.use(compression());
app.use(cookieParser());
app.use(bodyParser.json({ limit: "50mb" }));
app.use(bodyParser.urlencoded({ limit: "50mb", extended: true }));
// app.use(enforce.HTTPS({ trustProtoHeader: true }));
app.use(
cors({ credentials: true, exposedHeaders: ["set-cookie"] })
// cors({
// credentials: true,
// origin: [
// "https://test.imex.online",
// "http://localhost:3000",
// "https://imex.online",
// ],
// })
);
app.use(bodyParser.json({limit: "50mb"}));
app.use(bodyParser.urlencoded({limit: "50mb", extended: true}));
app.use(cors({credentials: true, exposedHeaders: ["set-cookie"]}));
//Email Based Paths.
var sendEmail = require("./server/email/sendemail.js");
app.post("/sendemail", fb.validateFirebaseIdToken, sendEmail.sendEmail);
app.post("/emailbounce", bodyParser.text(), sendEmail.emailBounce);
//Test route to ensure Express is responding.
app.get("/test", async function (req, res) {
const commit = require("child_process").execSync(
"git rev-parse --short HEAD"
);
// console.log(app.get('trust proxy'));
// console.log("remoteAddress", req.socket.remoteAddress);
// console.log("X-Forwarded-For", req.header('x-forwarded-for'));
logger.log("test-api-status", "DEBUG", "api", { commit });
// sendEmail.sendServerEmail({
// subject: `API Check - ${process.env.NODE_ENV}`,
// text: `Server API check has come in. Remote IP: ${req.socket.remoteAddress}, X-Forwarded-For: ${req.header('x-forwarded-for')}`,
// });
sendEmail.sendServerEmail({
subject: `API Check - ${process.env.NODE_ENV}`,
text: `Server API check has come in.`,
});
res.status(200).send(`OK - ${commit}`);
// Helper middleware
app.use((req, res, next) => {
req.logger = logger;
next();
});
//Accounting Qbxml
const accountQbxml = require("./server/accounting/qbxml/qbxml");
app.post(
"/accounting/qbxml/receivables",
fb.validateFirebaseIdToken,
accountQbxml.receivables
);
app.post(
"/accounting/qbxml/payables",
fb.validateFirebaseIdToken,
accountQbxml.payables
);
app.post(
"/accounting/qbxml/payments",
fb.validateFirebaseIdToken,
accountQbxml.payments
);
// Route groupings
app.use('/', require("./server/routes/miscellaneousRoutes"));
app.use("/notifications", require("./server/routes/notificationsRoutes"));
app.use("/render", require("./server/routes/renderRoutes"));
app.use('/mixdata', require("./server/routes/mixDataRoutes"));
app.use('/accounting', require("./server/routes/accountingRoutes"));
app.use('/qbo', require("./server/routes/qboRoutes"));
app.use('/media', require("./server/routes/mediaRoutes"));
app.use('/sms', require("./server/routes/smsRoutes"));
app.use('/job', require("./server/routes/jobRoutes"));
app.use('/scheduling', require("./server/routes/schedulingRoutes"));
app.use('/utils', require("./server/routes/utilRoutes"));
app.use('/data', require("./server/routes/dataRoutes"));
app.use('/adm', require("./server/routes/adminRoutes"));
app.use('/tech', require("./server/routes/techRoutes"));
app.use('/intellipay', require("./server/routes/intellipayRoutes"));
app.use('/cdk', require("./server/routes/cdkRoutes"));
//Cloudinary Media Paths
var media = require("./server/media/media");
app.post(
"/media/sign",
fb.validateFirebaseIdToken,
media.createSignedUploadURL
);
app.post("/media/download", fb.validateFirebaseIdToken, media.downloadFiles);
app.post("/media/rename", fb.validateFirebaseIdToken, media.renameKeys);
app.post("/media/delete", fb.validateFirebaseIdToken, media.deleteFiles);
//SMS/Twilio Paths
var smsReceive = require("./server/sms/receive");
app.post(
"/sms/receive",
twilio.webhook({ validate: process.env.NODE_ENV === "PRODUCTION" }),
smsReceive.receive
);
var smsSend = require("./server/sms/send");
app.post("/sms/send", fb.validateFirebaseIdToken, smsSend.send);
var smsStatus = require("./server/sms/status");
app.post(
"/sms/status",
twilio.webhook({ validate: process.env.NODE_ENV === "PRODUCTION" }),
smsStatus.status
);
app.post(
"/sms/markConversationRead",
fb.validateFirebaseIdToken,
smsStatus.markConversationRead
);
var job = require("./server/job/job");
app.post("/job/totals", fb.validateFirebaseIdToken, job.totals);
app.post(
"/job/statustransition",
// fb.validateFirebaseIdToken,
job.statustransition
);
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);
//Handlebars Paths for Email/Report Rendering
// var renderHandlebars = require("./server/render/renderHandlebars");
// app.post("/render", fb.validateFirebaseIdToken, renderHandlebars.render);
var inlineCss = require("./server/render/inlinecss");
app.post("/render/inlinecss", fb.validateFirebaseIdToken, inlineCss.inlinecss);
// app.post(
// "/notifications/send",
// fb.sendNotification
// );
app.post("/notifications/subscribe", fb.validateFirebaseIdToken, fb.subscribe);
app.post(
"/notifications/unsubscribe",
fb.validateFirebaseIdToken,
fb.unsubscribe
);
app.post("/adm/updateuser", fb.validateFirebaseIdToken, fb.updateUser);
app.post("/adm/getuser", fb.validateFirebaseIdToken, fb.getUser);
app.post("/adm/createuser", fb.validateFirebaseIdToken, fb.createUser);
const adm = require("./server/admin/adminops");
app.post(
"/adm/createassociation",
fb.validateFirebaseIdToken,
fb.validateAdmin,
adm.createAssociation
);
app.post(
"/adm/createshop",
fb.validateFirebaseIdToken,
fb.validateAdmin,
adm.createShop
);
app.post(
"/adm/updateshop",
fb.validateFirebaseIdToken,
fb.validateAdmin,
adm.updateShop
);
app.post(
"/adm/updatecounter",
fb.validateFirebaseIdToken,
fb.validateAdmin,
adm.updateCounter
);
//Stripe Processing
// var stripe = require("./server/stripe/payment");
// app.post("/stripe/payment", fb.validateFirebaseIdToken, stripe.payment);
// app.post(
// "/stripe/mobilepayment",
// fb.validateFirebaseIdToken,
// stripe.mobile_payment
// );
//Tech Console
var tech = require("./server/tech/tech");
app.post("/tech/login", fb.validateFirebaseIdToken, tech.techLogin);
var utils = require("./server/utils/utils");
app.post("/utils/time", utils.servertime);
app.post("/utils/jsr", fb.validateFirebaseIdToken, utils.jsrAuth);
var qbo = require("./server/accounting/qbo/qbo");
app.post("/qbo/authorize", fb.validateFirebaseIdToken, qbo.authorize);
app.get("/qbo/callback", qbo.callback);
app.post("/qbo/receivables", fb.validateFirebaseIdToken, qbo.receivables);
app.post("/qbo/payables", fb.validateFirebaseIdToken, qbo.payables);
app.post("/qbo/payments", fb.validateFirebaseIdToken, qbo.payments);
var data = require("./server/data/data");
app.post("/data/ah", data.autohouse);
app.post("/data/cc", data.claimscorp);
app.post("/data/kaizen", data.kaizen);
app.post("/record-handler/arms", data.arms);
var taskHandler = require("./server/tasks/tasks");
app.post("/taskHandler", fb.validateFirebaseIdToken, taskHandler.taskHandler);
var mixdataUpload = require("./server/mixdata/mixdata");
app.post(
"/mixdata/upload",
fb.validateFirebaseIdToken,
upload.any(),
mixdataUpload.mixdataUpload
);
var intellipay = require("./server/intellipay/intellipay");
app.post(
"/intellipay/lightbox_credentials",
fb.validateFirebaseIdToken,
intellipay.lightbox_credentials
);
app.post(
"/intellipay/payment_refund",
fb.validateFirebaseIdToken,
intellipay.payment_refund
);
app.post(
"/intellipay/generate_payment_url",
fb.validateFirebaseIdToken,
intellipay.generate_payment_url
);
app.post(
"/intellipay/postback",
// fb.validateFirebaseIdToken,
intellipay.postback
);
var ioevent = require("./server/ioevent/ioevent");
app.post("/ioevent", ioevent.default);
// app.post("/newlog", (req, res) => {
// const { message, type, user, record, object } = req.body;
// logger.log(message, type, user, record, object);
// });
var os = require("./server/opensearch/os-handler");
app.post(
"/opensearch", //fb.validateFirebaseIdToken,
os.handler
);
app.post("/search", fb.validateFirebaseIdToken, os.search);
var cdkGetMake = require("./server/cdk/cdk-get-makes");
app.post("/cdk/getvehicles", fb.validateFirebaseIdToken, cdkGetMake.default);
app.get("/", async function (req, res) {
res.status(200).send("Access Forbidden.");
// Default route for forbidden access
app.get("/", (req, res) => {
res.status(200).send("Access Forbidden.");
});
server.listen(port, (error) => {
if (error) throw error;
logger.log(
`[${process.env.NODE_ENV || "DEVELOPMENT"}] Server running on port ${port}`,
"INFO",
"api"
);
});
const main = async () => {
await server.listen(port);
}
// Start server
main()
.then(() => {
logger.log(`[${process.env.NODE_ENV || "DEVELOPMENT"}] Server started on port ${port}`, "INFO", "api");
})
.catch((error) => {
logger.log(`[${process.env.NODE_ENV || "DEVELOPMENT"}] Server failed to start on port ${port}`, "ERROR", "api", error);
});

View File

@@ -166,7 +166,7 @@ async function CheckForErrors(socket, response) {
CdkBase.createLogEvent(
socket,
"DEBUG",
`Succesful response from DMS. ${response.Message || ""}`
`Successful response from DMS. ${response.Message || ""}`
);
} else {
CdkBase.createLogEvent(

View File

@@ -18,10 +18,10 @@ const {
} = require("./qbo-callback");
const OAuthClient = require("intuit-oauth");
const moment = require("moment-timezone");
const GraphQLClient = require("graphql-request").GraphQLClient;
const findTaxCode = require("../qb-receivables-lines").findTaxCode;
exports.default = async (req, res) => {
const oauthClient = new OAuthClient({
clientId: process.env.QBO_CLIENT_ID,
clientSecret: process.env.QBO_SECRET,
@@ -30,29 +30,31 @@ exports.default = async (req, res) => {
redirectUri: process.env.QBO_REDIRECT_URI,
logging: true,
});
try {
//Fetch the API Access Tokens & Set them for the session.
const response = await apiGqlClient.request(queries.GET_QBO_AUTH, {
email: req.user.email,
});
const { qbo_realmId } = response.associations[0];
oauthClient.setToken(response.associations[0].qbo_auth);
if (!qbo_realmId) {
res.status(401).json({ error: "No company associated." });
return;
}
await refreshOauthToken(oauthClient, req);
const BearerToken = req.headers.authorization;
const { bills: billsToQuery, elgen } = req.body;
//Query Job Info
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {
headers: {
Authorization: BearerToken,
},
});
const BearerToken = req.BearerToken;
const client = req.userGraphQLClient;
logger.log("qbo-payable-create", "DEBUG", req.user.email, billsToQuery);
const result = await client
.setHeaders({ Authorization: BearerToken })
.request(queries.QUERY_BILLS_FOR_PAYABLES_EXPORT, {

View File

@@ -51,15 +51,13 @@ exports.default = async (req, res) => {
}
await refreshOauthToken(oauthClient, req);
const BearerToken = req.headers.authorization;
const { payments: paymentsToQuery, elgen } = req.body;
//Query Job Info
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {
headers: {
Authorization: BearerToken,
},
});
const BearerToken = req.BearerToken;
const client = req.userGraphQLClient;
logger.log("qbo-payment-create", "DEBUG", req.user.email, paymentsToQuery);
const result = await client
.setHeaders({ Authorization: BearerToken })
.request(queries.QUERY_PAYMENTS_FOR_EXPORT, {

View File

@@ -18,8 +18,6 @@ const {
const OAuthClient = require("intuit-oauth");
const CreateInvoiceLines = require("../qb-receivables-lines").default;
const moment = require("moment-timezone");
const GraphQLClient = require("graphql-request").GraphQLClient;
const { generateOwnerTier } = require("../qbxml/qbxml-utils");
const { createMultiQbPayerLines } = require("../qb-receivables-lines");
@@ -46,15 +44,14 @@ exports.default = async (req, res) => {
await refreshOauthToken(oauthClient, req);
const BearerToken = req.headers.authorization;
const { jobIds, elgen } = req.body;
//Query Job Info
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {
headers: {
Authorization: BearerToken,
},
});
const BearerToken = req.BearerToken;
const client = req.userGraphQLClient;
logger.log("qbo-receivable-create", "DEBUG", req.user.email, jobIds);
const result = await client
.setHeaders({ Authorization: BearerToken })
.request(queries.QUERY_JOBS_FOR_RECEIVABLES_EXPORT, {

View File

@@ -3,10 +3,11 @@ const path = require("path");
const DineroQbFormat = require("../accounting-constants").DineroQbFormat;
const queries = require("../../graphql-client/queries");
const Dinero = require("dinero.js");
var builder = require("xmlbuilder2");
const builder = require("xmlbuilder2");
const QbXmlUtils = require("./qbxml-utils");
const moment = require("moment-timezone");
const logger = require("../../utils/logger");
const logger = require('../../utils/logger');
require("dotenv").config({
path: path.resolve(
process.cwd(),
@@ -15,14 +16,10 @@ require("dotenv").config({
});
exports.default = async (req, res) => {
const BearerToken = req.headers.authorization;
const { bills: billsToQuery } = req.body;
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {
headers: {
Authorization: BearerToken,
},
});
const BearerToken = req.BearerToken;
const client = req.userGraphQLClient;
try {
logger.log(

View File

@@ -1,13 +1,12 @@
const GraphQLClient = require("graphql-request").GraphQLClient;
const path = require("path");
const DineroQbFormat = require("../accounting-constants").DineroQbFormat;
const queries = require("../../graphql-client/queries");
const Dinero = require("dinero.js");
var builder = require("xmlbuilder2");
const builder = require("xmlbuilder2");
const moment = require("moment-timezone");
const QbXmlUtils = require("./qbxml-utils");
const QbxmlReceivables = require("./qbxml-receivables");
const logger = require("../../utils/logger");
const logger = require('../../utils/logger');
require("dotenv").config({
path: path.resolve(
@@ -19,14 +18,10 @@ require("dotenv").config({
const { generateJobTier, generateOwnerTier, generateSourceTier } = QbXmlUtils;
exports.default = async (req, res) => {
const BearerToken = req.headers.authorization;
const { payments: paymentsToQuery } = req.body;
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {
headers: {
Authorization: BearerToken,
},
});
const BearerToken = req.BearerToken;
const client = req.userGraphQLClient;
try {
logger.log(

View File

@@ -1,13 +1,12 @@
const GraphQLClient = require("graphql-request").GraphQLClient;
const path = require("path");
const DineroQbFormat = require("../accounting-constants").DineroQbFormat;
const queries = require("../../graphql-client/queries");
const Dinero = require("dinero.js");
const moment = require("moment-timezone");
var builder = require("xmlbuilder2");
const builder = require("xmlbuilder2");
const QbXmlUtils = require("./qbxml-utils");
const logger = require("../../utils/logger");
const CreateInvoiceLines = require("../qb-receivables-lines").default;
const logger = require('../../utils/logger');
require("dotenv").config({
path: path.resolve(
@@ -20,14 +19,10 @@ Dinero.globalRoundingMode = "HALF_EVEN";
const { generateJobTier, generateOwnerTier, generateSourceTier } = QbXmlUtils;
exports.default = async (req, res) => {
const BearerToken = req.headers.authorization;
const { jobIds } = req.body;
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {
headers: {
Authorization: BearerToken,
},
});
const BearerToken = req.BearerToken;
const client = req.userGraphQLClient;
try {
logger.log(

View File

@@ -5,7 +5,6 @@ require("dotenv").config({
`.env.${process.env.NODE_ENV || "development"}`
),
});
const GraphQLClient = require("graphql-request").GraphQLClient;
const soap = require("soap");
const queries = require("../graphql-client/queries");
@@ -34,16 +33,11 @@ const { CDK_CREDENTIALS, CheckCdkResponseForError } = require("./cdk-wsdl");
exports.default = async function ReloadCdkMakes(req, res) {
const { bodyshopid, cdk_dealerid } = req.body;
try {
const BearerToken = req.headers.authorization;
//Query all CDK Models
const newList = await GetCdkMakes(req, cdk_dealerid);
//Clear out the existing records
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {
headers: {
Authorization: BearerToken,
},
});
const BearerToken = req.BearerToken;
const client = req.userGraphQLClient;
const deleteResult = await client
.setHeaders({ Authorization: BearerToken })

View File

@@ -1,269 +1,269 @@
const path = require("path");
require("dotenv").config({
path: path.resolve(
process.cwd(),
`.env.${process.env.NODE_ENV || "development"}`
),
path: path.resolve(
process.cwd(),
`.env.${process.env.NODE_ENV || "development"}`
),
});
const axios = require("axios");
let nodemailer = require("nodemailer");
let aws = require("@aws-sdk/client-ses");
let { defaultProvider } = require("@aws-sdk/credential-provider-node");
let {defaultProvider} = require("@aws-sdk/credential-provider-node");
const logger = require("../utils/logger");
const client = require("../graphql-client/graphql-client").client;
const queries = require("../graphql-client/queries");
const ses = new aws.SES({
// The key apiVersion is no longer supported in v3, and can be removed.
// @deprecated The client uses the "latest" apiVersion.
apiVersion: "latest",
region: "ca-central-1",
defaultProvider
// The key apiVersion is no longer supported in v3, and can be removed.
// @deprecated The client uses the "latest" apiVersion.
apiVersion: "latest",
region: "ca-central-1",
defaultProvider
});
let transporter = nodemailer.createTransport({
SES: { ses, aws },
SES: {ses, aws},
});
exports.sendServerEmail = async function ({ subject, text }) {
if (process.env.NODE_ENV === undefined) return;
try {
transporter.sendMail(
{
from: `ImEX Online API - ${process.env.NODE_ENV} <noreply@imex.online>`,
to: ["patrick@imexsystems.ca", "support@thinkimex.com"],
subject: subject,
text: text,
ses: {
// optional extra arguments for SendRawEmail
Tags: [
exports.sendServerEmail = async function ({subject, text}) {
if (process.env.NODE_ENV === undefined) return;
try {
transporter.sendMail(
{
Name: "tag_name",
Value: "tag_value",
from: `ImEX Online API - ${process.env.NODE_ENV} <noreply@imex.online>`,
to: ["patrick@imexsystems.ca", "support@thinkimex.com"],
subject: subject,
text: text,
ses: {
// optional extra arguments for SendRawEmail
Tags: [
{
Name: "tag_name",
Value: "tag_value",
},
],
},
},
],
},
},
(err, info) => {
console.log(err || info);
}
);
} catch (error) {
console.log(error);
logger.log("server-email-failure", "error", null, null, error);
}
(err, info) => {
console.log(err || info);
}
);
} catch (error) {
console.log(error);
logger.log("server-email-failure", "error", null, null, error);
}
};
exports.sendTaskEmail = async function ({ to, subject, text, attachments }) {
try {
transporter.sendMail(
{
from: `ImEX Online <noreply@imex.online>`,
to: to,
subject: subject,
text: text,
attachments: attachments || null,
},
(err, info) => {
console.log(err || info);
}
);
} catch (error) {
console.log(error);
logger.log("server-email-failure", "error", null, null, error);
}
exports.sendTaskEmail = async function ({to, subject, text, attachments}) {
try {
transporter.sendMail(
{
from: `ImEX Online <noreply@imex.online>`,
to: to,
subject: subject,
text: text,
attachments: attachments || null,
},
(err, info) => {
console.log(err || info);
}
);
} catch (error) {
console.log(error);
logger.log("server-email-failure", "error", null, null, error);
}
};
exports.sendEmail = async (req, res) => {
logger.log("send-email", "DEBUG", req.user.email, null, {
from: `${req.body.from.name} <${req.body.from.address}>`,
replyTo: req.body.ReplyTo.Email,
to: req.body.to,
cc: req.body.cc,
subject: req.body.subject,
});
logger.log("send-email", "DEBUG", req.user.email, null, {
from: `${req.body.from.name} <${req.body.from.address}>`,
replyTo: req.body.ReplyTo.Email,
to: req.body.to,
cc: req.body.cc,
subject: req.body.subject,
});
let downloadedMedia = [];
if (req.body.media && req.body.media.length > 0) {
downloadedMedia = await Promise.all(
req.body.media.map((m) => {
try {
return getImage(m);
} catch (error) {
logger.log("send-email-error", "ERROR", req.user.email, null, {
let downloadedMedia = [];
if (req.body.media && req.body.media.length > 0) {
downloadedMedia = await Promise.all(
req.body.media.map((m) => {
try {
return getImage(m);
} catch (error) {
logger.log("send-email-error", "ERROR", req.user.email, null, {
from: `${req.body.from.name} <${req.body.from.address}>`,
replyTo: req.body.ReplyTo.Email,
to: req.body.to,
cc: req.body.cc,
subject: req.body.subject,
error,
});
}
})
);
}
transporter.sendMail(
{
from: `${req.body.from.name} <${req.body.from.address}>`,
replyTo: req.body.ReplyTo.Email,
to: req.body.to,
cc: req.body.cc,
subject: req.body.subject,
error,
});
attachments:
[
...((req.body.attachments &&
req.body.attachments.map((a) => {
return {
filename: a.filename,
path: a.path,
};
})) ||
[]),
...downloadedMedia.map((a) => {
return {
path: a,
};
}),
] || null,
html: req.body.html,
ses: {
// optional extra arguments for SendRawEmail
Tags: [
{
Name: "tag_name",
Value: "tag_value",
},
],
},
},
(err, info) => {
console.log(err || info);
if (info) {
logger.log("send-email-success", "DEBUG", req.user.email, null, {
from: `${req.body.from.name} <${req.body.from.address}>`,
replyTo: req.body.ReplyTo.Email,
to: req.body.to,
cc: req.body.cc,
subject: req.body.subject,
// info,
});
logEmail(req, {
to: req.body.to,
cc: req.body.cc,
subject: req.body.subject,
messageId: info.response,
});
res.json({
success: true, //response: info
});
} else {
logger.log("send-email-failure", "ERROR", req.user.email, null, {
from: `${req.body.from.name} <${req.body.from.address}>`,
replyTo: req.body.ReplyTo.Email,
to: req.body.to,
cc: req.body.cc,
subject: req.body.subject,
error: err,
});
logEmail(req, {
to: req.body.to,
cc: req.body.cc,
subject: req.body.subject,
bodyshopid: req.body.bodyshopid,
});
res.status(500).json({success: false, error: err});
}
}
})
);
}
transporter.sendMail(
{
from: `${req.body.from.name} <${req.body.from.address}>`,
replyTo: req.body.ReplyTo.Email,
to: req.body.to,
cc: req.body.cc,
subject: req.body.subject,
attachments:
[
...((req.body.attachments &&
req.body.attachments.map((a) => {
return {
filename: a.filename,
path: a.path,
};
})) ||
[]),
...downloadedMedia.map((a) => {
return {
path: a,
};
}),
] || null,
html: req.body.html,
ses: {
// optional extra arguments for SendRawEmail
Tags: [
{
Name: "tag_name",
Value: "tag_value",
},
],
},
},
(err, info) => {
console.log(err || info);
if (info) {
logger.log("send-email-success", "DEBUG", req.user.email, null, {
from: `${req.body.from.name} <${req.body.from.address}>`,
replyTo: req.body.ReplyTo.Email,
to: req.body.to,
cc: req.body.cc,
subject: req.body.subject,
// info,
});
logEmail(req, {
to: req.body.to,
cc: req.body.cc,
subject: req.body.subject,
messageId: info.response,
});
res.json({
success: true, //response: info
});
} else {
logger.log("send-email-failure", "ERROR", req.user.email, null, {
from: `${req.body.from.name} <${req.body.from.address}>`,
replyTo: req.body.ReplyTo.Email,
to: req.body.to,
cc: req.body.cc,
subject: req.body.subject,
error: err,
});
logEmail(req, {
to: req.body.to,
cc: req.body.cc,
subject: req.body.subject,
bodyshopid: req.body.bodyshopid,
});
res.status(500).json({ success: false, error: err });
}
}
);
};
async function getImage(imageUrl) {
let image = await axios.get(imageUrl, { responseType: "arraybuffer" });
let raw = Buffer.from(image.data).toString("base64");
return "data:" + image.headers["content-type"] + ";base64," + raw;
let image = await axios.get(imageUrl, {responseType: "arraybuffer"});
let raw = Buffer.from(image.data).toString("base64");
return "data:" + image.headers["content-type"] + ";base64," + raw;
}
async function logEmail(req, email) {
try {
const insertresult = await client.request(queries.INSERT_EMAIL_AUDIT, {
email: {
to: email.to,
cc: email.cc,
subject: email.subject,
bodyshopid: req.body.bodyshopid,
useremail: req.user.email,
contents: req.body.html,
jobid: req.body.jobid,
sesmessageid: email.messageId,
status: "Sent",
},
});
console.log(insertresult);
} catch (error) {
logger.log("email-log-error", "error", req.user.email, null, {
from: `${req.body.from.name} <${req.body.from.address}>`,
to: req.body.to,
cc: req.body.cc,
subject: req.body.subject,
// info,
});
}
try {
const insertresult = await client.request(queries.INSERT_EMAIL_AUDIT, {
email: {
to: email.to,
cc: email.cc,
subject: email.subject,
bodyshopid: req.body.bodyshopid,
useremail: req.user.email,
contents: req.body.html,
jobid: req.body.jobid,
sesmessageid: email.messageId,
status: "Sent",
},
});
console.log(insertresult);
} catch (error) {
logger.log("email-log-error", "error", req.user.email, null, {
from: `${req.body.from.name} <${req.body.from.address}>`,
to: req.body.to,
cc: req.body.cc,
subject: req.body.subject,
// info,
});
}
}
exports.emailBounce = async function (req, res, next) {
try {
const body = JSON.parse(req.body);
if (body.Type === "SubscriptionConfirmation") {
logger.log("SNS-message", "DEBUG", "api", null, {
body: req.body,
});
}
const message = JSON.parse(body.Message);
if (message.notificationType === "Bounce") {
let replyTo, subject, messageId;
message.mail.headers.forEach((header) => {
if (header.name === "Reply-To") {
replyTo = header.value;
} else if (header.name === "Subject") {
subject = header.value;
exports.emailBounce = async function (req, res) {
try {
const body = JSON.parse(req.body);
if (body.Type === "SubscriptionConfirmation") {
logger.log("SNS-message", "DEBUG", "api", null, {
body: req.body,
});
}
});
messageId = message.mail.messageId;
if (replyTo === "noreply@imex.online") {
res.sendStatus(200);
return;
}
//If it's bounced, log it as bounced in audit log. Send an email to the user.
const result = await client.request(queries.UPDATE_EMAIL_AUDIT, {
sesid: messageId,
status: "Bounced",
context: message.bounce?.bouncedRecipients,
});
transporter.sendMail(
{
from: `ImEX Online <noreply@imex.online>`,
to: replyTo,
//bcc: "patrick@snapt.ca",
subject: `ImEX Online Bounced Email - RE: ${subject}`,
text: `ImEX Online has tried to deliver an email with the subject: ${subject} to the intended recipients but encountered an error.
const message = JSON.parse(body.Message);
if (message.notificationType === "Bounce") {
let replyTo, subject, messageId;
message.mail.headers.forEach((header) => {
if (header.name === "Reply-To") {
replyTo = header.value;
} else if (header.name === "Subject") {
subject = header.value;
}
});
messageId = message.mail.messageId;
if (replyTo === "noreply@imex.online") {
res.sendStatus(200);
return;
}
//If it's bounced, log it as bounced in audit log. Send an email to the user.
const result = await client.request(queries.UPDATE_EMAIL_AUDIT, {
sesid: messageId,
status: "Bounced",
context: message.bounce?.bouncedRecipients,
});
transporter.sendMail(
{
from: `ImEX Online <noreply@imex.online>`,
to: replyTo,
//bcc: "patrick@snapt.ca",
subject: `ImEX Online Bounced Email - RE: ${subject}`,
text: `ImEX Online has tried to deliver an email with the subject: ${subject} to the intended recipients but encountered an error.
${body.bounce?.bouncedRecipients.map(
(r) =>
`Recipient: ${r.emailAddress} | Status: ${r.action} | Code: ${r.diagnosticCode}
(r) =>
`Recipient: ${r.emailAddress} | Status: ${r.action} | Code: ${r.diagnosticCode}
`
)}
)}
`,
},
(err, info) => {
console.log("***", err || info);
},
(err, info) => {
console.log("***", err || info);
}
);
}
);
} catch (error) {
logger.log("sns-error", "ERROR", "api", null, {
error: JSON.stringify(error),
});
}
} catch (error) {
logger.log("sns-error", "ERROR", "api", null, {
error: JSON.stringify(error),
});
}
res.sendStatus(200);
res.sendStatus(200);
};

View File

@@ -1,287 +1,215 @@
var admin = require("firebase-admin");
const admin = require("firebase-admin");
const logger = require("../utils/logger");
const path = require("path");
const { auth } = require("firebase-admin");
const {auth} = require("firebase-admin");
require("dotenv").config({
path: path.resolve(
process.cwd(),
`.env.${process.env.NODE_ENV || "development"}`
),
path: path.resolve(
process.cwd(),
`.env.${process.env.NODE_ENV || "development"}`
),
});
const client = require("../graphql-client/graphql-client").client;
var serviceAccount = require(process.env.FIREBASE_ADMINSDK_JSON);
const serviceAccount = require(process.env.FIREBASE_ADMINSDK_JSON);
const adminEmail = require("../utils/adminEmail");
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
databaseURL: process.env.FIREBASE_DATABASE_URL,
credential: admin.credential.cert(serviceAccount),
databaseURL: process.env.FIREBASE_DATABASE_URL,
});
exports.admin = admin;
const adminEmail = [
"patrick@imex.dev",
//"patrick@imex.test",
"patrick@imex.prod",
"patrick@imexsystems.ca",
"patrick@thinkimex.com",
];
exports.createUser = async (req, res) => {
logger.log("admin-create-user", "ADMIN", req.user.email, null, {
request: req.body,
ioadmin: true,
});
logger.log("admin-create-user", "ADMIN", req.user.email, null, {
request: req.body,
ioadmin: true,
});
const { email, displayName, password, shopid, authlevel } = req.body;
try {
const userRecord = await admin
.auth()
.createUser({ email, displayName, password });
const {email, displayName, password, shopid, authlevel} = req.body;
try {
const userRecord = await admin
.auth()
.createUser({email, displayName, password});
// See the UserRecord reference doc for the contents of userRecord.
// See the UserRecord reference doc for the contents of userRecord.
const result = await client.request(
`
const result = await client.request(
`
mutation INSERT_USER($user: users_insert_input!) {
insert_users_one(object: $user) {
email
}
}
`,
{
user: {
email: email.toLowerCase(),
authid: userRecord.uid,
associations: {
data: [{ shopid, authlevel, active: true }],
},
},
}
);
{
user: {
email: email.toLowerCase(),
authid: userRecord.uid,
associations: {
data: [{shopid, authlevel, active: true}],
},
},
}
);
res.json({ userRecord, result });
} catch (error) {
logger.log("admin-update-user-error", "ERROR", req.user.email, null, {
error,
});
res.status(500).json(error);
}
res.json({userRecord, result});
} catch (error) {
logger.log("admin-update-user-error", "ERROR", req.user.email, null, {
error,
});
res.status(500).json(error);
}
};
exports.updateUser = (req, res) => {
logger.log("admin-update-user", "ADMIN", req.user.email, null, {
request: req.body,
ioadmin: true,
});
if (!adminEmail.includes(req.user.email) && !req.user.ioadmin) {
logger.log(
"admin-update-user-unauthorized",
"ERROR",
req.user.email,
null,
{
logger.log("admin-update-user", "ADMIN", req.user.email, null, {
request: req.body,
user: req.user,
}
);
res.sendStatus(404);
return;
}
admin
.auth()
.updateUser(
req.body.uid,
req.body.user
// {
// email: "modifiedUser@example.com",
// phoneNumber: "+11234567890",
// emailVerified: true,
// password: "newPassword",
// displayName: "Jane Doe",
// photoURL: "http://www.example.com/12345678/photo.png",
// disabled: true,
// }
)
.then((userRecord) => {
// See the UserRecord reference doc for the contents of userRecord.
logger.log("admin-update-user-success", "ADMIN", req.user.email, null, {
userRecord,
ioadmin: true,
});
res.json(userRecord);
})
.catch((error) => {
logger.log("admin-update-user-error", "ERROR", req.user.email, null, {
error,
});
res.status(500).json(error);
});
if (!adminEmail.includes(req.user.email) && !req.user.ioadmin) {
logger.log(
"admin-update-user-unauthorized",
"ERROR",
req.user.email,
null,
{
request: req.body,
user: req.user,
}
);
res.sendStatus(404);
return;
}
admin
.auth()
.updateUser(
req.body.uid,
req.body.user
// {
// email: "modifiedUser@example.com",
// phoneNumber: "+11234567890",
// emailVerified: true,
// password: "newPassword",
// displayName: "Jane Doe",
// photoURL: "http://www.example.com/12345678/photo.png",
// disabled: true,
// }
)
.then((userRecord) => {
// See the UserRecord reference doc for the contents of userRecord.
logger.log("admin-update-user-success", "ADMIN", req.user.email, null, {
userRecord,
ioadmin: true,
});
res.json(userRecord);
})
.catch((error) => {
logger.log("admin-update-user-error", "ERROR", req.user.email, null, {
error,
});
res.status(500).json(error);
});
};
exports.getUser = (req, res) => {
logger.log("admin-get-user", "ADMIN", req.user.email, null, {
request: req.body,
ioadmin: true,
});
if (!adminEmail.includes(req.user.email) && !req.user.ioadmin) {
logger.log(
"admin-update-user-unauthorized",
"ERROR",
req.user.email,
null,
{
logger.log("admin-get-user", "ADMIN", req.user.email, null, {
request: req.body,
user: req.user,
}
);
res.sendStatus(404);
return;
}
admin
.auth()
.getUser(req.body.uid)
.then((userRecord) => {
res.json(userRecord);
})
.catch((error) => {
logger.log("admin-get-user-error", "ERROR", req.user.email, null, {
error,
});
res.status(500).json(error);
ioadmin: true,
});
if (!adminEmail.includes(req.user.email) && !req.user.ioadmin) {
logger.log(
"admin-update-user-unauthorized",
"ERROR",
req.user.email,
null,
{
request: req.body,
user: req.user,
}
);
res.sendStatus(404);
return;
}
admin
.auth()
.getUser(req.body.uid)
.then((userRecord) => {
res.json(userRecord);
})
.catch((error) => {
logger.log("admin-get-user-error", "ERROR", req.user.email, null, {
error,
});
res.status(500).json(error);
});
};
exports.sendNotification = async (req, res) => {
setTimeout(() => {
// Send a message to the device corresponding to the provided
// registration token.
admin
.messaging()
.send({
topic: "PRD_PATRICK-messaging",
notification: {
title: `ImEX Online Message - +16049992002`,
body: "Test Noti.",
//imageUrl: "https://thinkimex.com/img/io-fcm.png",
},
data: {
type: "messaging-inbound",
conversationid: "e0eb17c3-3a78-4e3f-b932-55ef35aa2297",
text: "Hello. ",
image_path: "",
phone_num: "+16049992002",
},
})
.then((response) => {
// Response is a message ID string.
console.log("Successfully sent message:", response);
})
.catch((error) => {
console.log("Error sending message:", error);
});
setTimeout(() => {
// Send a message to the device corresponding to the provided
// registration token.
admin
.messaging()
.send({
topic: "PRD_PATRICK-messaging",
notification: {
title: `ImEX Online Message - +16049992002`,
body: "Test Noti.",
//imageUrl: "https://thinkimex.com/img/io-fcm.png",
},
data: {
type: "messaging-inbound",
conversationid: "e0eb17c3-3a78-4e3f-b932-55ef35aa2297",
text: "Hello. ",
image_path: "",
phone_num: "+16049992002",
},
})
.then((response) => {
// Response is a message ID string.
console.log("Successfully sent message:", response);
})
.catch((error) => {
console.log("Error sending message:", error);
});
res.sendStatus(200);
}, 500);
res.sendStatus(200);
}, 500);
};
exports.subscribe = async (req, res) => {
const result = await admin
.messaging()
.subscribeToTopic(
req.body.fcm_tokens,
`${req.body.imexshopid}-${req.body.type}`
);
const result = await admin
.messaging()
.subscribeToTopic(
req.body.fcm_tokens,
`${req.body.imexshopid}-${req.body.type}`
);
res.json(result);
res.json(result);
};
exports.unsubscribe = async (req, res) => {
try {
const result = await admin
.messaging()
.unsubscribeFromTopic(
req.body.fcm_tokens,
`${req.body.imexshopid}-${req.body.type}`
);
try {
const result = await admin
.messaging()
.unsubscribeFromTopic(
req.body.fcm_tokens,
`${req.body.imexshopid}-${req.body.type}`
);
res.json(result);
} catch (error) {
res.sendStatus(500);
}
res.json(result);
} catch (error) {
res.sendStatus(500);
}
};
exports.validateFirebaseIdToken = async (req, res, next) => {
if (
(!req.headers.authorization ||
!req.headers.authorization.startsWith("Bearer ")) &&
!(req.cookies && req.cookies.__session)
) {
console.error("Unauthorized attempt. No authorization provided.");
res.status(403).send("Unauthorized");
return;
}
let idToken;
if (
req.headers.authorization &&
req.headers.authorization.startsWith("Bearer ")
) {
// console.log('Found "Authorization" header');
// Read the ID Token from the Authorization header.
idToken = req.headers.authorization.split("Bearer ")[1];
} else if (req.cookies) {
//console.log('Found "__session" cookie');
// Read the ID Token from cookie.
idToken = req.cookies.__session;
} else {
// No cookie
console.error("Unauthorized attempt. No cookie provided.");
logger.log("api-unauthorized-call", "WARN", null, null, {
req,
type: "no-cookie",
});
res.status(403).send("Unauthorized");
return;
}
try {
const decodedIdToken = await admin.auth().verifyIdToken(idToken);
//console.log("ID Token correctly decoded", decodedIdToken);
req.user = decodedIdToken;
next();
return;
} catch (error) {
logger.log("api-unauthorized-call", "WARN", null, null, {
path: req.path,
body: req.body,
type: "unauthroized",
...error,
});
res.status(401).send("Unauthorized");
return;
}
};
exports.validateAdmin = async (req, res, next) => {
if (!adminEmail.includes(req.user.email) && !req.user.ioadmin) {
logger.log("admin-validation-failed", "ERROR", req.user.email, null, {
request: req.body,
user: req.user,
});
res.sendStatus(404);
return;
} else {
next();
return;
}
};
//Admin claims code.
// const uid = "JEqqYlsadwPEXIiyRBR55fflfko1";

View File

@@ -518,6 +518,21 @@ exports.QUERY_PAYMENTS_FOR_EXPORT = `
}
}`;
exports.QUERY_TRANSITIONS_BY_JOBID = `query QUERY_TRANSITIONS_BY_JOBID($jobids: [uuid!]!) {
transitions(where: {jobid: {_in: $jobids}}, order_by: {end: desc}) {
start
end
value
prev_value
next_value
duration
type
created_at
updated_at
jobid
}
}`;
exports.QUERY_UPCOMING_APPOINTMENTS = `query QUERY_UPCOMING_APPOINTMENTS($now: timestamptz!, $jobId: uuid!) {
jobs_by_pk(id: $jobId) {
bodyshop {

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,68 @@
const _ = require("lodash");
const queries = require("../graphql-client/queries");
const moment = require("moment");
const durationToHumanReadable = require("../utils/durationToHumanReadable");
const calculateStatusDuration = require("../utils/calculateStatusDuration");
const jobLifecycle = async (req, res) => {
// Grab the jobids and statuses from the request body
const {
jobids,
statuses
} = req.body;
if (!jobids) {
return res.status(400).json({
error: "Missing jobids"
});
}
const jobIDs = _.isArray(jobids) ? jobids : [jobids];
const client = req.userGraphQLClient;
const resp = await client.request(queries.QUERY_TRANSITIONS_BY_JOBID, {jobids: jobIDs,});
const transitions = resp.transitions;
if (!transitions) {
return res.status(200).json({
jobIDs,
transitions: []
});
}
const transitionsByJobId = _.groupBy(resp.transitions, 'jobid');
const groupedTransitions = {};
for (let jobId in transitionsByJobId) {
let lifecycle = transitionsByJobId[jobId].map(transition => {
transition.start_readable = transition.start ? moment(transition.start).fromNow() : 'N/A';
transition.end_readable = transition.end ? moment(transition.end).fromNow() : 'N/A';
if (transition.duration) {
transition.duration_seconds = Math.round(transition.duration / 1000);
transition.duration_minutes = Math.round(transition.duration_seconds / 60);
let duration = moment.duration(transition.duration);
transition.duration_readable = durationToHumanReadable(duration);
} else {
transition.duration_seconds = 0;
transition.duration_minutes = 0;
transition.duration_readable = 'N/A';
}
return transition;
});
groupedTransitions[jobId] = {
lifecycle: lifecycle,
durations: calculateStatusDuration(lifecycle, statuses),
};
}
return res.status(200).json({
jobIDs,
transition: groupedTransitions,
});
}
module.exports = jobLifecycle;

View File

@@ -9,83 +9,84 @@ const logger = require("../utils/logger");
Dinero.globalRoundingMode = "HALF_EVEN";
const path = require("path");
const client = require("../graphql-client/graphql-client").client;
require("dotenv").config({
path: path.resolve(
process.cwd(),
`.env.${process.env.NODE_ENV || "development"}`
),
path: path.resolve(
process.cwd(),
`.env.${process.env.NODE_ENV || "development"}`
),
});
async function StatusTransition(req, res) {
if (req.headers["event-secret"] !== process.env.EVENT_SECRET) {
res.status(401).send("Unauthorized");
return;
}
res.sendStatus(200);
return;
const {
id: jobid,
status: value,
shopid: bodyshopid,
} = req.body.event.data.new;
try {
const { update_transitions } = await client.request(
queries.UPDATE_OLD_TRANSITION,
{
jobid: jobid,
existingTransition: {
end: new Date(),
next_value: value,
const {
id: jobid,
status: value,
shopid: bodyshopid,
} = req.body.event.data.new;
//duration
},
}
);
// Create record OPEN on new item, enter state
// If change to SCHEDULE, update the last record and create a new record (update status and end time on old record, create a new record saying we came from previous status going to previous status
// (Timeline)
// Final status is exported, there is no end date as there is no further transition (has no end date)
try {
const {update_transitions} = await client.request(
queries.UPDATE_OLD_TRANSITION,
{
jobid: jobid,
existingTransition: {
end: new Date(),
next_value: value,
let duration =
update_transitions.affected_rows === 0
? 0
: new Date(update_transitions.returning[0].end) -
new Date(update_transitions.returning[0].start);
//duration
},
}
);
const resp2 = await client.request(queries.INSERT_NEW_TRANSITION, {
oldTransitionId:
update_transitions.affected_rows === 0
? null
: update_transitions.returning[0].id,
duration,
newTransition: {
bodyshopid: bodyshopid,
jobid: jobid,
start:
update_transitions.affected_rows === 0
? new Date()
: update_transitions.returning[0].end,
prev_value:
update_transitions.affected_rows === 0
? null
: update_transitions.returning[0].value,
value: value,
type: "status",
},
});
let duration =
update_transitions.affected_rows === 0
? 0
: new Date(update_transitions.returning[0].end) -
new Date(update_transitions.returning[0].start);
//Check to see if there is an existing status transition record.
//Query using Job ID, start is not null, end is null.
const resp2 = await client.request(queries.INSERT_NEW_TRANSITION, {
oldTransitionId:
update_transitions.affected_rows === 0
? null
: update_transitions.returning[0].id,
duration,
newTransition: {
bodyshopid: bodyshopid,
jobid: jobid,
start:
update_transitions.affected_rows === 0
? new Date()
: update_transitions.returning[0].end,
prev_value:
update_transitions.affected_rows === 0
? null
: update_transitions.returning[0].value,
value: value,
type: "status",
},
});
//If there is no existing record, this is the start of the transition life cycle.
// Create the initial transition record.
//Check to see if there is an existing status transition record.
//Query using Job ID, start is not null, end is null.
//If there is a current status transition record, update it with the end date, duration, and next value.
//If there is no existing record, this is the start of the transition life cycle.
// Create the initial transition record.
res.sendStatus(200); //.json(ret);
} catch (error) {
logger.log("job-status-transition-error", "ERROR", req.user?.email, jobid, {
message: error.message,
stack: error.stack,
});
//If there is a current status transition record, update it with the end date, duration, and next value.
res.status(400).send(JSON.stringify(error));
}
res.sendStatus(200); //.json(ret);
} catch (error) {
logger.log("job-status-transition-error", "ERROR", req.user?.email, jobid, {
message: error.message,
stack: error.stack,
});
res.status(400).send(JSON.stringify(error));
}
}
exports.statustransition = StatusTransition;

View File

@@ -1,20 +1,18 @@
const Dinero = require("dinero.js");
const queries = require("../graphql-client/queries");
const GraphQLClient = require("graphql-request").GraphQLClient;
const logger = require("../utils/logger");
const logger = require('../utils/logger');
// Dinero.defaultCurrency = "USD";
// Dinero.globalLocale = "en-CA";
Dinero.globalRoundingMode = "HALF_EVEN";
exports.totalsSsu = async function (req, res) {
const BearerToken = req.headers.authorization;
const { id } = req.body;
const BearerToken = req.BearerToken;
const client = req.userGraphQLClient;
logger.log("job-totals-ssu", "DEBUG", req.user.email, id, null);
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {
headers: {
Authorization: BearerToken,
},
});
try {
const job = await client
@@ -75,21 +73,19 @@ async function TotalsServerSide(req, res) {
}
async function Totals(req, res) {
const { job } = req.body;
const { job, id } = req.body;
const logger = req.logger;
const client = req.userGraphQLClient;
logger.log("job-totals", "DEBUG", req.user.email, job.id, {
jobid: job.id,
});
const BearerToken = req.headers.authorization;
const { id } = req.body;
logger.log("job-totals-ssu", "DEBUG", req.user.email, id, null);
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {
headers: {
Authorization: BearerToken,
},
});
await AutoAddAtsIfRequired({ job, client });
try {
let ret = {
parts: CalculatePartsTotals(job.joblines),

View File

@@ -3,3 +3,4 @@ exports.totalsSsu = require("./job-totals").totalsSsu;
exports.costing = require("./job-costing").JobCosting;
exports.costingmulti = require("./job-costing").JobCostingMulti;
exports.statustransition = require("./job-status-transition").statustransition;
exports.lifecycle = require('./job-lifecycle');

View File

@@ -0,0 +1,20 @@
const path = require("path");
/**
* Checks if the event secret is correct
* It adds the following properties to the request object:
* - req.isEventAuthorized - Returns true if the event secret is correct
* @param req
* @param res
* @param next
*/
function eventAuthorizationMiddleware(req, res, next) {
if (req.headers["event-secret"] !== process.env.EVENT_SECRET) {
return res.status(401).send("Unauthorized");
}
req.isEventAuthorized = true;
next();
}
module.exports = eventAuthorizationMiddleware;

View File

@@ -0,0 +1,26 @@
const logger = require("../utils/logger");
const adminEmail = require("../utils/adminEmail");
/**
* Validate admin middleware
* It adds the following properties to the request object:
* - req.isAdmin - returns true if the user passed an admin check
* @param req
* @param res
* @param next
* @returns {*}
*/
const validateAdminMiddleware = (req, res, next) => {
if (!adminEmail.includes(req.user.email) && !req.user.ioadmin) {
logger.log("admin-validation-failed", "ERROR", req.user.email, null, {
request: req.body,
user: req.user,
});
return res.sendStatus(404);
}
req.isAdmin = true;
next();
};
module.exports = validateAdminMiddleware;

View File

@@ -0,0 +1,69 @@
const logger = require("../utils/logger");
const admin = require("firebase-admin");
/**
* Middleware to validate Firebase ID Tokens.
* This middleware is used to protect API endpoints from unauthorized access.
* It adds the following properties to the request object:
* - req.user - the decoded Firebase ID Token
* @param req
* @param res
* @param next
* @returns {Promise<void>}
*/
const validateFirebaseIdTokenMiddleware = async (req, res, next) => {
if (
(
!req.headers.authorization ||
!req.headers.authorization.startsWith("Bearer ")) &&
!(req.cookies && req.cookies.__session
)
) {
console.error("Unauthorized attempt. No authorization provided.");
return res.status(403).send("Unauthorized");
}
let idToken;
if (
req.headers.authorization &&
req.headers.authorization.startsWith("Bearer ")
) {
// console.log('Found "Authorization" header');
// Read the ID Token from the Authorization header.
idToken = req.headers.authorization.split("Bearer ")[1];
} else if (req.cookies) {
//console.log('Found "__session" cookie');
// Read the ID Token from cookie.
idToken = req.cookies.__session;
} else {
// No cookie
console.error("Unauthorized attempt. No cookie provided.");
logger.log("api-unauthorized-call", "WARN", null, null, {
req,
type: "no-cookie",
});
return res.status(403).send("Unauthorized");
}
try {
const decodedIdToken = await admin.auth().verifyIdToken(idToken);
//console.log("ID Token correctly decoded", decodedIdToken);
req.user = decodedIdToken;
next();
} catch (error) {
logger.log("api-unauthorized-call", "WARN", null, null, {
path: req.path,
body: req.body,
type: "unauthroized",
...error,
});
return res.status(401).send("Unauthorized");
}
};
module.exports = validateFirebaseIdTokenMiddleware;

View File

@@ -0,0 +1,24 @@
const {GraphQLClient} = require("graphql-request");
/**
* Middleware to add a GraphQL Client to the request object
* Adds the following to the request object:
* req.userGraphQLClient - GraphQL Client with user Bearer Token
* req.BearerToken - Bearer Token
* @param req
* @param res
* @param next
*/
const withUserGraphQLClientMiddleware = (req, res, next) => {
const BearerToken = req.headers.authorization;
req.userGraphQLClient = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {
headers: {
Authorization: BearerToken,
},
});
req.BearerToken = BearerToken;
next();
};
module.exports = withUserGraphQLClientMiddleware;

View File

@@ -1,9 +1,8 @@
const path = require("path");
const _ = require("lodash");
const logger = require("../utils/logger");
const xml2js = require("xml2js");
const GraphQLClient = require("graphql-request").GraphQLClient;
const queries = require("../graphql-client/queries");
const logger = require('../utils/logger');
require("dotenv").config({
path: path.resolve(
@@ -15,13 +14,10 @@ require("dotenv").config({
exports.mixdataUpload = async (req, res) => {
const { bodyshopid } = req.body;
const BearerToken = req.headers.authorization;
const client = req.userGraphQLClient;
logger.log("job-mixdata-upload", "DEBUG", req.user.email, null, null);
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {
headers: {
Authorization: BearerToken,
},
});
try {
for (const element of req.files) {

View File

@@ -5,7 +5,6 @@ require("dotenv").config({
),
});
const GraphQLClient = require("graphql-request").GraphQLClient;
//const client = require("../graphql-client/graphql-client").client;
const logger = require("../utils/logger");
const queries = require("../graphql-client/queries");
@@ -15,10 +14,6 @@ const {getClient} = require('../../libs/awsUtils');
async function OpenSearchUpdateHandler(req, res) {
if (req.headers["event-secret"] !== process.env.EVENT_SECRET) {
res.status(401).send("Unauthorized");
return;
}
try {
const osClient = await getClient();
@@ -186,12 +181,8 @@ async function OpenSearchSearchHandler(req, res) {
search,
});
const BearerToken = req.headers.authorization;
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {
headers: {
Authorization: BearerToken,
},
});
const BearerToken = req.BearerToken;
const client = req.userGraphQLClient;
const assocs = await client
.setHeaders({Authorization: BearerToken})

View File

@@ -1,21 +1,19 @@
const Dinero = require("dinero.js");
const queries = require("../graphql-client/queries");
const logger = require('../utils/logger');
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;
const BearerToken = req.BearerToken;
const client = req.userGraphQLClient;
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.

View File

@@ -0,0 +1,13 @@
const express = require('express');
const router = express.Router();
const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware");
const {payments, payables, receivables} = require("../accounting/qbxml/qbxml");
const withUserGraphQLClientMiddleware = require("../middleware/withUserGraphQLClientMiddleware");
router.use(validateFirebaseIdTokenMiddleware);
router.post('/qbxml/receivables', withUserGraphQLClientMiddleware, receivables);
router.post('/qbxml/payables', withUserGraphQLClientMiddleware, payables);
router.post('/qbxml/payments', withUserGraphQLClientMiddleware, payments);
module.exports = router;

View File

@@ -0,0 +1,18 @@
const express = require('express');
const router = express.Router();
const fb = require('../firebase/firebase-handler');
const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware");
const {createAssociation, createShop, updateShop, updateCounter} = require("../admin/adminops");
const validateAdminMiddleware = require("../middleware/validateAdminMiddleware");
router.use(validateFirebaseIdTokenMiddleware);
router.post('/createassociation', validateAdminMiddleware, createAssociation);
router.post('/createshop', validateAdminMiddleware, createShop);
router.post('/updateshop', validateAdminMiddleware, updateShop);
router.post('/updatecounter', validateAdminMiddleware, updateCounter);
router.post('/updateuser', fb.updateUser);
router.post('/getuser', fb.getUser);
router.post('/createuser', fb.createUser);
module.exports = router;

View File

@@ -0,0 +1,11 @@
const express = require('express');
const router = express.Router();
const cdkGetMake = require('../cdk/cdk-get-makes');
const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware");
const withUserGraphQLClientMiddleware = require("../middleware/withUserGraphQLClientMiddleware");
router.use(validateFirebaseIdTokenMiddleware);
router.post('/getvehicles', withUserGraphQLClientMiddleware, cdkGetMake.default);
module.exports = router;

View File

@@ -0,0 +1,9 @@
const express = require('express');
const router = express.Router();
const {autohouse, claimscorp, kaizen} = require('../data/data');
router.post('/ah', autohouse);
router.post('/cc', claimscorp);
router.post('/kaizen', kaizen);
module.exports = router;

View File

@@ -0,0 +1,11 @@
const express = require('express');
const router = express.Router();
const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware");
const {lightbox_credentials, payment_refund, generate_payment_url, postback} = require("../intellipay/intellipay");
router.post('/lightbox_credentials', validateFirebaseIdTokenMiddleware, lightbox_credentials);
router.post('/payment_refund', validateFirebaseIdTokenMiddleware, payment_refund);
router.post('/generate_payment_url', validateFirebaseIdTokenMiddleware, generate_payment_url);
router.post('/postback', postback);
module.exports = router;

View File

@@ -0,0 +1,18 @@
const express = require('express');
const router = express.Router();
const job = require('../job/job');
const {partsScan} = require('../parts-scan/parts-scan');
const eventAuthorizationMiddleware = require('../middleware/eventAuthorizationMIddleware');
const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware");
const {totals, statustransition, totalsSsu, costing, lifecycle, costingmulti} = require("../job/job");
const withUserGraphQLClientMiddleware = require("../middleware/withUserGraphQLClientMiddleware");
router.post('/totals', validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, totals);
router.post('/statustransition', eventAuthorizationMiddleware, statustransition);
router.post('/totalsssu', validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware,totalsSsu);
router.post('/costing', validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware,costing);
router.post('/lifecycle', validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, lifecycle);
router.post('/costingmulti', validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, costingmulti);
router.post('/partsscan', validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, partsScan);
module.exports = router;

View File

@@ -0,0 +1,13 @@
const express = require('express');
const router = express.Router();
const {createSignedUploadURL, downloadFiles, renameKeys, deleteFiles} = require('../media/media');
const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware");
router.use(validateFirebaseIdTokenMiddleware);
router.post('/sign', createSignedUploadURL);
router.post('/download', downloadFiles);
router.post('/rename', renameKeys);
router.post('/delete', deleteFiles);
module.exports = router;

View File

@@ -0,0 +1,51 @@
const express = require('express');
const router = express.Router();
const logger = require("../../server/utils/logger");
const sendEmail = require("../email/sendemail");
const data = require("../data/data");
const bodyParser = require("body-parser");
const ioevent = require("../ioevent/ioevent");
const taskHandler = require("../tasks/tasks");
const os = require("../opensearch/os-handler");
const eventAuthorizationMiddleware = require("../middleware/eventAuthorizationMIddleware");
const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware");
const withUserGraphQLClientMiddleware = require("../middleware/withUserGraphQLClientMiddleware");
//Test route to ensure Express is responding.
router.get("/test", async function (req, res) {
const commit = require("child_process").execSync(
"git rev-parse --short HEAD"
);
// console.log(app.get('trust proxy'));
// console.log("remoteAddress", req.socket.remoteAddress);
// console.log("X-Forwarded-For", req.header('x-forwarded-for'));
logger.log("test-api-status", "DEBUG", "api", {commit});
// sendEmail.sendServerEmail({
// subject: `API Check - ${process.env.NODE_ENV}`,
// text: `Server API check has come in. Remote IP: ${req.socket.remoteAddress}, X-Forwarded-For: ${req.header('x-forwarded-for')}`,
// });
sendEmail.sendServerEmail({
subject: `API Check - ${process.env.NODE_ENV}`,
text: `Server API check has come in.`,
});
res.status(200).send(`OK - ${commit}`);
});
// Search
router.post("/search", validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, os.search);
router.post("/opensearch", eventAuthorizationMiddleware, os.handler);
// IO Events
router.post('/ioevent', ioevent.default);
// Email
router.post('/sendemail', validateFirebaseIdTokenMiddleware, sendEmail.sendEmail);
router.post('/emailbounce', bodyParser.text(), sendEmail.emailBounce);
// Handlers
router.post('/record-handler/arms', data.arms);
router.post("/taskHandler", validateFirebaseIdTokenMiddleware, taskHandler.taskHandler);
module.exports = router;

View File

@@ -0,0 +1,11 @@
const express = require('express');
const router = express.Router();
const multer = require('multer');
const upload = multer();
const {mixdataUpload} = require('../mixdata/mixdata');
const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware");
const withUserGraphQLClientMiddleware = require("../middleware/withUserGraphQLClientMiddleware");
router.post('/upload', validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, upload.any(), mixdataUpload);
module.exports = router;

View File

@@ -0,0 +1,11 @@
const express = require('express');
const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware");
const {subscribe, unsubscribe} = require("../firebase/firebase-handler");
const router = express.Router();
router.use(validateFirebaseIdTokenMiddleware);
router.post('/subscribe', subscribe);
router.post('/unsubscribe', unsubscribe);
module.exports = router;

View File

@@ -0,0 +1,14 @@
const express = require('express');
const router = express.Router();
const {authorize, callback, receivables, payables, payments} = require('../accounting/qbo/qbo');
const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware");
const withUserGraphQLClientMiddleware = require("../middleware/withUserGraphQLClientMiddleware"); // Assuming you have a qbo module for handling QuickBooks Online related functionalities
// Define the routes for QuickBooks Online
router.post('/authorize', validateFirebaseIdTokenMiddleware, authorize);
router.get('/callback', callback);
router.post('/receivables', validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, receivables);
router.post('/payables', validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, payables);
router.post('/payments', validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, payments);
module.exports = router;

View File

@@ -0,0 +1,9 @@
const express = require('express');
const router = express.Router();
const {inlinecss} = require('../render/inlinecss');
const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware");
// Define the route for inline CSS rendering
router.post('/inlinecss', validateFirebaseIdTokenMiddleware, inlinecss);
module.exports = router;

View File

@@ -0,0 +1,9 @@
const express = require('express');
const router = express.Router();
const {job} = require('../scheduling/scheduling-job');
const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware");
const withUserGraphQLClientMiddleware = require("../middleware/withUserGraphQLClientMiddleware");
router.post('/job', validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, job);
module.exports = router;

View File

@@ -0,0 +1,17 @@
const express = require('express');
const router = express.Router();
const twilio = require('twilio');
const {receive} = require('../sms/receive');
const {send} = require('../sms/send');
const {status, markConversationRead} = require('../sms/status');
const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware");
// Twilio Webhook Middleware for production
const twilioWebhookMiddleware = twilio.webhook({ validate: process.env.NODE_ENV === "PRODUCTION" });
router.post('/receive', twilioWebhookMiddleware, receive);
router.post('/send', validateFirebaseIdTokenMiddleware, send);
router.post('/status', twilioWebhookMiddleware, status);
router.post('/markConversationRead', validateFirebaseIdTokenMiddleware, markConversationRead);
module.exports = router;

View File

@@ -0,0 +1,8 @@
const express = require('express');
const router = express.Router();
const {techLogin} = require('../tech/tech');
const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware");
router.post('/login', validateFirebaseIdTokenMiddleware, techLogin);
module.exports = router;

View File

@@ -0,0 +1,9 @@
const express = require('express');
const router = express.Router();
const {servertime, jsrAuth} = require('../utils/utils');
const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware");
router.post('/time', servertime);
router.post('/jsr', validateFirebaseIdTokenMiddleware, jsrAuth);
module.exports = router;

View File

@@ -1,4 +1,3 @@
const GraphQLClient = require("graphql-request").GraphQLClient;
const path = require("path");
const queries = require("../graphql-client/queries");
const Dinero = require("dinero.js");
@@ -14,17 +13,14 @@ require("dotenv").config({
});
exports.job = async (req, res) => {
const BearerToken = req.headers.authorization;
const { jobId } = req.body;
const BearerToken = req.BearerToken;
const client = req.userGraphQLClient;
try {
logger.log("smart-scheduling-start", "DEBUG", req.user.email, jobId, null);
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {
headers: {
Authorization: BearerToken,
},
});
const result = await client
.setHeaders({ Authorization: BearerToken })
.request(queries.QUERY_UPCOMING_APPOINTMENTS, {

View File

@@ -0,0 +1,13 @@
/**
* List of admin email addresses
* @type {string[]}
*/
const adminEmail = [
"patrick@imex.dev",
//"patrick@imex.test",
"patrick@imex.prod",
"patrick@imexsystems.ca",
"patrick@thinkimex.com",
];
module.exports = adminEmail;

Some files were not shown because too many files have changed in this diff Show More