- Merge and update

Signed-off-by: Dave Richer <dave@imexsystems.ca>
This commit is contained in:
Dave Richer
2024-01-30 19:37:52 -05:00
43 changed files with 3336 additions and 1306 deletions

View File

@@ -48,25 +48,16 @@ jobs:
steps:
- checkout:
path: ~/repo
- restore_cache:
name: Restore Yarn Package Cache
keys:
- yarn-packages-{{ checksum "yarn.lock" }}
- run:
name: Install Dependencies
command: yarn install --frozen-lockfile --cache-folder ~/.cache/yarn
- save_cache:
name: Save Yarn Package Cache
key: yarn-packages-{{ checksum "yarn.lock" }}
paths:
- ~/.cache/yarn
command: npm i
- run: yarn run build
- run: npm run build
- aws-s3/sync:
from: build
to: "s3://imex-online-production/"
arguments: "--exclude '*.map'"
- jira/notify
rome-api-deploy:
@@ -185,7 +176,7 @@ jobs:
app-beta-build:
docker:
- image: cimg/node:18.18.2
resource_class: large
resource_class: xlarge
working_directory: ~/repo/client
steps:
@@ -200,6 +191,7 @@ jobs:
- aws-s3/sync:
from: build
to: "s3://imex-online-beta/"
arguments: "--exclude '*.map'"
- jira/notify
rome-app-beta-build:
@@ -251,31 +243,23 @@ jobs:
steps:
- checkout:
path: ~/repo
- restore_cache:
name: Restore Yarn Package Cache
keys:
- yarn-packages-{{ checksum "yarn.lock" }}
- run:
name: Install Dependencies
command: yarn install --frozen-lockfile --cache-folder ~/.cache/yarn
- save_cache:
name: Save Yarn Package Cache
key: yarn-packages-{{ checksum "yarn.lock" }}
paths:
- ~/.cache/yarn
command: npm i
- run: yarn run build:test
- run: npm run build:test
- aws-s3/sync:
from: build
to: "s3://imex-online-test/"
arguments: "--exclude '*.map'"
- jira/notify
test-app-beta-build:
docker:
- image: cimg/node:18.18.2
resource_class: large
machine: true
resource_class: snaptsoft/dell
working_directory: ~/repo/client
steps:
@@ -291,6 +275,7 @@ jobs:
- aws-s3/sync:
from: build
to: "s3://imex-online-test-beta/"
arguments: "--exclude '*.map'"
- jira/notify
rome-test-app-beta-build:

View File

@@ -1,3 +1,4 @@
GENERATE_SOURCEMAP=true
REACT_APP_GRAPHQL_ENDPOINT=https://db.romeonline.io/v1/graphql
REACT_APP_GRAPHQL_ENDPOINT_WS=wss://db.romeonline.io/v1/graphql
REACT_APP_GA_CODE=231103507

3
client/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
# Sentry Config File
.sentryclirc

View File

@@ -1,7 +1,6 @@
// craco.config.js
const TerserPlugin = require("terser-webpack-plugin");
const CracoLessPlugin = require("craco-less");
const SentryWebpackPlugin = require("@sentry/webpack-plugin");
const {convertLegacyToken} = require('@ant-design/compatible/lib');
const {theme} = require('antd/lib');
@@ -10,52 +9,31 @@ const {defaultAlgorithm, defaultSeed} = theme;
const mapToken = defaultAlgorithm(defaultSeed);
const v4Token = convertLegacyToken(mapToken);
// TODO, At the moment we are using less in the Dashboard. Once we remove this we can remove the less processor entirely.
module.exports = {
plugins: [
{
plugin: SentryWebpackPlugin,
options: {
// sentry-cli configuration
authToken:
"6b45b028a02342db97a9a2f92c0959058665443d379d4a3a876430009e744260",
org: "snapt-software",
project: "rome-online",
release: process.env.REACT_APP_GIT_SHA,
// webpack-specific configuration
include: ".",
ignore: ["node_modules", "webpack.config.js"],
},
},
// {
// plugin: SentryWebpackPlugin,
// options: {
// // sentry-cli configuration
// authToken:
// "6b45b028a02342db97a9a2f92c0959058665443d379d4a3a876430009e744260",
// org: "snapt-software",
// project: "rome-online",
// release: process.env.REACT_APP_GIT_SHA,
//
// // webpack-specific configuration
// include: ".",
// ignore: ["node_modules", "webpack.config.js"],
// },
// },
{
plugin: CracoLessPlugin,
options: {
lessLoaderOptions: {
lessOptions: {
modifyVars: {
...v4Token,
// TODO: This will no longer work in AntD 5.0
...(process.env.NODE_ENV === "development"
? {"colorPrimary": "#B22234"}
: {
//"@primary-color": "#1DA57A"
}),
// "@primary-color": " #1890ff", // primary color for all components
// "@link-color": "#1890ff", // link color
// "@success-color": "#52c41a", // success state color
// "@warning-color": "#faad14", // warning state color
// "@error-color": "#f5222d", // error state color
// "@font-size-base": "14px", // major text font size
// " @heading-color": "rgba(0, 0, 0, 0.85)", // heading text color
// "@text-color": "rgba(0, 0, 0, 0.65)", // major text color
// "@text-color-secondary": "rgba(0, 0, 0, 0.45)", // secondary text color
// "@disabled-color": "rgba(0, 0, 0, 0.25)", // disable state color
// "@border-radius-base": "2px", // major border radius
// "@border-color-base": "#d9d9d9", // major border color
// "@box-shadow-base":
// "0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 6px 16px 0 rgba(0, 0, 0, 0.08),0 9px 28px 8px rgba(0, 0, 0, 0.05); // major shadow for layers }",
},
modifyVars: {...v4Token},
javascriptEnabled: true,
},
},

1761
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,36 +6,38 @@
"dependencies": {
"@ant-design/compatible": "^5.1.2",
"@ant-design/pro-layout": "^7.17.16",
"@apollo/client": "^3.8.10",
"@apollo/client": "^3.9.0",
"@asseinfo/react-kanban": "^2.2.0",
"@craco/craco": "^7.1.0",
"@fingerprintjs/fingerprintjs": "^4.2.1",
"@fingerprintjs/fingerprintjs": "^4.2.2",
"@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/cli": "^2.27.0",
"@sentry/react": "^7.99.0",
"@sentry/tracing": "^7.99.0",
"@splitsoftware/splitio-react": "^1.11.0",
"@tanem/react-nprogress": "^5.0.51",
"antd": "^5.12.8",
"antd": "^5.13.3",
"apollo-link-logger": "^2.0.1",
"axios": "^1.6.5",
"apollo-link-sentry": "^3.3.0",
"axios": "^1.6.7",
"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.1",
"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.8.1",
"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",
"markerjs2": "^2.32.0",
"normalize-url": "^8.0.0",
"phone": "^3.1.42",
"preval.macro": "^5.0.0",
@@ -44,17 +46,18 @@
"rc-queue-anim": "^2.0.0",
"rc-scroll-anim": "^2.7.6",
"react": "^18.2.0",
"react-big-calendar": "^1.8.6",
"react-big-calendar": "^1.8.7",
"react-color": "^2.19.3",
"react-cookie": "^7.0.1",
"react-cookie": "^7.0.2",
"react-dom": "^18.2.0",
"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",
@@ -63,7 +66,7 @@
"react-sticky": "^6.0.3",
"react-sublime-video": "^0.2.5",
"react-virtualized": "^9.22.5",
"recharts": "^2.10.4",
"recharts": "^2.11.0",
"redux": "^5.0.1",
"redux-persist": "^6.0.0",
"redux-saga": "^1.3.0",
@@ -74,7 +77,7 @@
"styled-components": "^6.1.8",
"subscriptions-transport-ws": "^0.11.0",
"terser-webpack-plugin": "^5.3.10",
"web-vitals": "^3.5.1",
"web-vitals": "^3.5.2",
"workbox-core": "^7.0.0",
"workbox-expiration": "^7.0.0",
"workbox-navigation-preload": "^7.0.0",
@@ -86,13 +89,15 @@
"scripts": {
"analyze": "source-map-explorer 'build/static/js/*.js'",
"start": "craco start",
"build": "REACT_APP_GIT_SHA=`git rev-parse --short HEAD` craco build",
"build": "REACT_APP_GIT_SHA=`git rev-parse --short HEAD` craco build && npm run sentry:sourcemaps",
"build:test": "env-cmd -f .env.test npm run build",
"build-deploy:test": "npm run build:test && s3cmd sync build/* s3://imex-online-test && echo '🚀 TESTING Deployed!'",
"buildcra": "REACT_APP_GIT_SHA=`git rev-parse --short HEAD` craco build",
"test": "cypress open",
"eject": "react-scripts eject",
"madge": "madge --image ./madge-graph.svg --extensions js,jsx,ts,tsx --circular ."
"madge": "madge --image ./madge-graph.svg --extensions js,jsx,ts,tsx --circular .",
"eulaize": "node src/utils/eulaize.js",
"sentry:sourcemaps": "sentry-cli sourcemaps inject --org imex --project imexonline ./build && sentry-cli sourcemaps upload --org imex --project imexonline ./build"
},
"eslintConfig": {
"extends": [
@@ -118,9 +123,9 @@
},
"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",
"cypress": "^13.6.4",
"eslint-plugin-cypress": "^2.15.1",
"react-error-overlay": "6.0.11",
"redux-logger": "^3.0.6",

View File

@@ -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);

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="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}/>}>

View File

@@ -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;

View 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;

View File

@@ -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))
);

View 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);

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

@@ -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}`}

View File

@@ -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>
</>

View File

@@ -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,
},
});

View File

@@ -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>
});

View File

@@ -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"}>

View File

@@ -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}>

View File

@@ -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}>

View File

@@ -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",

View File

@@ -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,

View File

@@ -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);
}
);

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

@@ -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

@@ -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,
});
}

View File

@@ -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;
};

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

@@ -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}}"

View File

@@ -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}}"

View File

@@ -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}}"

View File

@@ -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

View File

@@ -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)))
)
)
);

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

1669
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -18,21 +18,21 @@
"start": "node server.js"
},
"dependencies": {
"@aws-sdk/client-secrets-manager": "^3.490.0",
"@aws-sdk/client-ses": "^3.490.0",
"@aws-sdk/credential-provider-node": "^3.490.0",
"@aws-sdk/client-secrets-manager": "^3.503.0",
"@aws-sdk/client-ses": "^3.503.0",
"@aws-sdk/credential-provider-node": "^3.503.0",
"@opensearch-project/opensearch": "^2.5.0",
"aws4": "^1.12.0",
"axios": "^1.6.5",
"bluebird": "^3.7.2",
"body-parser": "^1.20.2",
"cloudinary": "^1.41.2",
"cloudinary": "^2.0.0",
"compression": "^1.7.4",
"cookie-parser": "^1.4.6",
"cors": "2.8.5",
"csrf": "^3.1.0",
"dinero.js": "^1.9.1",
"dotenv": "^16.3.1",
"dotenv": "^16.4.1",
"express": "^4.18.2",
"firebase-admin": "^12.0.0",
"graphql": "^16.8.1",
@@ -54,7 +54,7 @@
"rimraf": "^5.0.5",
"soap": "^1.0.0",
"socket.io": "^4.7.4",
"ssh2-sftp-client": "^9.1.0",
"ssh2-sftp-client": "^10.0.3",
"stripe": "^14.11.0",
"twilio": "^4.20.0",
"uuid": "^9.0.1",