@@ -1,51 +1,53 @@
|
||||
import { ApolloProvider } from "@apollo/client";
|
||||
import { SplitFactory, SplitSdk } from "@splitsoftware/splitio-react";
|
||||
import { ConfigProvider } from "antd";
|
||||
import {ApolloProvider} from "@apollo/client";
|
||||
import {SplitFactoryProvider, SplitSdk,} from '@splitsoftware/splitio-react';
|
||||
import {ConfigProvider} from "antd";
|
||||
import enLocale from "antd/es/locale/en_US";
|
||||
import dayjs from "../utils/day";
|
||||
import 'dayjs/locale/en';
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import GlobalLoadingBar from "../components/global-loading-bar/global-loading-bar.component";
|
||||
import client from "../utils/GraphQLClient";
|
||||
import App from "./App";
|
||||
import * as Sentry from "@sentry/react";
|
||||
|
||||
import themeProvider from "./themeProvider";
|
||||
|
||||
dayjs.locale("en");
|
||||
|
||||
export const factory = SplitSdk({
|
||||
core: {
|
||||
authorizationKey: process.env.REACT_APP_SPLIT_API,
|
||||
key: "anon",
|
||||
},
|
||||
});
|
||||
const config = {
|
||||
core: {
|
||||
authorizationKey: process.env.REACT_APP_SPLIT_API,
|
||||
key: "anon",
|
||||
},
|
||||
};
|
||||
export const factory = SplitSdk(config);
|
||||
|
||||
export default function AppContainer() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<ApolloProvider client={client}>
|
||||
<ConfigProvider
|
||||
//componentSize="small"
|
||||
input={{ autoComplete: "new-password" }}
|
||||
locale={enLocale}
|
||||
theme={{
|
||||
token: {
|
||||
colorPrimary: "#326ade",
|
||||
colorInfo: "#326ade"
|
||||
},
|
||||
}}
|
||||
form={{
|
||||
validateMessages: {
|
||||
// eslint-disable-next-line no-template-curly-in-string
|
||||
required: t("general.validation.required", { label: "${label}" }),
|
||||
},
|
||||
}}
|
||||
>
|
||||
<GlobalLoadingBar />
|
||||
<SplitFactory factory={factory}>
|
||||
<App />
|
||||
</SplitFactory>
|
||||
</ConfigProvider>
|
||||
</ApolloProvider>
|
||||
);
|
||||
function AppContainer() {
|
||||
const {t} = useTranslation();
|
||||
|
||||
return (
|
||||
<ApolloProvider client={client}>
|
||||
<ConfigProvider
|
||||
//componentSize="small"
|
||||
input={{autoComplete: "new-password"}}
|
||||
locale={enLocale}
|
||||
theme={themeProvider}
|
||||
form={{
|
||||
validateMessages: {
|
||||
// eslint-disable-next-line no-template-curly-in-string
|
||||
required: t("general.validation.required", {label: "${label}"}),
|
||||
},
|
||||
}}
|
||||
>
|
||||
<GlobalLoadingBar/>
|
||||
<SplitFactoryProvider factory={factory}>
|
||||
<App/>
|
||||
</SplitFactoryProvider>
|
||||
</ConfigProvider>
|
||||
</ApolloProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default Sentry.withProfiler(AppContainer);
|
||||
|
||||
@@ -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="Rome 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}/>}>
|
||||
|
||||
@@ -5,10 +5,6 @@
|
||||
border-bottom: 1px solid #74695c !important;
|
||||
}
|
||||
|
||||
.ant-menu-dark .ant-menu-item:hover {
|
||||
background-color: #1890ff !important;
|
||||
}
|
||||
|
||||
.imex-table-header {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -151,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;
|
||||
|
||||
61
client/src/App/themeProvider.js
Normal file
61
client/src/App/themeProvider.js
Normal file
@@ -0,0 +1,61 @@
|
||||
import {defaultsDeep} from "lodash";
|
||||
import {theme} from "antd";
|
||||
|
||||
const {defaultAlgorithm, darkAlgorithm} = theme;
|
||||
|
||||
let isDarkMode = false;
|
||||
|
||||
/**
|
||||
* Default theme
|
||||
* @type {{components: {Menu: {itemDividerBorderColor: string}}}}
|
||||
*/
|
||||
const defaultTheme = {
|
||||
components: {
|
||||
Table: {
|
||||
rowHoverBg: '#e7f3ff',
|
||||
rowSelectedBg: '#e6f7ff',
|
||||
headerSortHoverBg: 'transparent',
|
||||
},
|
||||
Menu: {
|
||||
darkItemHoverBg: '#1677ff',
|
||||
itemHoverBg: '#1677ff',
|
||||
horizontalItemHoverBg: '#1677ff',
|
||||
}
|
||||
},
|
||||
token: {
|
||||
colorPrimary: "#326ade",
|
||||
colorInfo: "#326ade"
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Development theme
|
||||
* @type {{components: {Menu: {itemHoverBg: string, darkItemHoverBg: string, horizontalItemHoverBg: string}}, token: {colorPrimary: string}}}
|
||||
*/
|
||||
const devTheme = {
|
||||
components: {
|
||||
Menu: {
|
||||
darkItemHoverBg: '#a51d1d',
|
||||
itemHoverBg: '#a51d1d',
|
||||
horizontalItemHoverBg: '#a51d1d',
|
||||
}
|
||||
},
|
||||
token: {
|
||||
colorPrimary: '#a51d1d'
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Production theme
|
||||
* @type {{components: {Menu: {itemHoverBg: string, darkItemHoverBg: string, horizontalItemHoverBg: string}}, token: {colorPrimary: string}}}
|
||||
*/
|
||||
const prodTheme = {};
|
||||
|
||||
const currentTheme = process.env.NODE_ENV === "development" ? devTheme
|
||||
: prodTheme;
|
||||
|
||||
const finaltheme = {
|
||||
algorithm: isDarkMode ? darkAlgorithm : defaultAlgorithm,
|
||||
...defaultsDeep(currentTheme, defaultTheme)
|
||||
}
|
||||
export default finaltheme;
|
||||
@@ -1,11 +1,14 @@
|
||||
import {Button, Col, Collapse, Result, Row, Space} from "antd";
|
||||
import React from "react";
|
||||
import {withTranslation} from "react-i18next";
|
||||
import {logImEXEvent} from "../../firebase/firebase.utils";
|
||||
|
||||
import {connect} from "react-redux";
|
||||
import {createStructuredSelector} from "reselect";
|
||||
import {selectBodyshop, selectCurrentUser,} from "../../redux/user/user.selectors";
|
||||
import { withTranslation } from "react-i18next";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import * as Sentry from "@sentry/react";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import {
|
||||
selectBodyshop,
|
||||
selectCurrentUser,
|
||||
} from "../../redux/user/user.selectors";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
currentUser: selectCurrentUser,
|
||||
@@ -135,7 +138,6 @@ class ErrorBoundary extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(withTranslation()(ErrorBoundary));
|
||||
export default Sentry.withErrorBoundary(
|
||||
connect(mapStateToProps, mapDispatchToProps)(withTranslation()(ErrorBoundary))
|
||||
);
|
||||
|
||||
250
client/src/components/eula/eula.component.jsx
Normal file
250
client/src/components/eula/eula.component.jsx
Normal file
@@ -0,0 +1,250 @@
|
||||
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 = Object.entries(otherFormValues).reduce((acc, [key, value]) => {
|
||||
acc[key] = typeof value === 'string' ? value.trim() : value;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
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);
|
||||
21
client/src/components/eula/eula.styles.scss
Normal file
21
client/src/components/eula/eula.styles.scss
Normal 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%;
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, {useCallback, useEffect, useState} from 'react';
|
||||
import moment from "moment";
|
||||
import day from '../../utils/day';
|
||||
import axios from 'axios';
|
||||
import {Badge, Card, Space, Table, Tag} from 'antd';
|
||||
import {gql, useQuery} from "@apollo/client";
|
||||
@@ -69,7 +69,7 @@ export function JobLifecycleComponent({job, statuses, ...rest}) {
|
||||
dataIndex: 'start',
|
||||
key: 'start',
|
||||
render: (text) => DateTimeFormatterFunction(text),
|
||||
sorter: (a, b) => moment(a.start).unix() - moment(b.start).unix(),
|
||||
sorter: (a, b) => day(a.start).unix() - day(b.start).unix(),
|
||||
},
|
||||
{
|
||||
title: t('job_lifecycle.columns.relative_start'),
|
||||
@@ -87,7 +87,7 @@ export function JobLifecycleComponent({job, statuses, ...rest}) {
|
||||
}
|
||||
return isEmpty(a.end) ? 1 : -1;
|
||||
}
|
||||
return moment(a.end).unix() - moment(b.end).unix();
|
||||
return day(a.end).unix() - day(b.end).unix();
|
||||
},
|
||||
render: (text) => isEmpty(text) ? t('job_lifecycle.content.not_available') : DateTimeFormatterFunction(text)
|
||||
},
|
||||
@@ -179,7 +179,7 @@ export function JobLifecycleComponent({job, statuses, ...rest}) {
|
||||
style={{marginTop: '10px'}}>
|
||||
<div>
|
||||
{lifecycleData.durations.summations.map((key) => (
|
||||
<Tag color={key.color} style={{width: '13vh', padding: '4px', margin: '4px'}}>
|
||||
<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}`}
|
||||
|
||||
@@ -1,54 +1,54 @@
|
||||
import {DownCircleFilled} from "@ant-design/icons";
|
||||
import {useMutation} from "@apollo/client";
|
||||
import {Button, Dropdown, notification} from "antd";
|
||||
import { DownCircleFilled } from "@ant-design/icons";
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { Button, Dropdown, notification } from "antd";
|
||||
import React from "react";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {connect} from "react-redux";
|
||||
import {createStructuredSelector} from "reselect";
|
||||
import {UPDATE_JOB_STATUS} from "../../graphql/jobs.queries";
|
||||
import {insertAuditTrail} from "../../redux/application/application.actions";
|
||||
import {selectBodyshop} from "../../redux/user/user.selectors";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { UPDATE_JOB_STATUS } from "../../graphql/jobs.queries";
|
||||
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({jobid, operation}) =>
|
||||
dispatch(insertAuditTrail({jobid, operation})),
|
||||
insertAuditTrail: ({ jobid, operation }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation })),
|
||||
});
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(JobsAdminStatus);
|
||||
|
||||
export function JobsAdminStatus({insertAuditTrail, bodyshop, job}) {
|
||||
const {t} = useTranslation();
|
||||
export function JobsAdminStatus({ insertAuditTrail, bodyshop, job }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [mutationUpdateJobstatus] = useMutation(UPDATE_JOB_STATUS);
|
||||
const updateJobStatus = (status) => {
|
||||
mutationUpdateJobstatus({
|
||||
variables: {jobId: job.id, status: status},
|
||||
})
|
||||
.then((r) => {
|
||||
notification["success"]({message: t("jobs.successes.save")});
|
||||
insertAuditTrail({
|
||||
jobid: job.id,
|
||||
operation: AuditTrailMapping.admin_jobstatuschange(status),
|
||||
});
|
||||
// refetch();
|
||||
})
|
||||
.catch((error) => {
|
||||
notification["error"]({message: t("jobs.errors.saving")});
|
||||
});
|
||||
};
|
||||
const [mutationUpdateJobstatus] = useMutation(UPDATE_JOB_STATUS);
|
||||
const updateJobStatus = (status) => {
|
||||
mutationUpdateJobstatus({
|
||||
variables: { jobId: job.id, status: status },
|
||||
})
|
||||
.then((r) => {
|
||||
notification["success"]({ message: t("jobs.successes.save") });
|
||||
insertAuditTrail({
|
||||
jobid: job.id,
|
||||
operation: AuditTrailMapping.admin_jobstatuschange(status),
|
||||
});
|
||||
// refetch();
|
||||
})
|
||||
.catch((error) => {
|
||||
notification["error"]({ message: t("jobs.errors.saving") });
|
||||
});
|
||||
};
|
||||
|
||||
const statusMenu = {
|
||||
items: bodyshop.md_ro_statuses.statuses.map((item) => ({
|
||||
key: item,
|
||||
label: item,
|
||||
})),
|
||||
onClick: (e) => {
|
||||
updateJobStatus(e.key);
|
||||
},
|
||||
}
|
||||
const statusMenu = {
|
||||
items: bodyshop.md_ro_statuses.statuses.map((item) => ({
|
||||
key: item,
|
||||
label: item,
|
||||
})),
|
||||
onClick: (e) => {
|
||||
updateJobStatus(e.key);
|
||||
},
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -56,7 +56,7 @@ export function JobsAdminStatus({insertAuditTrail, bodyshop, job}) {
|
||||
<Button shape="round">
|
||||
<span>{job.status}</span>
|
||||
|
||||
<DownCircleFilled/>
|
||||
<DownCircleFilled />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</>
|
||||
|
||||
@@ -76,7 +76,7 @@ export function JobAdminMarkReexport({
|
||||
const result = await markJobExported({
|
||||
variables: {
|
||||
jobId: job.id,
|
||||
date_exported: moment(),
|
||||
date_exported: dayjs(),
|
||||
default_exported: bodyshop.md_ro_statuses.default_exported,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -732,12 +732,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>
|
||||
});
|
||||
|
||||
@@ -42,8 +42,13 @@ export function JobsDetailRatesTaxes({
|
||||
})
|
||||
);
|
||||
}
|
||||
formItems.push(Space({children: section, wrap: true}));
|
||||
formItems.push(<Divider/>);
|
||||
formItems.push(<>
|
||||
<Space wrap>
|
||||
{section}
|
||||
</Space>
|
||||
<Divider/>
|
||||
</>)
|
||||
|
||||
}
|
||||
return (
|
||||
<Collapse defaultActiveKey={expanded && "rates"}>
|
||||
|
||||
@@ -220,12 +220,12 @@ export function PayableExportAll({
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
if (bodyshop.pbs_serialnumber)
|
||||
return (
|
||||
<Link to={{state: {billids}, pathname: `/manage/dmsap`}}>
|
||||
<Button>{t("jobs.actions.export")}</Button>
|
||||
</Link>
|
||||
);
|
||||
if (bodyshop.pbs_serialnumber)
|
||||
return (
|
||||
<Link to='/manage/dmsap' state={{ billids }}>
|
||||
<Button>{t("jobs.actions.export")}</Button>
|
||||
</Link>
|
||||
);
|
||||
|
||||
return (
|
||||
<Button onClick={handleQbxml} loading={loading} disabled={disabled}>
|
||||
|
||||
@@ -213,12 +213,12 @@ export function PayableExportButton({
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
if (bodyshop.pbs_serialnumber)
|
||||
return (
|
||||
<Link to={{state: {billids: [billId]}, pathname: `/manage/dmsap`}}>
|
||||
<Button>{t("jobs.actions.export")}</Button>
|
||||
</Link>
|
||||
);
|
||||
if (bodyshop.pbs_serialnumber)
|
||||
return (
|
||||
<Link to='/manage/dmsap' state={{billids: [billId]}}>
|
||||
<Button>{t("jobs.actions.export")}</Button>
|
||||
</Link>
|
||||
);
|
||||
|
||||
return (
|
||||
<Button onClick={handleQbxml} loading={loading} disabled={disabled}>
|
||||
|
||||
@@ -106,36 +106,36 @@ export function ProductionBoardKanbanComponent({
|
||||
|
||||
const oldChildCardNewParent = oldChildCard ? card.kanbanparent : null;
|
||||
|
||||
let movedCardNewKanbanParent;
|
||||
if (movedCardWillBeFirst) {
|
||||
//console.log("==> New Card is first.");
|
||||
movedCardNewKanbanParent = "-1";
|
||||
} else if (movedCardWillBeLast) {
|
||||
// console.log("==> New Card is last.");
|
||||
movedCardNewKanbanParent = lastCardInDestinationColumn.id;
|
||||
} else if (!!newChildCard) {
|
||||
// console.log("==> New Card is somewhere in the middle");
|
||||
movedCardNewKanbanParent = newChildCard.kanbanparent;
|
||||
} else {
|
||||
throw new Error("==> !!!!!!Couldn't find a parent.!!!! <==");
|
||||
}
|
||||
const newChildCardNewParent = newChildCard ? card.id : null;
|
||||
const update = await client.mutate({
|
||||
mutation: generate_UPDATE_JOB_KANBAN(
|
||||
oldChildCard ? oldChildCard.id : null,
|
||||
oldChildCardNewParent,
|
||||
card.id,
|
||||
movedCardNewKanbanParent,
|
||||
destination.toColumnId,
|
||||
newChildCard ? newChildCard.id : null,
|
||||
newChildCardNewParent
|
||||
),
|
||||
// TODO: optimisticResponse
|
||||
});
|
||||
insertAuditTrail({
|
||||
jobid: card.id,
|
||||
operation: AuditTrailMapping.jobstatuschange(destination.toColumnId),
|
||||
});
|
||||
let movedCardNewKanbanParent;
|
||||
if (movedCardWillBeFirst) {
|
||||
//console.log("==> New Card is first.");
|
||||
movedCardNewKanbanParent = "-1";
|
||||
} else if (movedCardWillBeLast) {
|
||||
// console.log("==> New Card is last.");
|
||||
movedCardNewKanbanParent = lastCardInDestinationColumn.id;
|
||||
} else if (!!newChildCard) {
|
||||
// console.log("==> New Card is somewhere in the middle");
|
||||
movedCardNewKanbanParent = newChildCard.kanbanparent;
|
||||
} else {
|
||||
console.log("==> !!!!!!Couldn't find a parent.!!!! <==");
|
||||
}
|
||||
const newChildCardNewParent = newChildCard ? card.id : null;
|
||||
const update = await client.mutate({
|
||||
mutation: generate_UPDATE_JOB_KANBAN(
|
||||
oldChildCard ? oldChildCard.id : null,
|
||||
oldChildCardNewParent,
|
||||
card.id,
|
||||
movedCardNewKanbanParent,
|
||||
destination.toColumnId,
|
||||
newChildCard ? newChildCard.id : null,
|
||||
newChildCardNewParent
|
||||
),
|
||||
// TODO: optimisticResponse
|
||||
});
|
||||
insertAuditTrail({
|
||||
jobid: card.id,
|
||||
operation: AuditTrailMapping.jobstatuschange(destination.toColumnId),
|
||||
});
|
||||
|
||||
if (update.errors) {
|
||||
notification["error"]({
|
||||
@@ -146,30 +146,30 @@ export function ProductionBoardKanbanComponent({
|
||||
}
|
||||
};
|
||||
|
||||
const totalHrs = data
|
||||
.reduce(
|
||||
(acc, val) =>
|
||||
acc +
|
||||
(val.labhrs?.aggregate?.sum?.mod_lb_hrs || 0) +
|
||||
(val.larhrs?.aggregate?.sum?.mod_lb_hrs || 0),
|
||||
0
|
||||
)
|
||||
.toFixed(1);
|
||||
const totalLAB = data
|
||||
.reduce(
|
||||
(acc, val) => acc + (val.labhrs?.aggregate?.sum?.mod_lb_hrs || 0),
|
||||
0
|
||||
)
|
||||
.toFixed(1);
|
||||
const totalLAR = data
|
||||
.reduce(
|
||||
(acc, val) => acc + (val.larhrs?.aggregate?.sum?.mod_lb_hrs || 0),
|
||||
0
|
||||
)
|
||||
.toFixed(1);
|
||||
const selectedBreakpoint = Object.entries(Grid.useBreakpoint())
|
||||
.filter((screen) => !!screen[1])
|
||||
.slice(-1)[0];
|
||||
const totalHrs = data
|
||||
.reduce(
|
||||
(acc, val) =>
|
||||
acc +
|
||||
(val.labhrs?.aggregate?.sum?.mod_lb_hrs || 0) +
|
||||
(val.larhrs?.aggregate?.sum?.mod_lb_hrs || 0),
|
||||
0
|
||||
)
|
||||
.toFixed(1);
|
||||
const totalLAB = data
|
||||
.reduce(
|
||||
(acc, val) => acc + (val.labhrs?.aggregate?.sum?.mod_lb_hrs || 0),
|
||||
0
|
||||
)
|
||||
.toFixed(1);
|
||||
const totalLAR = data
|
||||
.reduce(
|
||||
(acc, val) => acc + (val.larhrs?.aggregate?.sum?.mod_lb_hrs || 0),
|
||||
0
|
||||
)
|
||||
.toFixed(1);
|
||||
const selectedBreakpoint = Object.entries(Grid.useBreakpoint())
|
||||
.filter((screen) => !!screen[1])
|
||||
.slice(-1)[0];
|
||||
|
||||
const standardSizes = {
|
||||
xs: "250",
|
||||
|
||||
@@ -344,15 +344,16 @@ export function ScoreboardTimeTicketsStats({bodyshop}) {
|
||||
|
||||
const jobData = {};
|
||||
|
||||
data.jobs.forEach((job) => {
|
||||
job.tthrs = job.joblines.reduce((acc, val) => acc + val.mod_lb_hrs, 0);
|
||||
});
|
||||
const dataJobs = data.jobs.map((job) => ({
|
||||
...job,
|
||||
tthrs: job.joblines.reduce((acc, val) => acc + val.mod_lb_hrs, 0)
|
||||
}));
|
||||
|
||||
jobData.tthrs = data.jobs
|
||||
jobData.tthrs = dataJobs
|
||||
.reduce((acc, val) => acc + val.tthrs, 0)
|
||||
.toFixed(1);
|
||||
|
||||
jobData.count = data.jobs.length.toFixed(0);
|
||||
jobData.count = dataJobs.length.toFixed(0);
|
||||
|
||||
return {
|
||||
fixed: ret,
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
import { onError } from "@apollo/client/link/error";
|
||||
//https://stackoverflow.com/questions/57163454/refreshing-a-token-with-apollo-client-firebase-auth
|
||||
import * as Sentry from "@sentry/react";
|
||||
|
||||
const errorLink = onError(
|
||||
({ graphQLErrors, networkError, operation, forward }) => {
|
||||
if (graphQLErrors)
|
||||
graphQLErrors.forEach(({ message, locations, path }) =>
|
||||
if (graphQLErrors) {
|
||||
graphQLErrors.forEach(({ message, locations, path }) => {
|
||||
console.log(
|
||||
`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`
|
||||
)
|
||||
);
|
||||
);
|
||||
Sentry.captureException({ message, locations, path });
|
||||
});
|
||||
}
|
||||
if (networkError)
|
||||
console.log(`[Network error]: ${JSON.stringify(networkError)}`);
|
||||
console.log(operation.getContext());
|
||||
return forward(operation);
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -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 } } }) {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -13,6 +13,7 @@ import reportWebVitals from "./reportWebVitals";
|
||||
import "./translations/i18n";
|
||||
import "./utils/CleanAxios";
|
||||
import { ConfigProvider } from "antd";
|
||||
|
||||
//import { BrowserTracing } from "@sentry/tracing";
|
||||
//import "antd/dist/antd.css";
|
||||
// import "antd/dist/antd.less";
|
||||
@@ -25,24 +26,20 @@ if (process.env.NODE_ENV !== "development") {
|
||||
Sentry.init({
|
||||
dsn: "https://a6acc91c073e414196014b8484627a61@o492140.ingest.sentry.io/4504561071161344",
|
||||
ignoreErrors: [
|
||||
|
||||
"ResizeObserver loop",
|
||||
"Module specifier, 'fs' does not start",
|
||||
"Module specifier, 'zlib' does not start with",
|
||||
],
|
||||
integrations: [
|
||||
// new BrowserTracing(),
|
||||
// new Sentry.Integrations.Breadcrumbs({ console: true }),
|
||||
// new Sentry.Replay(),
|
||||
Sentry.replayIntegration({
|
||||
maskAllText: false,
|
||||
blockAllMedia: true,
|
||||
}),
|
||||
new Sentry.BrowserTracing(),
|
||||
],
|
||||
// This sets the sample rate to be 10%. You may want this to be 100% while
|
||||
// in development and sample at a lower rate in production
|
||||
// replaysSessionSampleRate: 0.1,
|
||||
// // If the entire session is not sampled, use the below sample rate to sample
|
||||
// // sessions when an error occurs.
|
||||
// replaysOnErrorSampleRate: 1.0,
|
||||
tracesSampleRate: 1.0,
|
||||
replaysOnErrorSampleRate: 1.0,
|
||||
environment: process.env.NODE_ENV,
|
||||
// tracesSampleRate: 0.2,
|
||||
// We recommend adjusting this value in production, or using tracesSampler
|
||||
// for finer control
|
||||
// tracesSampleRate: 0.5,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -207,7 +207,7 @@ export function JobsDetailPage({
|
||||
});
|
||||
|
||||
await refetch();
|
||||
form.setFieldsValue(transormJobToForm(job));
|
||||
form.setFieldsValue(transformJobToForm(job));
|
||||
form.resetFields();
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -277,7 +277,7 @@ export function JobsDetailPage({
|
||||
onFinish={handleFinish}
|
||||
{...formItemLayout}
|
||||
autoComplete={"off"}
|
||||
initialValues={transormJobToForm(job)}
|
||||
initialValues={transformJobToForm(job)}
|
||||
>
|
||||
<PageHeader
|
||||
// onBack={() => window.history.back()}
|
||||
@@ -383,25 +383,23 @@ export function JobsDetailPage({
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(JobsDetailPage);
|
||||
|
||||
const transormJobToForm = (job) => {
|
||||
Object.keys(job.parts_tax_rates).forEach((parttype) => {
|
||||
Object.keys(job.parts_tax_rates[parttype]).forEach((key) => {
|
||||
if (key.includes("tx_in")) {
|
||||
if (
|
||||
job.parts_tax_rates[parttype][key] === "Y" ||
|
||||
job.parts_tax_rates[parttype][key] === true
|
||||
) {
|
||||
job.parts_tax_rates[parttype][key] = true;
|
||||
} else {
|
||||
job.parts_tax_rates[parttype][key] = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
const transformJobToForm = (job) => {
|
||||
const transformedJob = { ...job };
|
||||
|
||||
return {
|
||||
...job,
|
||||
loss_date: job.loss_date ? dayjs(job.loss_date) : null,
|
||||
date_estimated: job.date_estimated ? dayjs(job.date_estimated) : null,
|
||||
};
|
||||
};
|
||||
transformedJob.parts_tax_rates = Object.keys(transformedJob.parts_tax_rates).reduce((acc, parttype) => {
|
||||
acc[parttype] = Object.keys(transformedJob.parts_tax_rates[parttype]).reduce((innerAcc, key) => {
|
||||
if (key.includes("tx_in")) {
|
||||
innerAcc[key] = transformedJob.parts_tax_rates[parttype][key] === "Y" || transformedJob.parts_tax_rates[parttype][key] === true;
|
||||
} else {
|
||||
innerAcc[key] = transformedJob.parts_tax_rates[parttype][key];
|
||||
}
|
||||
return innerAcc;
|
||||
}, {});
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
transformedJob.loss_date = transformedJob.loss_date ? dayjs(transformedJob.loss_date) : null;
|
||||
transformedJob.date_estimated = transformedJob.date_estimated ? dayjs(transformedJob.date_estimated) : null;
|
||||
|
||||
return transformedJob;
|
||||
};
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -36,3 +36,8 @@ export const selectLoginLoading = createSelector(
|
||||
[selectUser],
|
||||
(user) => user.loginLoading
|
||||
);
|
||||
|
||||
export const selectCurrentEula = createSelector(
|
||||
[selectUser],
|
||||
(user) => user.currentEula
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -947,6 +947,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}}"
|
||||
|
||||
@@ -947,6 +947,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}}"
|
||||
|
||||
@@ -947,6 +947,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}}"
|
||||
|
||||
@@ -18,7 +18,7 @@ export function DateTimeFormatter(props) {
|
||||
: null;
|
||||
}
|
||||
export function DateTimeFormatterFunction(date) {
|
||||
return moment(date).format("MM/DD/YYYY hh:mm a");
|
||||
return dayjs(date).format("MM/DD/YYYY hh:mm a");
|
||||
}
|
||||
export function TimeFormatter(props) {
|
||||
return props.children
|
||||
|
||||
@@ -12,6 +12,8 @@ import apolloLogger from "apollo-link-logger";
|
||||
//import axios from "axios";
|
||||
import { auth } from "../firebase/firebase.utils";
|
||||
import errorLink from "../graphql/apollo-error-handling";
|
||||
import { SentryLink } from "apollo-link-sentry";
|
||||
|
||||
//import { store } from "../redux/store";
|
||||
const httpLink = new HttpLink({
|
||||
uri: process.env.REACT_APP_GRAPHQL_ENDPOINT,
|
||||
@@ -105,18 +107,30 @@ const link = split(
|
||||
const authLink = setContext((_, { headers }) => {
|
||||
return (
|
||||
auth.currentUser &&
|
||||
auth.currentUser.getIdToken().then((token) => {
|
||||
if (token) {
|
||||
return {
|
||||
headers: {
|
||||
...headers,
|
||||
authorization: token ? `Bearer ${token}` : "",
|
||||
},
|
||||
};
|
||||
} else {
|
||||
auth.currentUser
|
||||
.getIdToken()
|
||||
.then((token) => {
|
||||
if (token) {
|
||||
return {
|
||||
headers: {
|
||||
...headers,
|
||||
authorization: token ? `Bearer ${token}` : "",
|
||||
},
|
||||
};
|
||||
} else {
|
||||
console.error(
|
||||
"Authentication error. Unable to add authorization token because it was empty."
|
||||
);
|
||||
return { headers };
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(
|
||||
"Authentication error. Unable to add authorization token.",
|
||||
error.message
|
||||
);
|
||||
return { headers };
|
||||
}
|
||||
})
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
@@ -138,8 +152,10 @@ if (process.env.NODE_ENV === "development") {
|
||||
}
|
||||
|
||||
middlewares.push(
|
||||
roundTripLink.concat(
|
||||
retryLink.concat(errorLink.concat(authLink.concat(link)))
|
||||
new SentryLink().concat(
|
||||
roundTripLink.concat(
|
||||
retryLink.concat(errorLink.concat(authLink.concat(link)))
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
16
client/src/utils/eulaize.js
Normal file
16
client/src/utils/eulaize.js
Normal 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('--------------------------------------------------')
|
||||
});
|
||||
Reference in New Issue
Block a user