Compare commits

..

1 Commits

Author SHA1 Message Date
Patrick Fic
92103df2a9 IO-2348 add http auth for media. 2023-07-11 15:46:58 -07:00
458 changed files with 19911 additions and 100269 deletions

View File

@@ -8,13 +8,13 @@ orbs:
jobs: jobs:
api-deploy: api-deploy:
docker: docker:
- image: cimg/node:18.18.2 - image: "cimg/base:stable"
steps: steps:
- checkout - checkout
- eb/setup - eb/setup
- run: - run:
command: | command: |
eb init imex-online-production-api -r ca-central-1 -p "Node.js 18 running on 64bit Amazon Linux 2" eb init imex-online-production-api -r ca-central-1 -p "Node.js 16 running on 64bit Amazon Linux 2"
eb status --verbose eb status --verbose
eb deploy eb deploy
eb status eb status
@@ -42,22 +42,31 @@ jobs:
app-build: app-build:
docker: docker:
- image: cimg/node:16.15.0 - image: cimg/node:16.15.0
resource_class: large
working_directory: ~/repo/client working_directory: ~/repo/client
steps: steps:
- checkout: - checkout:
path: ~/repo path: ~/repo
- restore_cache:
name: Restore Yarn Package Cache
keys:
- yarn-packages-{{ checksum "yarn.lock" }}
- run: - run:
name: Install Dependencies name: Install Dependencies
command: npm i 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
- run: npm run build - run: yarn run build
- aws-s3/sync: - aws-s3/sync:
from: build from: build
to: "s3://imex-online-production/" to: "s3://imex-online-production/"
arguments: "--exclude '*.map'"
- jira/notify - jira/notify
test-hasura-migrate: test-hasura-migrate:
@@ -83,22 +92,31 @@ jobs:
test-app-build: test-app-build:
docker: docker:
- image: cimg/node:16.15.0 - image: cimg/node:16.15.0
resource_class: large
working_directory: ~/repo/client working_directory: ~/repo/client
steps: steps:
- checkout: - checkout:
path: ~/repo path: ~/repo
- restore_cache:
name: Restore Yarn Package Cache
keys:
- yarn-packages-{{ checksum "yarn.lock" }}
- run: - run:
name: Install Dependencies name: Install Dependencies
command: npm i 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
- run: npm run build:test - run: yarn run build:test
- aws-s3/sync: - aws-s3/sync:
from: build from: build
to: "s3://imex-online-test/" to: "s3://imex-online-test/"
arguments: "--exclude '*.map'"
- jira/notify - jira/notify
admin-app-build: admin-app-build:
@@ -134,6 +152,10 @@ jobs:
workflows: workflows:
deploy_and_build: deploy_and_build:
jobs: jobs:
- api-deploy:
filters:
branches:
only: master
- app-build: - app-build:
filters: filters:
branches: branches:
@@ -155,4 +177,4 @@ workflows:
#- admin-app-build: #- admin-app-build:
#filters: #filters:
#branches: #branches:
#only: master #only: master

4
.gitignore vendored
View File

@@ -117,6 +117,4 @@ logs/oAuthClient-log.log
.node-persist/** .node-persist/**
/*.env.* /*.env.*
.idea/*
.idea

1
.npmrc
View File

@@ -1 +0,0 @@
legacy-peer-deps=true

View File

@@ -1,16 +0,0 @@
exports.default = {
printWidth: 120,
useTabs: false,
tabWidth: 2,
trailingComma: "es5",
semi: true,
singleQuote: false,
bracketSpacing: true,
arrowParens: "always",
jsxSingleQuote: false,
bracketSameLine: false,
endOfLine: "lf",
importOrder: ["^@core/(.*)$", "^@server/(.*)$", "^@ui/(.*)$", "^[./]"],
importOrderSeparation: true,
importOrderSortSpecifiers: true,
};

View File

@@ -1,189 +0,0 @@
# Filters and Sorters
This documentation details the schema required for `.filters` files on the report server. It is used to dynamically
modify the graphQL query and provide the user more power over their reports.
For filters and sorters, valid types include (`type` key in the schema):
- string (default)
- number
- bool or boolean
- date
## Special Notes
- When passing the data to the template server, the property filters and sorters is added to the data object and will reflect the filters and sorters the user has selected
## High level Schema Overview
```javascript
const schema = {
"filters": [
{
"name": "jobs.joblines.mod_lb_hrs", // Name and path of the field in the graphQL query
"translation": "jobs.joblines.mod_lb_hrs_1", // Translation key for the label used in the GUI
"label": "mod_lb_hrs_1", // Label used in the case the GUI does not contain a translation
"type": "number" // Type of field, can be number or string currently
},
// ... more filters
],
"sorters": [
{
"name": "jobs.joblines.mod_lb_hrs", // Name and path of the field in the graphQL query
"translation": "jobs.joblines.mod_lb_hrs_1", // Translation key for the label used in the GUI
"label": "mod_lb_hrs_1", // Label used in the case the GUI does not contain a translation
"type": "number" // Type of field, can be number or string currently
},
// ... more sorters
],
"dates": {
// This is not yet implemented and will be added in a future release
}
}
```
## Filters
Filters effect the where clause of the graphQL query. They are used to filter the data returned from the server.
A note on special notation used in the `name` field.
## Reflection
Filters can make use of reflection to pre-fill select boxes, the following is an example of that in the filters file.
```json
{
"name": "jobs.status",
"translation": "jobs.fields.status",
"label": "Status",
"type": "string",
"reflector": {
"type": "internal",
"name": "special.job_statuses"
}
}
```
in this example, a reflector with the type 'internal' (all types at the moment require this, and it is used for future functionality), with a name of `special.job_statuses`
The following cases are available
- `special.job_statuses` - This will reflect the statuses of the jobs table `bodyshop.md_ro_statuses.statuses'`
- `special.cost_centers` - This will reflect the cost centers `bodyshop.md_responsibility_centers.costs`
- `special.categories` - This will reflect the categories `bodyshop.md_categories`
- `special.insurance_companies` - This will reflect the insurance companies `bodyshop.md_ins_cos`'
- `special.employee_teams` - This will reflect the employee teams `bodyshop.employee_teams`
- `special.employees` - This will reflect the employees `bodyshop.employees`
- `special.first_names` - This will reflect the first names `bodyshop.employees`
- `special.last_names` - This will reflect the last names `bodyshop.employees`
- `special.referral_sources` - This will reflect the referral sources `bodyshop.md_referral_sources`
- `special.class`- This will reflect the class `bodyshop.md_classes`
- `special.lost_sale_reasons` - This will reflect the lost sale reasons `bodyshop.md_lost_sale_reasons`
- `special.alt_transports` - This will reflect the alternative transports `bodyshop.appt_alt_transport`
- `special.payment_types` - This will reflect the payment types `bodyshop.md_payment_types`
- `special.payment_payers` - This is a special case with a key value set of [Customer, Insurance]
### Path without brackets, multi level
`"name": "jobs.joblines.mod_lb_hrs",`
This will produce a where clause at the `joblines` level of the graphQL query,
```graphql
query gendoc_hours_sold_detail_open($starttz: timestamptz!, $endtz: timestamptz!) {
jobs(
where: {date_invoiced: {_is_null: true}, date_open: {_gte: $starttz, _lte: $endtz}, ro_number: {_is_null: false}, voided: {_eq: false}}
) {
joblines(
order_by: {line_no: asc}
where: {removed: {_eq: false}, mod_lb_hrs: {_lt: 3}}
) {
line_no
mod_lbr_ty
mod_lb_hrs
convertedtolbr
convertedtolbr_data
}
ownr_co_nm
ownr_fn
ownr_ln
plate_no
ro_number
status
v_make_desc
v_model_desc
v_model_yr
v_vin
v_color
}
}
```
### Path with brackets,top level
`"name": "[jobs].joblines.mod_lb_hrs",`
This will produce a where clause at the `jobs` level of the graphQL query.
```graphql
query gendoc_hours_sold_detail_open($starttz: timestamptz!, $endtz: timestamptz!) {
jobs(
where: {date_invoiced: {_is_null: true}, date_open: {_gte: $starttz, _lte: $endtz}, ro_number: {_is_null: false}, voided: {_eq: false}, joblines: {mod_lb_hrs: {_gt: 4}}}
) {
joblines(
order_by: {line_no: asc}
where: {removed: {_eq: false}}
) {
line_no
mod_lbr_ty
mod_lb_hrs
convertedtolbr
convertedtolbr_data
}
ownr_co_nm
ownr_fn
ownr_ln
plate_no
ro_number
status
v_make_desc
v_model_desc
v_model_yr
v_vin
v_color
}
}
```
## Known Caveats
- Will only support two level of nesting in the graphQL query `jobs.joblines.mod_lb_hrs` vs `[jobs].joblines.mod_lb_hrs`
is fine, but `jobs.[joblines.].some_table.mod_lb_hrs` is not.
- The type object must be 'string' or 'number' or 'bool' or 'boolean' or 'date' and is case-sensitive.
- The `translation` key is used to look up the label in the GUI, if it is not found, the `label` key is used.
- Do not add the ability to filter things that are already filtered as part of the original query, this would be
redundant and could cause issues.
- Do not add the ability to filter on things like FK constraints, must like the above example.
- *INHERITANCE CAVEAT* If you have a filters file on a parent report that has a child that you do not want the filters inherited from, you must place a blank filters file (valid json so {}) on the child report level. This will than fetch the child filters, which are empty and move along, versus inheriting the parent filters.
## Sorters
- Sorters follow the same schema as filters, however, they do not do square bracket wrapping to indicate level hoisting,
a filter added on `job.md_status` would be added at the top level, and a filter added on `jobs.joblines.mod_lb_hrs`
would be added at the `joblines` level.
- Most of the reports currently do sorting on a template level, this will need to change to actually see the results
using the sorters.
### Default Sorters
- A sorter can be given a default object containing a `order` and `direction` key value. This will be used to sort the report if the user does not select any of the sorters themselves.
- The `order` key is the order in which the sorters are applied, and the `direction` key is the direction of the sort, either `asc` or `desc`.
```json
{
"name": "jobs.joblines.mod_lb_hrs",
"translation": "jobs.joblines.mod_lb_hrs_1",
"label": "mod_lb_hrs_1",
"type": "number",
"default": {
"order": 1,
"direction": "asc"
}
}
```

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
GENERATE_SOURCEMAP=true GENERATE_SOURCEMAP=false
REACT_APP_GRAPHQL_ENDPOINT=https://db.imex.online/v1/graphql REACT_APP_GRAPHQL_ENDPOINT=https://db.imex.online/v1/graphql
REACT_APP_GRAPHQL_ENDPOINT_WS=wss://db.imex.online/v1/graphql REACT_APP_GRAPHQL_ENDPOINT_WS=wss://db.imex.online/v1/graphql
REACT_APP_GA_CODE=231103507 REACT_APP_GA_CODE=231103507

3
client/.gitignore vendored
View File

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

View File

@@ -1,25 +1,25 @@
// craco.config.js // craco.config.js
const TerserPlugin = require("terser-webpack-plugin"); const TerserPlugin = require("terser-webpack-plugin");
const CracoLessPlugin = require("craco-less"); const CracoLessPlugin = require("craco-less");
//const SentryWebpackPlugin = require("@sentry/webpack-plugin"); const SentryWebpackPlugin = require("@sentry/webpack-plugin");
module.exports = { module.exports = {
plugins: [ plugins: [
// { {
// plugin: SentryWebpackPlugin, plugin: SentryWebpackPlugin,
// options: { options: {
// // sentry-cli configuration // sentry-cli configuration
// authToken: authToken:
// "6b45b028a02342db97a9a2f92c0959058665443d379d4a3a876430009e744260", "6b45b028a02342db97a9a2f92c0959058665443d379d4a3a876430009e744260",
// org: "snapt-software", org: "snapt-software",
// project: "imexonline", project: "imexonline",
// release: process.env.REACT_APP_GIT_SHA, release: process.env.REACT_APP_GIT_SHA,
// // webpack-specific configuration // webpack-specific configuration
// include: ".", include: ".",
// ignore: ["node_modules", "webpack.config.js"], ignore: ["node_modules", "webpack.config.js"],
// }, },
// }, },
{ {
plugin: CracoLessPlugin, plugin: CracoLessPlugin,
options: { options: {

24741
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -7,16 +7,14 @@
"@apollo/client": "^3.7.9", "@apollo/client": "^3.7.9",
"@asseinfo/react-kanban": "^2.2.0", "@asseinfo/react-kanban": "^2.2.0",
"@craco/craco": "^7.0.0", "@craco/craco": "^7.0.0",
"@fingerprintjs/fingerprintjs": "^3.4.2", "@fingerprintjs/fingerprintjs": "^3.3.3",
"@jsreport/browser-client": "^3.1.0", "@jsreport/browser-client": "^3.1.0",
"@sentry/cli": "^2.27.0", "@sentry/react": "^7.40.0",
"@sentry/react": "^7.99.0",
"@sentry/tracing": "^7.40.0", "@sentry/tracing": "^7.40.0",
"@splitsoftware/splitio-react": "^1.8.1", "@splitsoftware/splitio-react": "^1.8.1",
"@tanem/react-nprogress": "^5.0.8", "@tanem/react-nprogress": "^5.0.8",
"antd": "^4.24.8", "antd": "^4.24.8",
"apollo-link-logger": "^2.0.1", "apollo-link-logger": "^2.0.1",
"apollo-link-sentry": "^3.3.0",
"axios": "^1.3.4", "axios": "^1.3.4",
"craco-less": "^2.0.0", "craco-less": "^2.0.0",
"dinero.js": "^1.9.1", "dinero.js": "^1.9.1",
@@ -90,14 +88,13 @@
"scripts": { "scripts": {
"analyze": "source-map-explorer 'build/static/js/*.js'", "analyze": "source-map-explorer 'build/static/js/*.js'",
"start": "craco start", "start": "craco start",
"build": "REACT_APP_GIT_SHA=`git rev-parse --short HEAD` craco build && npm run sentry:sourcemaps", "build": "REACT_APP_GIT_SHA=`git rev-parse --short HEAD` craco build",
"build:test": "env-cmd -f .env.test npm run build", "build:test": "env-cmd -f .env.test yarn run build",
"build-deploy:test": "npm run build:test && s3cmd sync build/* s3://imex-online-test && echo '🚀 TESTING Deployed!'", "build-deploy:test": "yarn 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", "buildcra": "REACT_APP_GIT_SHA=`git rev-parse --short HEAD` craco build",
"test": "cypress open", "test": "cypress open",
"eject": "react-scripts eject", "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 ."
"sentry:sourcemaps": "sentry-cli sourcemaps inject --org imex --project imexonline ./build && sentry-cli sourcemaps upload --org imex --project imexonline ./build"
}, },
"eslintConfig": { "eslintConfig": {
"extends": [ "extends": [

View File

@@ -8,7 +8,6 @@ import { useTranslation } from "react-i18next";
import GlobalLoadingBar from "../components/global-loading-bar/global-loading-bar.component"; import GlobalLoadingBar from "../components/global-loading-bar/global-loading-bar.component";
import client from "../utils/GraphQLClient"; import client from "../utils/GraphQLClient";
import App from "./App"; import App from "./App";
import * as Sentry from "@sentry/react";
moment.locale("en-US"); moment.locale("en-US");
@@ -19,7 +18,7 @@ export const factory = SplitSdk({
}, },
}); });
function AppContainer() { export default function AppContainer() {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
@@ -43,5 +42,3 @@ function AppContainer() {
</ApolloProvider> </ApolloProvider>
); );
} }
export default Sentry.withProfiler(AppContainer);

View File

@@ -16,34 +16,48 @@ import TechPageContainer from "../pages/tech/tech.page.container";
import { setOnline } from "../redux/application/application.actions"; import { setOnline } from "../redux/application/application.actions";
import { selectOnline } from "../redux/application/application.selectors"; import { selectOnline } from "../redux/application/application.selectors";
import { checkUserSession } from "../redux/user/user.actions"; import { checkUserSession } from "../redux/user/user.actions";
import { selectBodyshop, selectCurrentUser } from "../redux/user/user.selectors"; import {
selectBodyshop,
selectCurrentUser,
} from "../redux/user/user.selectors";
import PrivateRoute from "../utils/private-route"; import PrivateRoute from "../utils/private-route";
import "./App.styles.scss"; import "./App.styles.scss";
const ResetPassword = lazy(() => import("../pages/reset-password/reset-password.component")); const ResetPassword = lazy(() =>
import("../pages/reset-password/reset-password.component")
);
const ManagePage = lazy(() => import("../pages/manage/manage.page.container")); const ManagePage = lazy(() => import("../pages/manage/manage.page.container"));
const SignInPage = lazy(() => import("../pages/sign-in/sign-in.page")); const SignInPage = lazy(() => import("../pages/sign-in/sign-in.page"));
const CsiPage = lazy(() => import("../pages/csi/csi.container.page")); const CsiPage = lazy(() => import("../pages/csi/csi.container.page"));
const MobilePaymentContainer = lazy(() => import("../pages/mobile-payment/mobile-payment.container")); const MobilePaymentContainer = lazy(() =>
import("../pages/mobile-payment/mobile-payment.container")
);
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser, currentUser: selectCurrentUser,
online: selectOnline, online: selectOnline,
bodyshop: selectBodyshop bodyshop: selectBodyshop,
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
checkUserSession: () => dispatch(checkUserSession()), checkUserSession: () => dispatch(checkUserSession()),
setOnline: (isOnline) => dispatch(setOnline(isOnline)) setOnline: (isOnline) => dispatch(setOnline(isOnline)),
}); });
export function App({ bodyshop, checkUserSession, currentUser, online, setOnline }) { export function App({
bodyshop,
checkUserSession,
currentUser,
online,
setOnline,
}) {
const client = useClient(); const client = useClient();
useEffect(() => { useEffect(() => {
if (!navigator.onLine) { if (!navigator.onLine) {
setOnline(false); setOnline(false);
} }
checkUserSession(); checkUserSession();
}, [checkUserSession, setOnline]); }, [checkUserSession, setOnline]);
@@ -59,7 +73,6 @@ export function App({ bodyshop, checkUserSession, currentUser, online, setOnline
window.addEventListener("online", function (e) { window.addEventListener("online", function (e) {
setOnline(true); setOnline(true);
}); });
useEffect(() => { useEffect(() => {
if (currentUser.authorized && bodyshop) { if (currentUser.authorized && bodyshop) {
client.setAttribute("imexshopid", bodyshop.imexshopid); client.setAttribute("imexshopid", bodyshop.imexshopid);
@@ -113,16 +126,32 @@ export function App({ bodyshop, checkUserSession, currentUser, online, setOnline
<Route exact path="/disclaimer" component={DisclaimerPage} /> <Route exact path="/disclaimer" component={DisclaimerPage} />
</ErrorBoundary> </ErrorBoundary>
<ErrorBoundary> <ErrorBoundary>
<Route exact path="/mp/:paymentIs" component={MobilePaymentContainer} /> <Route
exact
path="/mp/:paymentIs"
component={MobilePaymentContainer}
/>
</ErrorBoundary> </ErrorBoundary>
<ErrorBoundary> <ErrorBoundary>
<PrivateRoute isAuthorized={currentUser.authorized} path="/manage" component={ManagePage} /> <PrivateRoute
isAuthorized={currentUser.authorized}
path="/manage"
component={ManagePage}
/>
</ErrorBoundary> </ErrorBoundary>
<ErrorBoundary> <ErrorBoundary>
<PrivateRoute isAuthorized={currentUser.authorized} path="/tech" component={TechPageContainer} /> <PrivateRoute
isAuthorized={currentUser.authorized}
path="/tech"
component={TechPageContainer}
/>
</ErrorBoundary> </ErrorBoundary>
<ErrorBoundary> <ErrorBoundary>
<PrivateRoute isAuthorized={currentUser.authorized} path="/edit" component={DocumentEditorContainer} /> <PrivateRoute
isAuthorized={currentUser.authorized}
path="/edit"
component={DocumentEditorContainer}
/>
</ErrorBoundary> </ErrorBoundary>
</Suspense> </Suspense>
</Switch> </Switch>

View File

@@ -0,0 +1,49 @@
import React from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { logImEXEvent } from "../../firebase/firebase.utils";
import { setEmailOptions } from "../../redux/email/email.actions";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { TemplateList } from "../../utils/TemplateConstants";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
setEmailOptions: (e) => dispatch(setEmailOptions(e)),
});
function Test({ bodyshop, setEmailOptions }) {
return (
<div>
<button
onClick={() => {
setEmailOptions({
messageOptions: {
to: ["patrickwf@gmail.com"],
replyTo: bodyshop.email,
},
template: {
name: TemplateList().parts_order.key,
variables: {
id: "a7c2d4e1-f519-42a9-a071-c48cf0f22979",
},
},
});
}}
>
send email
</button>
<button
onClick={() => {
logImEXEvent("IMEXEVENT", { somethignArThare: 5 });
}}
>
Log an ImEX Event.
</button>
</div>
);
}
export default connect(mapStateToProps, mapDispatchToProps)(Test);

View File

@@ -1,63 +0,0 @@
{
"status": 24201299,
"custid": 19607899,
"paymentid": 24201299,
"response": "A",
"authcode": "498680",
"declinereason": "Approved",
"fee": 0,
"invoice": "",
"account": "john",
"amount": 1000,
"amountincludesfee": false,
"total": 1000,
"paymenttype": "C",
"methodhint": "VI ***1111",
"cardbrand": "Visa",
"cardnumdisplay": "***1111",
"receiptelements": {
"authcode": "498680",
"cust_srv_ph_num": "1-555-555-5555",
"rcpt_pg_ftr_txt": "Thank You\nPlease Come Again",
"rcpt_currency": "USD",
"responsecode": "A",
"rcpt_pay_mthd": "Visa",
"transid": "C00 915799",
"merch_disp_nm": "CP Devel Test",
"rcpt_input_mthd": "Keyed",
"rcpt_pg_hdr_txt": "Welcome!",
"rcpt_tran_time": "Thursday February 23 2023, 11:25:36 pm +08",
"rcpt_trans_type": "Normal Transaction (Sale)",
"message": "Approved",
"rcpt_dba_addr": "1234 Storefront Ave\nSome City, UT 84111",
"avsdata": "N",
"receiptrequirements": "S",
"rcpt_cardnum": "************1111",
"cv2result": "M",
"rfnd_policy_txt": "<b>No Refunds</b>\nStore Credit Only",
"labels": {
"tranref": "REF#",
"tid": "TID",
"validationcode": "ValCode",
"emvapplicationid": "AID",
"emvatc": "ATC",
"rcpt_pay_mthd": "Pay Method",
"transid": "TransID",
"rcpt_input_mthd": "IMode",
"emvtsi": "TSI",
"emvac": "AC",
"rcpt_trans_type": "TranType",
"emvapplicationname": "PApp",
"visarewards": "RewardsProg"
}
},
"receipttoken": "H4sIAAAAAAAAACXMTQuCMBgA4P/ynh3tw_3dBI/ipQ8NOtRN53QiblpBRfTfCzo/8LwhxGAdZCCwFYoJJFQjI2kvHdGu74lVkgmrWyWNhASW5jW7cB87yHjKKePGJODnxrrnMl7dDTKmEJlSOqV/_N30XPpyj2Eddq57_KKZ8FLzmh_G1VQnVfhjiXGK1XYTc/h8AVOkf4qUAAAA",
"call": "card_payment",
"nonce": "488b5568-b5c1-4f38-8b2f-3b050f3abb11P",
"hmac": "JyPAJ9Yx0SlYBTtqns1OxAFRt+xF3l2UiLPO5zTDRBE=",
"paymentreferenceid": "C19607899P24201299",
"cardnum": "...1111",
"email": "",
"nameOnCard": "John Allen",
"cardType": "visa"
}

View File

@@ -0,0 +1,9 @@
import React from "react";
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
export default function Test() {
return (
<div>
<QboAuthorizeComponent />
</div>
);
}

View File

@@ -1,31 +0,0 @@
import { Button } from "antd";
import React from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { setModalContext } from "../../redux/modals/modals.actions";
const mapStateToProps = createStructuredSelector({});
const mapDispatchToProps = (dispatch) => ({
setRefundPaymentContext: (context) =>
dispatch(setModalContext({ context: context, modal: "refund_payment" })),
});
function Test({ setRefundPaymentContext, refundPaymentModal }) {
console.log("refundPaymentModal", refundPaymentModal);
return (
<div>
<Button
onClick={() =>
setRefundPaymentContext({
context: {},
})
}
>
Open Modal
</Button>
</div>
);
}
export default connect(mapStateToProps, mapDispatchToProps)(Test);

View File

@@ -1,21 +1,20 @@
import { Card, Checkbox, Input, Space, Table } from "antd"; import { Input, Table, Checkbox, Card, Space } from "antd";
import queryString from "query-string";
import React, { useState } from "react"; import React, { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { logImEXEvent } from "../../firebase/firebase.utils";
import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter"; import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { DateFormatter } from "../../utils/DateFormatter";
import { pageLimit } from "../../utils/config";
import { alphaSort, dateSort } from "../../utils/sorters"; import { alphaSort, dateSort } from "../../utils/sorters";
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
import PayableExportAll from "../payable-export-all-button/payable-export-all-button.component";
import PayableExportButton from "../payable-export-button/payable-export-button.component"; import PayableExportButton from "../payable-export-button/payable-export-button.component";
import BillMarkSelectedExported from "../payable-mark-selected-exported/payable-mark-selected-exported.component"; import PayableExportAll from "../payable-export-all-button/payable-export-all-button.component";
import { DateFormatter } from "../../utils/DateFormatter";
import queryString from "query-string";
import { logImEXEvent } from "../../firebase/firebase.utils";
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component"; import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
import BillMarkSelectedExported from "../payable-mark-selected-exported/payable-mark-selected-exported.component";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
@@ -138,6 +137,7 @@ export function AccountingPayablesTableComponent({
title: t("exportlogs.labels.attempts"), title: t("exportlogs.labels.attempts"),
dataIndex: "attempts", dataIndex: "attempts",
key: "attempts", key: "attempts",
render: (text, record) => ( render: (text, record) => (
<ExportLogsCountDisplay logs={record.exportlogs} /> <ExportLogsCountDisplay logs={record.exportlogs} />
), ),
@@ -146,6 +146,8 @@ export function AccountingPayablesTableComponent({
title: t("general.labels.actions"), title: t("general.labels.actions"),
dataIndex: "actions", dataIndex: "actions",
key: "actions", key: "actions",
sorter: (a, b) => a.clm_total - b.clm_total,
render: (text, record) => ( render: (text, record) => (
<PayableExportButton <PayableExportButton
billId={record.id} billId={record.id}
@@ -208,7 +210,7 @@ export function AccountingPayablesTableComponent({
<Table <Table
loading={loading} loading={loading}
dataSource={dataSource} dataSource={dataSource}
pagination={{ position: "top", pageSize: pageLimit }} pagination={{ position: "top", pageSize: 50 }}
columns={columns} columns={columns}
rowKey="id" rowKey="id"
onChange={handleTableChange} onChange={handleTableChange}

View File

@@ -8,12 +8,9 @@ import { logImEXEvent } from "../../firebase/firebase.utils";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter"; import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { DateFormatter, DateTimeFormatter } from "../../utils/DateFormatter"; import { DateFormatter, DateTimeFormatter } from "../../utils/DateFormatter";
import { pageLimit } from "../../utils/config";
import { alphaSort, dateSort } from "../../utils/sorters"; import { alphaSort, dateSort } from "../../utils/sorters";
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component"; import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
import OwnerNameDisplay, { import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
OwnerNameDisplayFunction,
} from "../owner-name-display/owner-name-display.component";
import PaymentExportButton from "../payment-export-button/payment-export-button.component"; import PaymentExportButton from "../payment-export-button/payment-export-button.component";
import PaymentMarkSelectedExported from "../payment-mark-selected-exported/payment-mark-selected-exported.component"; import PaymentMarkSelectedExported from "../payment-mark-selected-exported/payment-mark-selected-exported.component";
import PaymentsExportAllButton from "../payments-export-all-button/payments-export-all-button.component"; import PaymentsExportAllButton from "../payments-export-all-button/payments-export-all-button.component";
@@ -77,11 +74,7 @@ export function AccountingPayablesTableComponent({
dataIndex: "owner", dataIndex: "owner",
key: "owner", key: "owner",
ellipsis: true, ellipsis: true,
sorter: (a, b) => sorter: (a, b) => alphaSort(a.job.ownr_ln, b.job.ownr_ln),
alphaSort(
OwnerNameDisplayFunction(a.job),
OwnerNameDisplayFunction(b.job)
),
sortOrder: sortOrder:
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order, state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
render: (text, record) => { render: (text, record) => {
@@ -100,9 +93,6 @@ export function AccountingPayablesTableComponent({
title: t("payments.fields.amount"), title: t("payments.fields.amount"),
dataIndex: "amount", dataIndex: "amount",
key: "amount", key: "amount",
sorter: (a, b) => a.amount - b.amount,
sortOrder:
state.sortedInfo.columnKey === "amount" && state.sortedInfo.order,
render: (text, record) => ( render: (text, record) => (
<CurrencyFormatter>{record.amount}</CurrencyFormatter> <CurrencyFormatter>{record.amount}</CurrencyFormatter>
), ),
@@ -121,21 +111,18 @@ export function AccountingPayablesTableComponent({
title: t("payments.fields.created_at"), title: t("payments.fields.created_at"),
dataIndex: "created_at", dataIndex: "created_at",
key: "created_at", key: "created_at",
sorter: (a, b) => dateSort(a.created_at, b.created_at),
sortOrder:
state.sortedInfo.columnKey === "created_at" && state.sortedInfo.order,
render: (text, record) => ( render: (text, record) => (
<DateTimeFormatter>{record.created_at}</DateTimeFormatter> <DateTimeFormatter>{record.created_at}</DateTimeFormatter>
), ),
}, },
// { {
// title: t("payments.fields.exportedat"), title: t("payments.fields.exportedat"),
// dataIndex: "exportedat", dataIndex: "exportedat",
// key: "exportedat", key: "exportedat",
// render: (text, record) => ( render: (text, record) => (
// <DateTimeFormatter>{record.exportedat}</DateTimeFormatter> <DateTimeFormatter>{record.exportedat}</DateTimeFormatter>
// ), ),
// }, },
{ {
title: t("exportlogs.labels.attempts"), title: t("exportlogs.labels.attempts"),
dataIndex: "attempts", dataIndex: "attempts",
@@ -149,6 +136,8 @@ export function AccountingPayablesTableComponent({
title: t("general.labels.actions"), title: t("general.labels.actions"),
dataIndex: "actions", dataIndex: "actions",
key: "actions", key: "actions",
sorter: (a, b) => a.clm_total - b.clm_total,
render: (text, record) => ( render: (text, record) => (
<PaymentExportButton <PaymentExportButton
paymentId={record.id} paymentId={record.id}
@@ -220,7 +209,7 @@ export function AccountingPayablesTableComponent({
<Table <Table
loading={loading} loading={loading}
dataSource={dataSource} dataSource={dataSource}
pagination={{ position: "top", pageSize: pageLimit }} pagination={{ position: "top", pageSize: 50 }}
columns={columns} columns={columns}
rowKey="id" rowKey="id"
onChange={handleTableChange} onChange={handleTableChange}

View File

@@ -4,19 +4,17 @@ import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { logImEXEvent } from "../../firebase/firebase.utils"; import { logImEXEvent } from "../../firebase/firebase.utils";
import CurrencyFormatter from "../../utils/CurrencyFormatter"; import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { alphaSort, dateSort, statusSort } from "../../utils/sorters"; import { alphaSort, dateSort } from "../../utils/sorters";
import JobExportButton from "../jobs-close-export-button/jobs-close-export-button.component"; import JobExportButton from "../jobs-close-export-button/jobs-close-export-button.component";
import JobsExportAllButton from "../jobs-export-all-button/jobs-export-all-button.component"; import JobsExportAllButton from "../jobs-export-all-button/jobs-export-all-button.component";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import { DateFormatter } from "../../utils/DateFormatter";
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
import OwnerNameDisplay, {
OwnerNameDisplayFunction,
} from "../owner-name-display/owner-name-display.component";
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component"; import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
import { DateFormatter } from "../../utils/DateFormatter";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
@@ -65,7 +63,7 @@ export function AccountingReceivablesTableComponent({
title: t("jobs.fields.status"), title: t("jobs.fields.status"),
dataIndex: "status", dataIndex: "status",
key: "status", key: "status",
sorter: (a, b) => statusSort(a, b, bodyshop.md_ro_statuses.statuses), sorter: (a, b) => a.status - b.status,
sortOrder: sortOrder:
state.sortedInfo.columnKey === "status" && state.sortedInfo.order, state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
}, },
@@ -85,8 +83,7 @@ export function AccountingReceivablesTableComponent({
title: t("jobs.fields.owner"), title: t("jobs.fields.owner"),
dataIndex: "owner", dataIndex: "owner",
key: "owner", key: "owner",
sorter: (a, b) => sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
sortOrder: sortOrder:
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order, state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
render: (text, record) => { render: (text, record) => {
@@ -106,15 +103,6 @@ export function AccountingReceivablesTableComponent({
dataIndex: "vehicle", dataIndex: "vehicle",
key: "vehicle", key: "vehicle",
ellipsis: true, ellipsis: true,
sorter: (a, b) =>
alphaSort(
`${a.v_model_yr || ""} ${a.v_make_desc || ""} ${
a.v_model_desc || ""
}`,
`${b.v_model_yr || ""} ${b.v_make_desc || ""} ${b.v_model_desc || ""}`
),
sortOrder:
state.sortedInfo.columnKey === "vehicle" && state.sortedInfo.order,
render: (text, record) => { render: (text, record) => {
return record.vehicleid ? ( return record.vehicleid ? (
<Link to={"/manage/vehicles/" + record.vehicleid}> <Link to={"/manage/vehicles/" + record.vehicleid}>

View File

@@ -4,7 +4,6 @@ import { alphaSort } from "../../utils/sorters";
import { DateTimeFormatter } from "../../utils/DateFormatter"; import { DateTimeFormatter } from "../../utils/DateFormatter";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import AuditTrailValuesComponent from "../audit-trail-values/audit-trail-values.component"; import AuditTrailValuesComponent from "../audit-trail-values/audit-trail-values.component";
import {pageLimit} from "../../utils/config";
export default function AuditTrailListComponent({ loading, data }) { export default function AuditTrailListComponent({ loading, data }) {
const [state, setState] = useState({ const [state, setState] = useState({
@@ -75,7 +74,7 @@ export default function AuditTrailListComponent({ loading, data }) {
<Table <Table
{...formItemLayout} {...formItemLayout}
loading={loading} loading={loading}
pagination={{ position: "top", defaultPageSize: pageLimit }} pagination={{ position: "top", defaultPageSize: 25 }}
columns={columns} columns={columns}
rowKey="id" rowKey="id"
dataSource={data} dataSource={data}

View File

@@ -3,7 +3,6 @@ import React, { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { DateTimeFormatter } from "../../utils/DateFormatter"; import { DateTimeFormatter } from "../../utils/DateFormatter";
import { alphaSort } from "../../utils/sorters"; import { alphaSort } from "../../utils/sorters";
import {pageLimit} from "../../utils/config";
export default function EmailAuditTrailListComponent({ loading, data }) { export default function EmailAuditTrailListComponent({ loading, data }) {
const [state, setState] = useState({ const [state, setState] = useState({
@@ -54,7 +53,7 @@ export default function EmailAuditTrailListComponent({ loading, data }) {
<Table <Table
{...formItemLayout} {...formItemLayout}
loading={loading} loading={loading}
pagination={{ position: "top", defaultPageSize: pageLimit }} pagination={{ position: "top", defaultPageSize: 25 }}
columns={columns} columns={columns}
rowKey="id" rowKey="id"
dataSource={data} dataSource={data}

View File

@@ -3,22 +3,10 @@ import { useMutation } from "@apollo/client";
import { Button, notification, Popconfirm } from "antd"; import { Button, notification, Popconfirm } from "antd";
import React, { useState } from "react"; import React, { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { DELETE_BILL } from "../../graphql/bills.queries"; import { DELETE_BILL } from "../../graphql/bills.queries";
import { insertAuditTrail } from "../../redux/application/application.actions";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component"; import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component";
const mapStateToProps = createStructuredSelector({}); export default function BillDeleteButton({ bill, callback }) {
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(insertAuditTrail({ jobid, operation, type })),
});
export default connect(mapStateToProps, mapDispatchToProps)(BillDeleteButton);
export function BillDeleteButton({ bill, jobid, callback, insertAuditTrail }) {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const { t } = useTranslation(); const { t } = useTranslation();
const [deleteBill] = useMutation(DELETE_BILL); const [deleteBill] = useMutation(DELETE_BILL);
@@ -48,11 +36,6 @@ export function BillDeleteButton({ bill, jobid, callback, insertAuditTrail }) {
if (!!!result.errors) { if (!!!result.errors) {
notification["success"]({ message: t("bills.successes.deleted") }); notification["success"]({ message: t("bills.successes.deleted") });
insertAuditTrail({
jobid: jobid,
operation: AuditTrailMapping.billdeleted(bill.invoice_number),
type: "billdeleted",
});
if (callback && typeof callback === "function") callback(bill.id); if (callback && typeof callback === "function") callback(bill.id);
} else { } else {

View File

@@ -1,5 +1,5 @@
import { useMutation, useQuery } from "@apollo/client"; import { useMutation, useQuery } from "@apollo/client";
import { Button, Divider, Form, PageHeader, Popconfirm, Space } from "antd"; import { Button, Form, PageHeader, Popconfirm, Space } from "antd";
import moment from "moment"; import moment from "moment";
import queryString from "query-string"; import queryString from "query-string";
import React, { useState } from "react"; import React, { useState } from "react";
@@ -10,7 +10,7 @@ import { createStructuredSelector } from "reselect";
import { import {
DELETE_BILL_LINE, DELETE_BILL_LINE,
INSERT_NEW_BILL_LINES, INSERT_NEW_BILL_LINES,
UPDATE_BILL_LINE, UPDATE_BILL_LINE
} from "../../graphql/bill-lines.queries"; } from "../../graphql/bill-lines.queries";
import { QUERY_BILL_BY_PK, UPDATE_BILL } from "../../graphql/bills.queries"; import { QUERY_BILL_BY_PK, UPDATE_BILL } from "../../graphql/bills.queries";
import { insertAuditTrail } from "../../redux/application/application.actions"; import { insertAuditTrail } from "../../redux/application/application.actions";
@@ -20,7 +20,6 @@ import AuditTrailMapping from "../../utils/AuditTrailMappings";
import AlertComponent from "../alert/alert.component"; import AlertComponent from "../alert/alert.component";
import BillFormContainer from "../bill-form/bill-form.container"; import BillFormContainer from "../bill-form/bill-form.container";
import BillMarkExportedButton from "../bill-mark-exported-button/bill-mark-exported-button.component"; import BillMarkExportedButton from "../bill-mark-exported-button/bill-mark-exported-button.component";
import BillPrintButton from "../bill-print-button/bill-print-button.component";
import BillReeportButtonComponent from "../bill-reexport-button/bill-reexport-button.component"; import BillReeportButtonComponent from "../bill-reexport-button/bill-reexport-button.component";
import JobDocumentsGallery from "../jobs-documents-gallery/jobs-documents-gallery.container"; import JobDocumentsGallery from "../jobs-documents-gallery/jobs-documents-gallery.container";
import JobsDocumentsLocalGallery from "../jobs-documents-local-gallery/jobs-documents-local-gallery.container"; import JobsDocumentsLocalGallery from "../jobs-documents-local-gallery/jobs-documents-local-gallery.container";
@@ -33,8 +32,8 @@ const mapStateToProps = createStructuredSelector({
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
setPartsOrderContext: (context) => setPartsOrderContext: (context) =>
dispatch(setModalContext({ context: context, modal: "partsOrder" })), dispatch(setModalContext({ context: context, modal: "partsOrder" })),
insertAuditTrail: ({ jobid, operation, type }) => insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation, type })), dispatch(insertAuditTrail({ jobid, operation })),
}); });
export default connect( export default connect(
@@ -150,7 +149,6 @@ export function BillDetailEditcontainer({
jobid: bill.jobid, jobid: bill.jobid,
billid: search.billid, billid: search.billid,
operation: AuditTrailMapping.billupdated(bill.invoice_number), operation: AuditTrailMapping.billupdated(bill.invoice_number),
type: "billupdated",
}); });
await refetch(); await refetch();
@@ -164,7 +162,6 @@ export function BillDetailEditcontainer({
if (!search.billid) return <></>; //<div>{t("bills.labels.noneselected")}</div>; if (!search.billid) return <></>; //<div>{t("bills.labels.noneselected")}</div>;
const exported = data && data.bills_by_pk && data.bills_by_pk.exported; const exported = data && data.bills_by_pk && data.bills_by_pk.exported;
const isinhouse = data && data.bills_by_pk && data.bills_by_pk.isinhouse;
return ( return (
<> <>
@@ -179,7 +176,7 @@ export function BillDetailEditcontainer({
extra={ extra={
<Space> <Space>
<BillDetailEditReturn data={data} /> <BillDetailEditReturn data={data} />
<BillPrintButton billid={search.billid} />
<Popconfirm <Popconfirm
visible={visible} visible={visible}
onConfirm={() => form.submit()} onConfirm={() => form.submit()}
@@ -208,8 +205,8 @@ export function BillDetailEditcontainer({
initialValues={transformData(data)} initialValues={transformData(data)}
layout="vertical" layout="vertical"
> >
<BillFormContainer form={form} billEdit disabled={exported} disableInHouse={isinhouse}/> <BillFormContainer form={form} billEdit disabled={exported} />
<Divider orientation="left">{t("general.labels.media")}</Divider>
{bodyshop.uselocalmediaserver ? ( {bodyshop.uselocalmediaserver ? (
<JobsDocumentsLocalGallery <JobsDocumentsLocalGallery
job={{ id: data ? data.bills_by_pk.jobid : null }} job={{ id: data ? data.bills_by_pk.jobid : null }}

View File

@@ -16,8 +16,8 @@ const mapStateToProps = createStructuredSelector({
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
setPartsOrderContext: (context) => setPartsOrderContext: (context) =>
dispatch(setModalContext({ context: context, modal: "partsOrder" })), dispatch(setModalContext({ context: context, modal: "partsOrder" })),
insertAuditTrail: ({ jobid, operation, type }) => insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation, type })), dispatch(insertAuditTrail({ jobid, operation })),
}); });
export default connect( export default connect(
@@ -45,7 +45,6 @@ export function BillDetailEditReturn({
actions: {}, actions: {},
context: { context: {
jobId: data.bills_by_pk.jobid, jobId: data.bills_by_pk.jobid,
job: data.bills_by_pk.job,
vendorId: data.bills_by_pk.vendorid, vendorId: data.bills_by_pk.vendorid,
returnFromBill: data.bills_by_pk.id, returnFromBill: data.bills_by_pk.id,
invoiceNumber: data.bills_by_pk.invoice_number, invoiceNumber: data.bills_by_pk.invoice_number,
@@ -174,11 +173,7 @@ export function BillDetailEditReturn({
</Form> </Form>
</Modal> </Modal>
<Button <Button
disabled={ disabled={data.bills_by_pk.is_credit_memo || disabled}
data.bills_by_pk.is_credit_memo ||
data.bills_by_pk.isinhouse ||
disabled
}
onClick={() => { onClick={() => {
setVisible(true); setVisible(true);
}} }}

View File

@@ -37,8 +37,8 @@ const mapStateToProps = createStructuredSelector({
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
toggleModalVisible: () => dispatch(toggleModalVisible("billEnter")), toggleModalVisible: () => dispatch(toggleModalVisible("billEnter")),
insertAuditTrail: ({ jobid, billid, operation, type }) => insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, billid, operation, type })), dispatch(insertAuditTrail({ jobid, operation })),
}); });
const Templates = TemplateList("job_special"); const Templates = TemplateList("job_special");
@@ -94,7 +94,6 @@ function BillEnterModalContainer({
location, location,
outstanding_returns, outstanding_returns,
inventory, inventory,
federal_tax_exempt,
...remainingValues ...remainingValues
} = values; } = values;
@@ -144,7 +143,7 @@ function BillEnterModalContainer({
}, },
], ],
}, },
refetchQueries: ["QUERY_PARTS_BILLS_BY_JOBID", "GET_JOB_BY_PK"], refetchQueries: ["QUERY_PARTS_BILLS_BY_JOBID"],
}); });
const adjKeys = Object.keys(adjustmentsToInsert); const adjKeys = Object.keys(adjustmentsToInsert);
@@ -171,7 +170,6 @@ function BillEnterModalContainer({
mod_lbr_ty: key, mod_lbr_ty: key,
hours: adjustmentsToInsert[key].toFixed(1), hours: adjustmentsToInsert[key].toFixed(1),
}), }),
type: "jobmodifylbradj",
}); });
}); });
@@ -318,10 +316,7 @@ function BillEnterModalContainer({
insertAuditTrail({ insertAuditTrail({
jobid: values.jobid, jobid: values.jobid,
billid: billId, billid: billId,
operation: AuditTrailMapping.billposted( operation: AuditTrailMapping.billposted(remainingValues.invoice_number),
r1.data.insert_bills.returning[0].invoice_number
),
type: "billposted",
}); });
if (enterAgain) { if (enterAgain) {

View File

@@ -1,6 +1,6 @@
import Icon, { UploadOutlined } from "@ant-design/icons"; import Icon, { UploadOutlined } from "@ant-design/icons";
import { useApolloClient } from "@apollo/client"; import { useApolloClient } from "@apollo/client";
import { useTreatments } from "@splitsoftware/splitio-react"; import { MdOpenInNew } from "react-icons/md";
import { import {
Alert, Alert,
Divider, Divider,
@@ -12,17 +12,14 @@ import {
Switch, Switch,
Upload, Upload,
} from "antd"; } from "antd";
import moment from "moment";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { MdOpenInNew } from "react-icons/md";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { CHECK_BILL_INVOICE_NUMBER } from "../../graphql/bills.queries"; import { CHECK_BILL_INVOICE_NUMBER } from "../../graphql/bills.queries";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import AlertComponent from "../alert/alert.component"; import AlertComponent from "../alert/alert.component";
import BillFormLinesExtended from "../bill-form-lines-extended/bill-form-lines-extended.component";
import FormDatePicker from "../form-date-picker/form-date-picker.component"; import FormDatePicker from "../form-date-picker/form-date-picker.component";
import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component"; import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
import CurrencyInput from "../form-items-formatted/currency-form-item.component"; import CurrencyInput from "../form-items-formatted/currency-form-item.component";
@@ -31,6 +28,8 @@ import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import VendorSearchSelect from "../vendor-search-select/vendor-search-select.component"; import VendorSearchSelect from "../vendor-search-select/vendor-search-select.component";
import BillFormLines from "./bill-form.lines.component"; import BillFormLines from "./bill-form.lines.component";
import { CalculateBillTotal } from "./bill-form.totals.utility"; import { CalculateBillTotal } from "./bill-form.totals.utility";
import { useTreatments } from "@splitsoftware/splitio-react";
import BillFormLinesExtended from "../bill-form-lines-extended/bill-form-lines-extended.component";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
@@ -47,11 +46,9 @@ export function BillFormComponent({
loadLines, loadLines,
billEdit, billEdit,
disableInvNumber, disableInvNumber,
disableInHouse,
job, job,
loadOutstandingReturns, loadOutstandingReturns,
loadInventory, loadInventory,
preferredMake,
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const client = useApolloClient(); const client = useApolloClient();
@@ -61,11 +58,6 @@ export function BillFormComponent({
{}, {},
bodyshop.imexshopid bodyshop.imexshopid
); );
const { ClosingPeriod } = useTreatments(
["ClosingPeriod"],
{},
bodyshop.imexshopid
);
const handleVendorSelect = (props, opt) => { const handleVendorSelect = (props, opt) => {
setDiscount(opt.discount); setDiscount(opt.discount);
@@ -80,20 +72,6 @@ export function BillFormComponent({
}); });
}; };
const handleFederalTaxExemptSwitchToggle = (checked) => {
// Early gate
if (!checked) return;
const values = form.getFieldsValue("billlines");
// Gate bill lines
if (!values?.billlines?.length) return;
const billlines = values.billlines.map((b) => {
b.applicable_taxes.federal = false;
return b;
});
form.setFieldsValue({ billlines });
};
useEffect(() => { useEffect(() => {
if (job) form.validateFields(["is_credit_memo"]); if (job) form.validateFields(["is_credit_memo"]);
}, [job, form]); }, [job, form]);
@@ -199,9 +177,8 @@ export function BillFormComponent({
]} ]}
> >
<VendorSearchSelect <VendorSearchSelect
disabled={disabled || disableInHouse} disabled={disabled}
options={vendorAutoCompleteOptions} options={vendorAutoCompleteOptions}
preferredMake={preferredMake}
onSelect={handleVendorSelect} onSelect={handleVendorSelect}
/> />
</Form.Item> </Form.Item>
@@ -272,7 +249,7 @@ export function BillFormComponent({
}), }),
]} ]}
> >
<Input disabled={disabled || disableInvNumber || disableInHouse} /> <Input disabled={disabled || disableInvNumber} />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label={t("bills.fields.date")} label={t("bills.fields.date")}
@@ -282,37 +259,6 @@ export function BillFormComponent({
required: true, required: true,
//message: t("general.validation.required"), //message: t("general.validation.required"),
}, },
({ getFieldValue }) => ({
validator(rule, value) {
if (
ClosingPeriod.treatment === "on" &&
bodyshop.accountingconfig.ClosingPeriod
) {
if (
moment(value)
.startOf("day")
.isSameOrAfter(
moment(
bodyshop.accountingconfig.ClosingPeriod[0]
).startOf("day")
) &&
moment(value)
.startOf("day")
.isSameOrBefore(
moment(
bodyshop.accountingconfig.ClosingPeriod[1]
).endOf("day")
)
) {
return Promise.resolve();
} else {
return Promise.reject(t("bills.validation.closingperiod"));
}
} else {
return Promise.resolve();
}
},
}),
]} ]}
> >
<FormDatePicker disabled={disabled} /> <FormDatePicker disabled={disabled} />
@@ -402,16 +348,7 @@ export function BillFormComponent({
> >
<CurrencyInput min={0} /> <CurrencyInput min={0} />
</Form.Item> </Form.Item>
{bodyshop.pbs_serialnumber || bodyshop.cdk_dealerid ? ( <Form.Item shouldUpdate span={15}>
<Form.Item
span={2}
label={t("bills.labels.federal_tax_exempt")}
name="federal_tax_exempt"
>
<Switch onChange={handleFederalTaxExemptSwitchToggle} />
</Form.Item>
) : null}
<Form.Item shouldUpdate span={13}>
{() => { {() => {
const values = form.getFieldsValue([ const values = form.getFieldsValue([
"billlines", "billlines",
@@ -429,7 +366,7 @@ export function BillFormComponent({
totals = CalculateBillTotal(values); totals = CalculateBillTotal(values);
if (!!totals) if (!!totals)
return ( return (
<div align="right"> <div>
<Space wrap> <Space wrap>
<Statistic <Statistic
title={t("bills.labels.subtotal")} title={t("bills.labels.subtotal")}
@@ -505,11 +442,10 @@ export function BillFormComponent({
billEdit={billEdit} billEdit={billEdit}
/> />
)} )}
<Divider orientation="left" style={{ display: billEdit ? "none" : null }}>
{t("documents.labels.upload")}
</Divider>
<Form.Item <Form.Item
name="upload" name="upload"
label="Upload"
style={{ display: billEdit ? "none" : null }} style={{ display: billEdit ? "none" : null }}
valuePropName="fileList" valuePropName="fileList"
getValueFromEvent={(e) => { getValueFromEvent={(e) => {

View File

@@ -1,16 +1,16 @@
import { useLazyQuery, useQuery } from "@apollo/client"; import { useLazyQuery, useQuery } from "@apollo/client";
import { useTreatments } from "@splitsoftware/splitio-react";
import React from "react"; import React from "react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { QUERY_OUTSTANDING_INVENTORY } from "../../graphql/inventory.queries";
import { GET_JOB_LINES_TO_ENTER_BILL } from "../../graphql/jobs-lines.queries"; import { GET_JOB_LINES_TO_ENTER_BILL } from "../../graphql/jobs-lines.queries";
import { QUERY_UNRECEIVED_LINES } from "../../graphql/parts-orders.queries";
import { SEARCH_VENDOR_AUTOCOMPLETE } from "../../graphql/vendors.queries"; import { SEARCH_VENDOR_AUTOCOMPLETE } from "../../graphql/vendors.queries";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import BillCmdReturnsTableComponent from "../bill-cm-returns-table/bill-cm-returns-table.component";
import BillInventoryTable from "../bill-inventory-table/bill-inventory-table.component";
import BillFormComponent from "./bill-form.component"; import BillFormComponent from "./bill-form.component";
import BillCmdReturnsTableComponent from "../bill-cm-returns-table/bill-cm-returns-table.component";
import { QUERY_UNRECEIVED_LINES } from "../../graphql/parts-orders.queries";
import BillInventoryTable from "../bill-inventory-table/bill-inventory-table.component";
import { QUERY_OUTSTANDING_INVENTORY } from "../../graphql/inventory.queries";
import { useTreatments } from "@splitsoftware/splitio-react";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
@@ -22,7 +22,6 @@ export function BillFormContainer({
billEdit, billEdit,
disabled, disabled,
disableInvNumber, disableInvNumber,
disableInHouse
}) { }) {
const { Simple_Inventory } = useTreatments( const { Simple_Inventory } = useTreatments(
["Simple_Inventory"], ["Simple_Inventory"],
@@ -58,10 +57,8 @@ export function BillFormContainer({
job={lineData ? lineData.jobs_by_pk : null} job={lineData ? lineData.jobs_by_pk : null}
responsibilityCenters={bodyshop.md_responsibility_centers || null} responsibilityCenters={bodyshop.md_responsibility_centers || null}
disableInvNumber={disableInvNumber} disableInvNumber={disableInvNumber}
disableInHouse={disableInHouse}
loadOutstandingReturns={loadOutstandingReturns} loadOutstandingReturns={loadOutstandingReturns}
loadInventory={loadInventory} loadInventory={loadInventory}
preferredMake={lineData ? lineData.jobs_by_pk.v_make_desc : null}
/> />
{!billEdit && ( {!billEdit && (
<BillCmdReturnsTableComponent <BillCmdReturnsTableComponent

View File

@@ -1,15 +1,14 @@
import { DeleteFilled, DollarCircleFilled } from "@ant-design/icons"; import { DeleteFilled, DollarCircleFilled } from "@ant-design/icons";
import { useTreatments } from "@splitsoftware/splitio-react"; import { useTreatments } from "@splitsoftware/splitio-react";
import { import {
Button, Button, Form,
Form,
Input, Input,
InputNumber, InputNumber,
Select, Select,
Space, Space,
Switch, Switch,
Table, Table,
Tooltip, Tooltip
} from "antd"; } from "antd";
import React from "react"; import React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -467,8 +466,7 @@ export function BillEnterModalLinesComponent({
return { return {
key: `${field.index}fedtax`, key: `${field.index}fedtax`,
valuePropName: "checked", valuePropName: "checked",
initialValue: initialValue: true,
form.getFieldValue("federal_tax_exempt") === true ? false : true,
name: [field.name, "applicable_taxes", "federal"], name: [field.name, "applicable_taxes", "federal"],
}; };
}, },

View File

@@ -1,38 +0,0 @@
import { Button, Space } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { GenerateDocument } from "../../utils/RenderTemplate";
import { TemplateList } from "../../utils/TemplateConstants";
export default function BillPrintButton({ billid }) {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const Templates = TemplateList("job_special");
const submitHandler = async () => {
setLoading(true);
try {
await GenerateDocument(
{
name: Templates.parts_invoice_label_single.key,
variables: {
id: billid,
},
},
{},
"p"
);
} catch (e) {
console.warn("Warning: Error generating a document.");
}
setLoading(false);
};
return (
<Space wrap>
<Button loading={loading} onClick={submitHandler}>
{t("bills.labels.printlabels")}
</Button>
</Space>
);
}

View File

@@ -9,8 +9,8 @@ import { setModalContext } from "../../redux/modals/modals.actions";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter"; import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { DateFormatter } from "../../utils/DateFormatter"; import { DateFormatter } from "../../utils/DateFormatter";
import { TemplateList } from "../../utils/TemplateConstants";
import { alphaSort, dateSort } from "../../utils/sorters"; import { alphaSort, dateSort } from "../../utils/sorters";
import { TemplateList } from "../../utils/TemplateConstants";
import BillDeleteButton from "../bill-delete-button/bill-delete-button.component"; import BillDeleteButton from "../bill-delete-button/bill-delete-button.component";
import BillDetailEditReturnComponent from "../bill-detail-edit/bill-detail-edit-return.component"; import BillDetailEditReturnComponent from "../bill-detail-edit/bill-detail-edit-return.component";
import PrintWrapperComponent from "../print-wrapper/print-wrapper.component"; import PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
@@ -58,9 +58,9 @@ export function BillsListTableComponent({
<EditFilled /> <EditFilled />
</Button> </Button>
)} )}
<BillDeleteButton bill={record} jobid={job.id} /> <BillDeleteButton bill={record} />
<BillDetailEditReturnComponent <BillDetailEditReturnComponent
data={{ bills_by_pk: { ...record, jobid: job.id, job: job } }} data={{ bills_by_pk: { ...record, jobid: job.id } }}
disabled={ disabled={
record.is_credit_memo || record.is_credit_memo ||
record.vendorid === bodyshop.inhousevendorid || record.vendorid === bodyshop.inhousevendorid ||

View File

@@ -1,8 +1,8 @@
import { CalculatorFilled } from "@ant-design/icons";
import { Button, Form, InputNumber, Popover, Space } from "antd";
import React, { useState } from "react"; import React, { useState } from "react";
import { useTranslation } from "react-i18next"; import { Button, Form, InputNumber, Popover } from "antd";
import { logImEXEvent } from "../../firebase/firebase.utils"; import { logImEXEvent } from "../../firebase/firebase.utils";
import { useTranslation } from "react-i18next";
import { CalculatorFilled } from "@ant-design/icons";
export default function CABCpvrtCalculator({ disabled, form }) { export default function CABCpvrtCalculator({ disabled, form }) {
const [visibility, setVisibility] = useState(false); const [visibility, setVisibility] = useState(false);
@@ -26,14 +26,10 @@ export default function CABCpvrtCalculator({ disabled, form }) {
<Form.Item name="days" label={t("jobs.labels.ca_bc_pvrt.days")}> <Form.Item name="days" label={t("jobs.labels.ca_bc_pvrt.days")}>
<InputNumber precision={0} min={0} /> <InputNumber precision={0} min={0} />
</Form.Item> </Form.Item>
<div style={{ display: "flex", justifyContent: "flex-end" }}> <Button type="primary" htmlType="submit">
<Space> {t("general.actions.calculate")}
<Button type="primary" htmlType="submit"> </Button>
{t("general.actions.calculate")} <Button onClick={() => setVisibility(false)}>Close</Button>
</Button>
<Button onClick={() => setVisibility(false)}>Close</Button>
</Space>
</div>
</Form> </Form>
</div> </div>
); );

View File

@@ -1,328 +0,0 @@
import { DeleteFilled } from "@ant-design/icons";
import { useLazyQuery, useMutation } from "@apollo/client";
import {
Button,
Card,
Col,
Form,
Input,
Row,
Space,
Spin,
Statistic,
notification,
} from "antd";
import axios from "axios";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import {
INSERT_PAYMENT_RESPONSE,
QUERY_RO_AND_OWNER_BY_JOB_PKS,
} from "../../graphql/payment_response.queries";
import { insertAuditTrail } from "../../redux/application/application.actions";
import { toggleModalVisible } from "../../redux/modals/modals.actions";
import { selectCardPayment } from "../../redux/modals/modals.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import CurrencyFormItemComponent from "../form-items-formatted/currency-form-item.component";
import JobSearchSelectComponent from "../job-search-select/job-search-select.component";
const mapStateToProps = createStructuredSelector({
cardPaymentModal: selectCardPayment,
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(insertAuditTrail({ jobid, operation, type })),
toggleModalVisible: () => dispatch(toggleModalVisible("cardPayment")),
});
const CardPaymentModalComponent = ({
bodyshop,
cardPaymentModal,
toggleModalVisible,
insertAuditTrail,
}) => {
const { context, actions } = cardPaymentModal;
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
// const [insertPayment] = useMutation(INSERT_NEW_PAYMENT);
const [insertPaymentResponse] = useMutation(INSERT_PAYMENT_RESPONSE);
const { t } = useTranslation();
const [, { data, refetch, queryLoading }] = useLazyQuery(
QUERY_RO_AND_OWNER_BY_JOB_PKS,
{
variables: { jobids: [context.jobid] },
skip: true,
}
);
//Initialize the intellipay window.
const SetIntellipayCallbackFunctions = () => {
console.log("*** Set IntelliPay callback functions.");
window.intellipay.runOnClose(() => {
//window.intellipay.initialize();
});
window.intellipay.runOnApproval(async function (response) {
//2024-04-25: Nothing is going to happen here anymore. We'll completely rely on the callback.
//Add a slight delay to allow the refetch to properly get the data.
setTimeout(() => {
if (actions && actions.refetch && typeof actions.refetch === "function")
actions.refetch();
setLoading(false);
toggleModalVisible();
}, 750);
});
window.intellipay.runOnNonApproval(async function (response) {
// Mutate unsuccessful payment
const { payments } = form.getFieldsValue();
await insertPaymentResponse({
variables: {
paymentResponse: payments.map((payment) => ({
amount: payment.amount,
bodyshopid: bodyshop.id,
jobid: payment.jobid,
declinereason: response.declinereason,
ext_paymentid: response.paymentid.toString(),
successful: false,
response,
})),
},
});
payments.forEach((payment) =>
insertAuditTrail({
jobid: payment.jobid,
operation: AuditTrailMapping.failedpayment(),
type: "failedpayment",
})
);
});
};
const handleIntelliPayCharge = async () => {
setLoading(true);
//Validate
try {
await form.validateFields();
} catch (error) {
setLoading(false);
return;
}
try {
const response = await axios.post("/intellipay/lightbox_credentials", {
bodyshop,
refresh: !!window.intellipay,
paymentSplitMeta: form.getFieldsValue(),
});
if (window.intellipay) {
// eslint-disable-next-line no-eval
eval(response.data);
SetIntellipayCallbackFunctions();
window.intellipay.autoOpen();
} else {
var rg = document.createRange();
let node = rg.createContextualFragment(response.data);
document.documentElement.appendChild(node);
SetIntellipayCallbackFunctions();
window.intellipay.isAutoOpen = true;
window.intellipay.initialize();
}
} catch (error) {
notification.open({
type: "error",
message: t("job_payments.notifications.error.openingip"),
});
setLoading(false);
}
};
return (
<Card title="Card Payment">
<Spin spinning={loading}>
<Form
form={form}
layout="vertical"
initialValues={{
payments: context.jobid ? [{ jobid: context.jobid }] : [],
}}
>
<Form.List name={["payments"]}>
{(fields, { add, remove, move }) => {
return (
<div>
{fields.map((field, index) => (
<Form.Item key={field.key}>
<Row gutter={[16, 16]}>
<Col span={16}>
<Form.Item
key={`${index}jobid`}
label={t("jobs.fields.ro_number")}
name={[field.name, "jobid"]}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<JobSearchSelectComponent
notExported={false}
clm_no
/>
</Form.Item>
</Col>
<Col span={6}>
<Form.Item
key={`${index}amount`}
label={t("payments.fields.amount")}
name={[field.name, "amount"]}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<CurrencyFormItemComponent />
</Form.Item>
</Col>
<Col span={2}>
<DeleteFilled
style={{ margin: "1rem" }}
onClick={() => {
remove(field.name);
}}
/>
</Col>
</Row>
</Form.Item>
))}
<Form.Item>
<Button
type="dashed"
onClick={() => {
add();
}}
style={{ width: "100%" }}
>
{t("general.actions.add")}
</Button>
</Form.Item>
</div>
);
}}
</Form.List>
<Form.Item
shouldUpdate={(prevValues, curValues) =>
prevValues.payments?.map((p) => p?.jobid).join() !==
curValues.payments?.map((p) => p?.jobid).join()
}
>
{() => {
//If all of the job ids have been fileld in, then query and update the IP field.
const { payments } = form.getFieldsValue();
if (
payments?.length > 0 &&
payments?.filter((p) => p?.jobid).length === payments?.length
) {
refetch({ jobids: payments.map((p) => p.jobid) });
}
return (
<>
<Input
className="ipayfield"
data-ipayname="account"
//type="hidden"
value={
payments && data && data.jobs.length > 0
? data.jobs.map((j) => j.ro_number).join(", ")
: null
}
hidden
/>
<Input
className="ipayfield"
data-ipayname="email"
// type="hidden"
value={
payments && data && data.jobs.length > 0
? data.jobs.filter((j) => j.ownr_ea)[0]?.ownr_ea
: null
}
hidden
/>
</>
);
}}
</Form.Item>
<Form.Item
shouldUpdate={(prevValues, curValues) =>
prevValues.payments?.map((p) => p?.amount).join() !==
curValues.payments?.map((p) => p?.amount).join()
}
>
{() => {
const { payments } = form.getFieldsValue();
const totalAmountToCharge = payments?.reduce((acc, val) => {
return acc + (val?.amount || 0);
}, 0);
return (
<Space style={{ float: "right" }}>
<Statistic
title="Amount To Charge"
value={totalAmountToCharge}
precision={2}
/>
<Input
className="ipayfield"
data-ipayname="amount"
//type="hidden"
value={totalAmountToCharge?.toFixed(2)}
hidden
/>
<Input
className="ipayfield"
data-ipayname="comment"
//type="hidden"
value={btoa(JSON.stringify(payments))}
hidden
/>
<Button
type="primary"
// data-ipayname="submit"
className="ipayfield"
loading={queryLoading || loading}
disabled={!(totalAmountToCharge > 0)}
onClick={handleIntelliPayCharge}
>
{t("job_payments.buttons.proceedtopayment")}
</Button>
</Space>
);
}}
</Form.Item>
</Form>
</Spin>
</Card>
);
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(CardPaymentModalComponent);

View File

@@ -1,57 +0,0 @@
import { Button, Modal } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { toggleModalVisible } from "../../redux/modals/modals.actions";
import { selectCardPayment } from "../../redux/modals/modals.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import CardPaymentModalComponent from "./card-payment-modal.component.";
const mapStateToProps = createStructuredSelector({
cardPaymentModal: selectCardPayment,
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
toggleModalVisible: () => dispatch(toggleModalVisible("cardPayment")),
});
function CardPaymentModalContainer({
cardPaymentModal,
toggleModalVisible,
bodyshop,
}) {
const { visible } = cardPaymentModal;
const { t } = useTranslation();
const handleCancel = () => {
toggleModalVisible();
};
const handleOK = () => {
toggleModalVisible();
};
return (
<Modal
open={visible}
onOk={handleOK}
onCancel={handleCancel}
footer={[
<Button key="back" onClick={handleCancel}>
{t("job_payments.buttons.goback")}
</Button>,
]}
width="80%"
destroyOnClose
>
<CardPaymentModalComponent />
</Modal>
);
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(CardPaymentModalContainer);

View File

@@ -4,7 +4,6 @@ import PhoneNumberFormatter from "../../utils/PhoneFormatter";
import ChatArchiveButton from "../chat-archive-button/chat-archive-button.component"; import ChatArchiveButton from "../chat-archive-button/chat-archive-button.component";
import ChatConversationTitleTags from "../chat-conversation-title-tags/chat-conversation-title-tags.component"; import ChatConversationTitleTags from "../chat-conversation-title-tags/chat-conversation-title-tags.component";
import ChatLabelComponent from "../chat-label/chat-label.component"; import ChatLabelComponent from "../chat-label/chat-label.component";
import ChatPrintButton from "../chat-print-button/chat-print-button.component";
import ChatTagRoContainer from "../chat-tag-ro/chat-tag-ro.container"; import ChatTagRoContainer from "../chat-tag-ro/chat-tag-ro.container";
export default function ChatConversationTitle({ conversation }) { export default function ChatConversationTitle({ conversation }) {
@@ -14,7 +13,6 @@ export default function ChatConversationTitle({ conversation }) {
{conversation && conversation.phone_num} {conversation && conversation.phone_num}
</PhoneNumberFormatter> </PhoneNumberFormatter>
<ChatLabelComponent conversation={conversation} /> <ChatLabelComponent conversation={conversation} />
<ChatPrintButton conversation={conversation} />
<ChatConversationTitleTags <ChatConversationTitleTags
jobConversations={ jobConversations={
(conversation && conversation.job_conversations) || [] (conversation && conversation.job_conversations) || []

View File

@@ -1,5 +1,5 @@
import Icon from "@ant-design/icons"; import Icon from "@ant-design/icons";
import { Tooltip } from "antd"; import { Spin, Tooltip } from "antd";
import i18n from "i18next"; import i18n from "i18next";
import moment from "moment"; import moment from "moment";
import React, { useEffect, useRef } from "react"; import React, { useEffect, useRef } from "react";
@@ -12,6 +12,8 @@ import {
} from "react-virtualized"; } from "react-virtualized";
import { DateTimeFormatter } from "../../utils/DateFormatter"; import { DateTimeFormatter } from "../../utils/DateFormatter";
import "./chat-message-list.styles.scss"; import "./chat-message-list.styles.scss";
import { useState } from "react";
import axios from "axios";
export default function ChatMessageListComponent({ messages }) { export default function ChatMessageListComponent({ messages }) {
const virtualizedListRef = useRef(null); const virtualizedListRef = useRef(null);
@@ -95,9 +97,7 @@ const MessageRender = (message) => {
key={idx} key={idx}
style={{ display: "flex", justifyContent: "center" }} style={{ display: "flex", justifyContent: "center" }}
> >
<a href={i} target="__blank"> <ImageDisplay src={i} />
<img alt="Received" className="message-img" src={i} />
</a>
</div> </div>
))} ))}
<div>{message.text}</div> <div>{message.text}</div>
@@ -116,3 +116,21 @@ const StatusRender = (status) => {
return null; return null;
} }
}; };
const ImageDisplay = ({ src }) => {
const [state, setstate] = useState({ loading: true, url: null });
useEffect(() => {
axios
.post("/sms/fetchmedia", { mediaUrl: src })
.then(({ data }) => setstate({ loading: false, url: data }));
}, [src]);
if (state.loading === true) return <Spin />;
return (
<a href={i} target="__blank">
<img alt="Received" className="message-img" src={state.url} />
</a>
);
};

View File

@@ -8,23 +8,15 @@ import PhoneNumberFormatter from "../../utils/PhoneFormatter";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import { searchingForConversation } from "../../redux/messaging/messaging.selectors";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
searchingForConversation: searchingForConversation,
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
openChatByPhone: (phone) => dispatch(openChatByPhone(phone)), openChatByPhone: (phone) => dispatch(openChatByPhone(phone)),
}); });
export function ChatOpenButton({ export function ChatOpenButton({ bodyshop, phone, jobid, openChatByPhone }) {
bodyshop,
searchingForConversation,
phone,
jobid,
openChatByPhone,
}) {
const { t } = useTranslation(); const { t } = useTranslation();
if (!phone) return <></>; if (!phone) return <></>;
@@ -37,7 +29,7 @@ export function ChatOpenButton({
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
const p = parsePhoneNumber(phone, "CA"); const p = parsePhoneNumber(phone, "CA");
if (searchingForConversation) return; //This is to prevent finding the same thing twice.
if (p && p.isValid()) { if (p && p.isValid()) {
openChatByPhone({ phone_num: p.formatInternational(), jobid: jobid }); openChatByPhone({ phone_num: p.formatInternational(), jobid: jobid });
} else { } else {

View File

@@ -132,7 +132,7 @@ export function ChatPopupComponent({
onClick={() => toggleChatVisible()} onClick={() => toggleChatVisible()}
style={{ cursor: "pointer" }} style={{ cursor: "pointer" }}
> >
<MessageOutlined className="chat-popup-info-icon" /> <MessageOutlined />
<strong>{t("messaging.labels.messaging")}</strong> <strong>{t("messaging.labels.messaging")}</strong>
</div> </div>
)} )}

View File

@@ -13,9 +13,6 @@
height: 100%; height: 100%;
} }
} }
.chat-popup-info-icon {
margin-right: 5px;
}
@media only screen and (min-width: 992px) { @media only screen and (min-width: 992px) {
.chat-popup { .chat-popup {

View File

@@ -1,59 +0,0 @@
import { MailOutlined, PrinterOutlined } from "@ant-design/icons";
import { Space, Spin } from "antd";
import React, { useState } from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { setEmailOptions } from "../../redux/email/email.actions";
import { GenerateDocument } from "../../utils/RenderTemplate";
import { TemplateList } from "../../utils/TemplateConstants";
const mapStateToProps = createStructuredSelector({});
const mapDispatchToProps = (dispatch) => ({
setEmailOptions: (e) => dispatch(setEmailOptions(e)),
});
export function ChatPrintButton({ conversation }) {
const [loading, setLoading] = useState(false);
return (
<Space wrap>
<PrinterOutlined
onClick={() => {
setLoading(true);
GenerateDocument(
{
name: TemplateList("messaging").conversation_list.key,
variables: { id: conversation.id },
},
{
subject: TemplateList("messaging").conversation_list.subject,
},
"p",
conversation.id
);
setLoading(false);
}}
/>
<MailOutlined
onClick={() => {
setLoading(true);
GenerateDocument(
{
name: TemplateList("messaging").conversation_list.key,
variables: { id: conversation.id },
},
{
subject: TemplateList("messaging").conversation_list.subject,
},
"e",
conversation.id
);
setLoading(false);
}}
/>
{loading && <Spin />}
</Space>
);
}
export default connect(mapStateToProps, mapDispatchToProps)(ChatPrintButton);

View File

@@ -35,15 +35,6 @@ export default function ContractsCarsComponent({
state.sortedInfo.columnKey === "status" && state.sortedInfo.order, state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
render: (text, record) => <div>{t(record.status)}</div>, render: (text, record) => <div>{t(record.status)}</div>,
}, },
{
title: t("courtesycars.fields.readiness"),
dataIndex: "readiness",
key: "readiness",
sorter: (a, b) => alphaSort(a.readiness, b.readiness),
sortOrder:
state.sortedInfo.columnKey === "readiness" && state.sortedInfo.order,
render: (text, record) => t(record.readiness),
},
{ {
title: t("courtesycars.fields.year"), title: t("courtesycars.fields.year"),
dataIndex: "year", dataIndex: "year",
@@ -68,14 +59,6 @@ export default function ContractsCarsComponent({
sortOrder: sortOrder:
state.sortedInfo.columnKey === "model" && state.sortedInfo.order, state.sortedInfo.columnKey === "model" && state.sortedInfo.order,
}, },
{
title: t("courtesycars.fields.color"),
dataIndex: "color",
key: "color",
sorter: (a, b) => alphaSort(a.color, b.color),
sortOrder:
state.sortedInfo.columnKey === "color" && state.sortedInfo.order,
},
{ {
title: t("courtesycars.fields.plate"), title: t("courtesycars.fields.plate"),
dataIndex: "plate", dataIndex: "plate",
@@ -110,9 +93,6 @@ export default function ContractsCarsComponent({
(cc.model || "") (cc.model || "")
.toLowerCase() .toLowerCase()
.includes(state.search.toLowerCase()) || .includes(state.search.toLowerCase()) ||
(cc.color || "")
.toLowerCase()
.includes(state.search.toLowerCase()) ||
(cc.plate || "").toLowerCase().includes(state.search.toLowerCase()) (cc.plate || "").toLowerCase().includes(state.search.toLowerCase())
); );

View File

@@ -68,30 +68,6 @@ export default function ContractFormComponent({
<FormDateTimePicker /> <FormDateTimePicker />
</Form.Item> </Form.Item>
)} )}
{create && (
<Form.Item
shouldUpdate={(p, c) => p.scheduledreturn !== c.scheduledreturn}
>
{() => {
const insuranceOver =
selectedCar &&
selectedCar.insuranceexpires &&
moment(selectedCar.insuranceexpires)
.endOf("day")
.isBefore(moment(form.getFieldValue("scheduledreturn")));
if (insuranceOver)
return (
<Space direction="vertical" style={{ color: "tomato" }}>
<span>
<WarningFilled style={{ marginRight: ".3rem" }} />
{t("contracts.labels.insuranceexpired")}
</span>
</Space>
);
return <></>;
}}
</Form.Item>
)}
</LayoutFormRow> </LayoutFormRow>
<LayoutFormRow grow> <LayoutFormRow grow>
<Form.Item <Form.Item
@@ -114,17 +90,16 @@ export default function ContractFormComponent({
> >
{() => { {() => {
const mileageOver = const mileageOver =
selectedCar && selectedCar.nextservicekm selectedCar &&
? selectedCar.nextservicekm <= form.getFieldValue("kmstart") selectedCar.nextservicekm <= form.getFieldValue("kmstart");
: false;
const dueForService = const dueForService =
selectedCar && selectedCar &&
selectedCar.nextservicedate && selectedCar.nextservicedate &&
moment(selectedCar.nextservicedate) moment(selectedCar.nextservicedate).isBefore(
.endOf("day") moment(form.getFieldValue("scheduledreturn"))
.isSameOrBefore( );
moment(form.getFieldValue("scheduledreturn"))
);
if (mileageOver || dueForService) if (mileageOver || dueForService)
return ( return (
<Space direction="vertical" style={{ color: "tomato" }}> <Space direction="vertical" style={{ color: "tomato" }}>
@@ -142,6 +117,7 @@ export default function ContractFormComponent({
</span> </span>
</Space> </Space>
); );
return <></>; return <></>;
}} }}
</Form.Item> </Form.Item>

View File

@@ -4,7 +4,6 @@ import { useTranslation } from "react-i18next";
import { alphaSort } from "../../utils/sorters"; import { alphaSort } from "../../utils/sorters";
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component"; import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component"; import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import {pageLimit} from "../../utils/config";
export default function ContractsJobsComponent({ export default function ContractsJobsComponent({
loading, loading,
@@ -176,7 +175,7 @@ export default function ContractsJobsComponent({
loading={loading} loading={loading}
pagination={{ pagination={{
position: "top", position: "top",
defaultPageSize: pageLimit, defaultPageSize: 10,
defaultCurrent: defaultCurrent, defaultCurrent: defaultCurrent,
}} }}
columns={columns} columns={columns}

View File

@@ -2,10 +2,10 @@ import { useQuery } from "@apollo/client";
import React from "react"; import React from "react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { QUERY_ALL_ACTIVE_JOBS } from "../../graphql/jobs.queries";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import AlertComponent from "../alert/alert.component"; import AlertComponent from "../alert/alert.component";
import ContractJobsComponent from "./contract-jobs.component"; import ContractJobsComponent from "./contract-jobs.component";
import { QUERY_ALL_ACTIVE_JOBS } from "../../graphql/jobs.queries";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser //currentUser: selectCurrentUser
@@ -15,7 +15,6 @@ export function ContractJobsContainer({ selectedJobState, bodyshop }) {
const { loading, error, data } = useQuery(QUERY_ALL_ACTIVE_JOBS, { const { loading, error, data } = useQuery(QUERY_ALL_ACTIVE_JOBS, {
variables: { variables: {
statuses: bodyshop.md_ro_statuses.active_statuses || ["Open"], statuses: bodyshop.md_ro_statuses.active_statuses || ["Open"],
isConverted: true,
}, },
fetchPolicy: "network-only", fetchPolicy: "network-only",
nextFetchPolicy: "network-only", nextFetchPolicy: "network-only",

View File

@@ -4,16 +4,15 @@ import queryString from "query-string";
import React, { useState } from "react"; import React, { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Link, useHistory, useLocation } from "react-router-dom"; import { Link, useHistory, useLocation } from "react-router-dom";
import { setModalContext } from "../../redux/modals/modals.actions";
import { DateTimeFormatter } from "../../utils/DateFormatter"; import { DateTimeFormatter } from "../../utils/DateFormatter";
import { alphaSort } from "../../utils/sorters"; import { alphaSort } from "../../utils/sorters";
import ContractsFindModalContainer from "../contracts-find-modal/contracts-find-modal.container"; import ContractsFindModalContainer from "../contracts-find-modal/contracts-find-modal.container";
import { setModalContext } from "../../redux/modals/modals.actions";
import moment from "moment";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import moment from "moment";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import {pageLimit} from "../../utils/config";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
@@ -210,7 +209,7 @@ export function ContractsList({
}} }}
pagination={{ pagination={{
position: "top", position: "top",
pageSize: pageLimit, pageSize: 25,
current: parseInt(page || 1), current: parseInt(page || 1),
total: total, total: total,
}} }}

View File

@@ -5,7 +5,6 @@ import { useTranslation } from "react-i18next";
import { Link, useHistory, useLocation } from "react-router-dom"; import { Link, useHistory, useLocation } from "react-router-dom";
import { DateFormatter } from "../../utils/DateFormatter"; import { DateFormatter } from "../../utils/DateFormatter";
import { alphaSort } from "../../utils/sorters"; import { alphaSort } from "../../utils/sorters";
import {pageLimit} from "../../utils/config";
export default function CourtesyCarContractListComponent({ export default function CourtesyCarContractListComponent({
contracts, contracts,
@@ -90,7 +89,7 @@ export default function CourtesyCarContractListComponent({
scroll={{ x: true }} scroll={{ x: true }}
pagination={{ pagination={{
position: "top", position: "top",
pageSize: pageLimit, pageSize: 25,
current: parseInt(page || 1), current: parseInt(page || 1),
total: totalContracts, total: totalContracts,
}} }}

View File

@@ -7,18 +7,13 @@ import { useTranslation } from "react-i18next";
import { CHECK_CC_FLEET_NUMBER } from "../../graphql/courtesy-car.queries"; import { CHECK_CC_FLEET_NUMBER } from "../../graphql/courtesy-car.queries";
import { DateFormatter } from "../../utils/DateFormatter"; import { DateFormatter } from "../../utils/DateFormatter";
import CourtesyCarFuelSlider from "../courtesy-car-fuel-select/courtesy-car-fuel-select.component"; import CourtesyCarFuelSlider from "../courtesy-car-fuel-select/courtesy-car-fuel-select.component";
import CourtesyCarReadiness from "../courtesy-car-readiness-select/courtesy-car-readiness-select.component";
import CourtesyCarStatus from "../courtesy-car-status-select/courtesy-car-status-select.component"; import CourtesyCarStatus from "../courtesy-car-status-select/courtesy-car-status-select.component";
import FormDatePicker from "../form-date-picker/form-date-picker.component"; import FormDatePicker from "../form-date-picker/form-date-picker.component";
import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component"; //import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
import CurrencyInput from "../form-items-formatted/currency-form-item.component"; import CurrencyInput from "../form-items-formatted/currency-form-item.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component"; import LayoutFormRow from "../layout-form-row/layout-form-row.component";
export default function CourtesyCarCreateFormComponent({ export default function CourtesyCarCreateFormComponent({ form, saveLoading }) {
form,
saveLoading,
newCC,
}) {
const { t } = useTranslation(); const { t } = useTranslation();
const client = useApolloClient(); const client = useApolloClient();
@@ -37,20 +32,8 @@ export default function CourtesyCarCreateFormComponent({
} }
/> />
{newCC ? null : <FormFieldsChanged form={form} />} {/* <FormFieldsChanged form={form} /> */}
<LayoutFormRow header={t("courtesycars.labels.vehicle")}> <LayoutFormRow header={t("courtesycars.labels.vehicle")}>
<Form.Item
label={t("courtesycars.fields.year")}
name="year"
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<Input />
</Form.Item>
<Form.Item <Form.Item
label={t("courtesycars.fields.make")} label={t("courtesycars.fields.make")}
name="make" name="make"
@@ -75,6 +58,18 @@ export default function CourtesyCarCreateFormComponent({
> >
<Input /> <Input />
</Form.Item> </Form.Item>
<Form.Item
label={t("courtesycars.fields.year")}
name="year"
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<Input />
</Form.Item>
<Form.Item <Form.Item
label={t("courtesycars.fields.plate")} label={t("courtesycars.fields.plate")}
name="plate" name="plate"
@@ -123,7 +118,7 @@ export default function CourtesyCarCreateFormComponent({
}, },
]} ]}
> >
<InputNumber min={0} precision={0} /> <InputNumber precision={0} />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label={t("courtesycars.fields.fleetnumber")} label={t("courtesycars.fields.fleetnumber")}
@@ -218,27 +213,49 @@ export default function CourtesyCarCreateFormComponent({
> >
<CourtesyCarStatus /> <CourtesyCarStatus />
</Form.Item> </Form.Item>
<Form.Item label={t("courtesycars.fields.readiness")} name="readiness"> <Form.Item
<CourtesyCarReadiness /> label={t("courtesycars.fields.nextservicekm")}
name="nextservicekm"
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<InputNumber />
</Form.Item> </Form.Item>
<div> <div>
<Form.Item <Form.Item
label={t("courtesycars.fields.nextservicekm")} label={t("courtesycars.fields.nextservicedate")}
name="nextservicekm" name="nextservicedate"
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
> >
<InputNumber min={0} precision={0} /> <FormDatePicker />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
shouldUpdate={(p, c) => shouldUpdate={(p, c) =>
p.mileage !== c.mileage || p.nextservicekm !== c.nextservicekm p.mileage !== c.mileage ||
p.nextservicedate !== c.nextservicedate ||
p.nextservicekm !== c.nextservicekm
} }
> >
{() => { {() => {
const nextservicedate = form.getFieldValue("nextservicedate");
const nextservicekm = form.getFieldValue("nextservicekm"); const nextservicekm = form.getFieldValue("nextservicekm");
const mileageOver = nextservicekm
? nextservicekm <= form.getFieldValue("mileage") const mileageOver =
: false; nextservicekm <= form.getFieldValue("mileage");
if (mileageOver)
const dueForService =
nextservicedate && moment(nextservicedate).isBefore(moment());
if (mileageOver || dueForService)
return ( return (
<Space direction="vertical" style={{ color: "tomato" }}> <Space direction="vertical" style={{ color: "tomato" }}>
<span> <span>
@@ -246,36 +263,6 @@ export default function CourtesyCarCreateFormComponent({
{t("contracts.labels.cardueforservice")} {t("contracts.labels.cardueforservice")}
</span> </span>
<span>{`${nextservicekm} km`}</span> <span>{`${nextservicekm} km`}</span>
</Space>
);
return <></>;
}}
</Form.Item>
</div>
<div>
<Form.Item
label={t("courtesycars.fields.nextservicedate")}
name="nextservicedate"
>
<FormDatePicker />
</Form.Item>
<Form.Item
shouldUpdate={(p, c) => p.nextservicedate !== c.nextservicedate}
>
{() => {
const nextservicedate = form.getFieldValue("nextservicedate");
const dueForService =
nextservicedate &&
moment(nextservicedate).endOf("day").isSameOrBefore(moment());
if (dueForService)
return (
<Space direction="vertical" style={{ color: "tomato" }}>
<span>
<WarningFilled style={{ marginRight: ".3rem" }} />
{t("contracts.labels.cardueforservice")}
</span>
<span> <span>
<DateFormatter>{nextservicedate}</DateFormatter> <DateFormatter>{nextservicedate}</DateFormatter>
</span> </span>
@@ -296,6 +283,12 @@ export default function CourtesyCarCreateFormComponent({
<Form.Item <Form.Item
label={t("courtesycars.fields.registrationexpires")} label={t("courtesycars.fields.registrationexpires")}
name="registrationexpires" name="registrationexpires"
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
> >
<FormDatePicker /> <FormDatePicker />
</Form.Item> </Form.Item>
@@ -307,8 +300,7 @@ export default function CourtesyCarCreateFormComponent({
{() => { {() => {
const expires = form.getFieldValue("registrationexpires"); const expires = form.getFieldValue("registrationexpires");
const dateover = const dateover = expires && moment(expires).isBefore(moment());
expires && moment(expires).endOf("day").isBefore(moment());
if (dateover) if (dateover)
return ( return (
@@ -338,13 +330,14 @@ export default function CourtesyCarCreateFormComponent({
<FormDatePicker /> <FormDatePicker />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
shouldUpdate={(p, c) => p.insuranceexpires !== c.insuranceexpires} shouldUpdate={(p, c) =>
p.insuranceexpires !== c.insuranceexpires
}
> >
{() => { {() => {
const expires = form.getFieldValue("insuranceexpires"); const expires = form.getFieldValue("insuranceexpires");
const dateover = const dateover = expires && moment(expires).isBefore(moment());
expires && moment(expires).endOf("day").isBefore(moment());
if (dateover) if (dateover)
return ( return (

View File

@@ -34,32 +34,6 @@ const CourtesyCarFuelComponent = (props, ref) => {
step={null} step={null}
style={{ marginLeft: "2rem", marginRight: "2rem" }} style={{ marginLeft: "2rem", marginRight: "2rem" }}
{...props} {...props}
tooltip={{
formatter: (value) => {
switch (value) {
case 0:
return t("courtesycars.labels.fuel.empty");
case 13:
return t("courtesycars.labels.fuel.18");
case 25:
return t("courtesycars.labels.fuel.14");
case 38:
return t("courtesycars.labels.fuel.38");
case 50:
return t("courtesycars.labels.fuel.12");
case 63:
return t("courtesycars.labels.fuel.58");
case 75:
return t("courtesycars.labels.fuel.34");
case 88:
return t("courtesycars.labels.fuel.78");
case 100:
return t("courtesycars.labels.fuel.full");
default:
return value;
}
},
}}
/> />
); );
}; };

View File

@@ -1,35 +0,0 @@
import { Select } from "antd";
import React, { forwardRef, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
const { Option } = Select;
const CourtesyCarReadinessComponent = ({ value, onChange }, ref) => {
const [option, setOption] = useState(value);
const { t } = useTranslation();
useEffect(() => {
if (value !== option && onChange) {
onChange(option);
}
}, [value, option, onChange]);
return (
<Select
allowClear
ref={ref}
value={option}
style={{
width: 100,
}}
onChange={setOption}
>
<Option value="courtesycars.readiness.ready">
{t("courtesycars.readiness.ready")}
</Option>
<Option value="courtesycars.readiness.notready">
{t("courtesycars.readiness.notready")}
</Option>
</Select>
);
};
export default forwardRef(CourtesyCarReadinessComponent);

View File

@@ -37,9 +37,6 @@ const CourtesyCarStatusComponent = ({ value, onChange }, ref) => {
<Option value="courtesycars.status.leasereturn"> <Option value="courtesycars.status.leasereturn">
{t("courtesycars.status.leasereturn")} {t("courtesycars.status.leasereturn")}
</Option> </Option>
<Option value="courtesycars.status.unavailable">
{t("courtesycars.status.unavailable")}
</Option>
</Select> </Select>
); );
}; };

View File

@@ -9,26 +9,21 @@ import {
Table, Table,
Tooltip, Tooltip,
} from "antd"; } from "antd";
import moment from "moment";
import React, { useState } from "react"; import React, { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { DateTimeFormatter } from "../../utils/DateFormatter"; import { DateTimeFormatter } from "../../utils/DateFormatter";
import { alphaSort } from "../../utils/sorters";
import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
import moment from "moment";
import { GenerateDocument } from "../../utils/RenderTemplate"; import { GenerateDocument } from "../../utils/RenderTemplate";
import { TemplateList } from "../../utils/TemplateConstants"; import { TemplateList } from "../../utils/TemplateConstants";
import { alphaSort } from "../../utils/sorters";
import useLocalStorage from "../../utils/useLocalStorage";
import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
export default function CourtesyCarsList({ loading, courtesycars, refetch }) { export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
const [state, setState] = useState({ const [state, setState] = useState({
sortedInfo: {}, sortedInfo: {},
filteredInfo: { text: "" },
}); });
const [searchText, setSearchText] = useState(""); const [searchText, setSearchText] = useState("");
const [filter, setFilter] = useLocalStorage(
"filter_courtesy_cars_list",
null
);
const { t } = useTranslation(); const { t } = useTranslation();
const columns = [ const columns = [
@@ -55,16 +50,11 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
dataIndex: "status", dataIndex: "status",
key: "status", key: "status",
sorter: (a, b) => alphaSort(a.status, b.status), sorter: (a, b) => alphaSort(a.status, b.status),
filteredValue: filter?.status || null,
filters: [ filters: [
{ {
text: t("courtesycars.status.in"), text: t("courtesycars.status.in"),
value: "courtesycars.status.in", value: "courtesycars.status.in",
}, },
{
text: t("courtesycars.status.inservice"),
value: "courtesycars.status.inservice",
},
{ {
text: t("courtesycars.status.out"), text: t("courtesycars.status.out"),
value: "courtesycars.status.out", value: "courtesycars.status.out",
@@ -77,43 +67,23 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
text: t("courtesycars.status.leasereturn"), text: t("courtesycars.status.leasereturn"),
value: "courtesycars.status.leasereturn", value: "courtesycars.status.leasereturn",
}, },
{
text: t("courtesycars.status.unavailable"),
value: "courtesycars.status.unavailable",
},
], ],
onFilter: (value, record) => record.status === value, onFilter: (value, record) => value.includes(record.status),
sortOrder: sortOrder:
state.sortedInfo.columnKey === "status" && state.sortedInfo.order, state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
render: (text, record) => { render: (text, record) => {
const { nextservicedate, nextservicekm, mileage, insuranceexpires } = const { nextservicedate, nextservicekm, mileage } = record;
record;
const mileageOver = nextservicekm ? nextservicekm <= mileage : false; const mileageOver = nextservicekm <= mileage;
const dueForService = const dueForService =
nextservicedate && nextservicedate && moment(nextservicedate).isBefore(moment());
moment(nextservicedate).endOf("day").isSameOrBefore(moment());
const insuranceOver =
insuranceexpires &&
moment(insuranceexpires).endOf("day").isBefore(moment());
return ( return (
<Space> <Space>
{t(record.status)} {t(record.status)}
{(mileageOver || dueForService || insuranceOver) && ( {(mileageOver || dueForService) && (
<Tooltip <Tooltip title={t("contracts.labels.cardueforservice")}>
title={
(mileageOver || dueForService) && insuranceOver
? t("contracts.labels.insuranceexpired") +
" / " +
t("contracts.labels.cardueforservice")
: insuranceOver
? t("contracts.labels.insuranceexpired")
: t("contracts.labels.cardueforservice")
}
>
<WarningFilled style={{ color: "tomato" }} /> <WarningFilled style={{ color: "tomato" }} />
</Tooltip> </Tooltip>
)} )}
@@ -121,27 +91,6 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
); );
}, },
}, },
{
title: t("courtesycars.fields.readiness"),
dataIndex: "readiness",
key: "readiness",
sorter: (a, b) => alphaSort(a.readiness, b.readiness),
filteredValue: filter?.readiness || null,
filters: [
{
text: t("courtesycars.readiness.ready"),
value: "courtesycars.readiness.ready",
},
{
text: t("courtesycars.readiness.notready"),
value: "courtesycars.readiness.notready",
},
],
onFilter: (value, record) => value.includes(record.readiness),
sortOrder:
state.sortedInfo.columnKey === "readiness" && state.sortedInfo.order,
render: (text, record) => t(record.readiness),
},
{ {
title: t("courtesycars.fields.year"), title: t("courtesycars.fields.year"),
dataIndex: "year", dataIndex: "year",
@@ -166,14 +115,6 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
sortOrder: sortOrder:
state.sortedInfo.columnKey === "model" && state.sortedInfo.order, state.sortedInfo.columnKey === "model" && state.sortedInfo.order,
}, },
{
title: t("courtesycars.fields.color"),
dataIndex: "color",
key: "color",
sorter: (a, b) => alphaSort(a.color, b.color),
sortOrder:
state.sortedInfo.columnKey === "color" && state.sortedInfo.order,
},
{ {
title: t("courtesycars.fields.plate"), title: t("courtesycars.fields.plate"),
dataIndex: "plate", dataIndex: "plate",
@@ -182,38 +123,6 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
sortOrder: sortOrder:
state.sortedInfo.columnKey === "plate" && state.sortedInfo.order, state.sortedInfo.columnKey === "plate" && state.sortedInfo.order,
}, },
{
title: t("courtesycars.fields.fuel"),
dataIndex: "fuel",
key: "fuel",
sorter: (a, b) => a.fuel - b.fuel,
sortOrder:
state.sortedInfo.columnKey === "fuel" && state.sortedInfo.order,
render: (text, record) => {
switch (record.fuel) {
case 100:
return t("courtesycars.labels.fuel.full");
case 88:
return t("courtesycars.labels.fuel.78");
case 75:
return t("courtesycars.labels.fuel.34");
case 63:
return t("courtesycars.labels.fuel.58");
case 50:
return t("courtesycars.labels.fuel.12");
case 38:
return t("courtesycars.labels.fuel.38");
case 25:
return t("courtesycars.labels.fuel.14");
case 13:
return t("courtesycars.labels.fuel.18");
case 0:
return t("courtesycars.labels.fuel.empty");
default:
return record.fuel;
}
},
},
{ {
title: t("courtesycars.labels.outwith"), title: t("courtesycars.labels.outwith"),
dataIndex: "outwith", dataIndex: "outwith",
@@ -244,8 +153,7 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
]; ];
const handleTableChange = (pagination, filters, sorter) => { const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, sortedInfo: sorter }); setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
setFilter(filters);
}; };
const tableData = searchText const tableData = searchText
@@ -258,7 +166,6 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
(c.year || "").toLowerCase().includes(searchText.toLowerCase()) || (c.year || "").toLowerCase().includes(searchText.toLowerCase()) ||
(c.make || "").toLowerCase().includes(searchText.toLowerCase()) || (c.make || "").toLowerCase().includes(searchText.toLowerCase()) ||
(c.model || "").toLowerCase().includes(searchText.toLowerCase()) || (c.model || "").toLowerCase().includes(searchText.toLowerCase()) ||
(c.plate || "").toLowerCase().includes(searchText.toLowerCase()) ||
(t(c.status) || "").toLowerCase().includes(searchText.toLowerCase()) (t(c.status) || "").toLowerCase().includes(searchText.toLowerCase())
) )
: courtesycars; : courtesycars;

View File

@@ -5,7 +5,6 @@ import React, { useEffect } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useLocation } from "react-router-dom"; import { useLocation } from "react-router-dom";
import { QUERY_CSI_RESPONSE_BY_PK } from "../../graphql/csi.queries"; import { QUERY_CSI_RESPONSE_BY_PK } from "../../graphql/csi.queries";
import { DateFormatter } from "../../utils/DateFormatter";
import AlertComponent from "../alert/alert.component"; import AlertComponent from "../alert/alert.component";
import ConfigFormComponents from "../config-form-components/config-form-components.component"; import ConfigFormComponents from "../config-form-components/config-form-components.component";
import LoadingSpinner from "../loading-spinner/loading-spinner.component"; import LoadingSpinner from "../loading-spinner/loading-spinner.component";
@@ -45,13 +44,6 @@ export default function CsiResponseFormContainer() {
readOnly readOnly
componentList={data.csi_by_pk.csiquestion.config} componentList={data.csi_by_pk.csiquestion.config}
/> />
{data.csi_by_pk.validuntil ? (
<>
{t("csi.fields.validuntil")}
{": "}
<DateFormatter>{data.csi_by_pk.validuntil}</DateFormatter>
</>
) : null}
</Form> </Form>
</Card> </Card>
); );

View File

@@ -5,11 +5,8 @@ import React, { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Link, useHistory, useLocation } from "react-router-dom"; import { Link, useHistory, useLocation } from "react-router-dom";
import { DateFormatter } from "../../utils/DateFormatter"; import { DateFormatter } from "../../utils/DateFormatter";
import { pageLimit } from "../../utils/config"; import { alphaSort } from "../../utils/sorters";
import { alphaSort, dateSort } from "../../utils/sorters"; import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import OwnerNameDisplay, {
OwnerNameDisplayFunction,
} from "../owner-name-display/owner-name-display.component";
export default function CsiResponseListPaginated({ export default function CsiResponseListPaginated({
refetch, refetch,
@@ -18,23 +15,23 @@ export default function CsiResponseListPaginated({
total, total,
}) { }) {
const search = queryString.parse(useLocation().search); const search = queryString.parse(useLocation().search);
const { responseid } = search; const { responseid, page, sortcolumn, sortorder } = search;
const history = useHistory(); const history = useHistory();
const { t } = useTranslation();
const [state, setState] = useState({ const [state, setState] = useState({
sortedInfo: {}, sortedInfo: {},
filteredInfo: { text: "" }, filteredInfo: { text: "" },
page: "",
}); });
const { t } = useTranslation();
const columns = [ const columns = [
{ {
title: t("jobs.fields.ro_number"), title: t("jobs.fields.ro_number"),
dataIndex: "ro_number", dataIndex: "ro_number",
key: "ro_number", key: "ro_number",
width: "8%",
sorter: (a, b) => alphaSort(a.job.ro_number, b.job.ro_number), sorter: (a, b) => alphaSort(a.job.ro_number, b.job.ro_number),
sortOrder: sortOrder: sortcolumn === "ro_number" && sortorder,
state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
render: (text, record) => ( render: (text, record) => (
<Link to={"/manage/jobs/" + record.job.id}> <Link to={"/manage/jobs/" + record.job.id}>
{record.job.ro_number || t("general.labels.na")} {record.job.ro_number || t("general.labels.na")}
@@ -43,18 +40,15 @@ export default function CsiResponseListPaginated({
}, },
{ {
title: t("jobs.fields.owner"), title: t("jobs.fields.owner"),
dataIndex: "owner_name", dataIndex: "owner",
key: "owner_name", key: "owner",
sorter: (a, b) => ellipsis: true,
alphaSort( sorter: (a, b) => alphaSort(a.job.ownr_ln, b.job.ownr_ln),
OwnerNameDisplayFunction(a.job), width: "25%",
OwnerNameDisplayFunction(b.job) sortOrder: sortcolumn === "owner" && sortorder,
),
sortOrder:
state.sortedInfo.columnKey === "owner_name" && state.sortedInfo.order,
render: (text, record) => { render: (text, record) => {
return record.job.ownerid ? ( return record.job.owner ? (
<Link to={"/manage/owners/" + record.job.ownerid}> <Link to={"/manage/owners/" + record.job.owner.id}>
<OwnerNameDisplay ownerObject={record.job} /> <OwnerNameDisplay ownerObject={record.job} />
</Link> </Link>
) : ( ) : (
@@ -69,9 +63,9 @@ export default function CsiResponseListPaginated({
dataIndex: "completedon", dataIndex: "completedon",
key: "completedon", key: "completedon",
ellipsis: true, ellipsis: true,
sorter: (a, b) => dateSort(a.completedon, b.completedon), sorter: (a, b) => a.completedon - b.completedon,
sortOrder: width: "25%",
state.sortedInfo.columnKey === "completedon" && state.sortedInfo.order, sortOrder: sortcolumn === "completedon" && sortorder,
render: (text, record) => { render: (text, record) => {
return record.completedon ? ( return record.completedon ? (
<DateFormatter>{record.completedon}</DateFormatter> <DateFormatter>{record.completedon}</DateFormatter>
@@ -81,12 +75,11 @@ export default function CsiResponseListPaginated({
]; ];
const handleTableChange = (pagination, filters, sorter) => { const handleTableChange = (pagination, filters, sorter) => {
setState({ setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
...state, search.page = pagination.current;
filteredInfo: filters, search.sortcolumn = sorter.columnKey;
sortedInfo: sorter, search.sortorder = sorter.order;
page: pagination.current, history.push({ search: queryString.stringify(search) });
});
}; };
const handleOnRowClick = (record) => { const handleOnRowClick = (record) => {
@@ -113,8 +106,8 @@ export default function CsiResponseListPaginated({
loading={loading} loading={loading}
pagination={{ pagination={{
position: "top", position: "top",
pageSize: pageLimit, pageSize: 25,
current: parseInt(state.page || 1), current: parseInt(page || 1),
total: total, total: total,
}} }}
columns={columns} columns={columns}
@@ -128,6 +121,13 @@ export default function CsiResponseListPaginated({
selectedRowKeys: [responseid], selectedRowKeys: [responseid],
type: "radio", type: "radio",
}} }}
onRow={(record, rowIndex) => {
return {
onClick: (event) => {
handleOnRowClick(record);
}, // click row
};
}}
/> />
</Card> </Card>
); );

View File

@@ -1,169 +0,0 @@
import {Card, Table, Tag} from "antd";
import LoadingSkeleton from "../../loading-skeleton/loading-skeleton.component";
import {useTranslation} from "react-i18next";
import React, {useEffect, useState} from "react";
import moment from "moment";
import DashboardRefreshRequired from "../refresh-required.component";
import axios from "axios";
const fortyFiveDaysAgo = () => moment().subtract(45, 'days').toLocaleString();
export default function JobLifecycleDashboardComponent({data, bodyshop, ...cardProps}) {
const {t} = useTranslation();
const [loading, setLoading] = useState(false);
const [lifecycleData, setLifecycleData] = useState(null);
useEffect(() => {
async function getLifecycleData() {
if (data && data.job_lifecycle) {
setLoading(true);
const response = await axios.post("/job/lifecycle", {
jobids: data.job_lifecycle.map(x => x.id),
statuses: bodyshop.md_ro_statuses
});
setLifecycleData(response.data.durations);
setLoading(false);
}
}
getLifecycleData().catch(e => {
console.error(`Error in getLifecycleData: ${e}`);
})
}, [data, bodyshop]);
const columns = [
{
title: t('job_lifecycle.columns.status'),
dataIndex: 'status',
bgColor: 'red',
key: 'status',
render: (text, record) => {
return <Tag color={record.color}>{record.status}</Tag>
}
},
{
title: t('job_lifecycle.columns.human_readable'),
dataIndex: 'humanReadable',
key: 'humanReadable',
},
{
title: t('job_lifecycle.columns.status_count'),
key: 'statusCount',
render: (text, record) => {
return lifecycleData.statusCounts[record.status];
}
},
{
title: t('job_lifecycle.columns.percentage'),
dataIndex: 'percentage',
key: 'percentage',
render: (text, record) => {
return record.percentage.toFixed(2) + '%';
}
},
];
if (!data) return null;
if (!data.job_lifecycle || !lifecycleData) return <DashboardRefreshRequired {...cardProps} />;
const extra = `${t('job_lifecycle.content.calculated_based_on')} ${lifecycleData.jobs} ${t('job_lifecycle.content.jobs_in_since')} ${fortyFiveDaysAgo()}`
return (
<Card title={t("job_lifecycle.titles.dashboard")} {...cardProps}>
<LoadingSkeleton loading={loading}>
<div style={{overflow: 'scroll', height: "100%"}}>
<div id="bar-container" style={{
display: 'flex',
width: '100%',
height: '100px',
textAlign: 'center',
borderRadius: '5px',
borderWidth: '5px',
borderStyle: 'solid',
borderColor: '#f0f2f5',
margin: 0,
padding: 0
}}>
{lifecycleData.summations.map((key, index, array) => {
const isFirst = index === 0;
const isLast = index === array.length - 1;
return (
<div key={key.status} style={{
overflow: 'hidden',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
margin: 0,
padding: 0,
borderTop: '1px solid #f0f2f5',
borderBottom: '1px solid #f0f2f5',
borderLeft: isFirst ? '1px solid #f0f2f5' : undefined,
borderRight: isLast ? '1px solid #f0f2f5' : undefined,
backgroundColor: key.color,
width: `${key.percentage}%`
}}
aria-label={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`}
title={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`}
>
{key.percentage > 15 ?
<>
<div>{key.roundedPercentage}</div>
<div style={{
backgroundColor: '#f0f2f5',
borderRadius: '5px',
paddingRight: '2px',
paddingLeft: '2px',
fontSize: '0.8rem',
}}>
{key.status}
</div>
</>
: null}
</div>
);
})}
</div>
<Card extra={extra} type='inner' title={t('job_lifecycle.content.legend_title')}
style={{marginTop: '10px'}}>
<div>
{lifecycleData.summations.map((key) => (
<Tag color={key.color} style={{width: '13vh', padding: '4px', margin: '4px'}}>
<div
aria-label={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`}
title={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`}
style={{
backgroundColor: '#f0f2f5',
color: '#000',
padding: '4px',
textAlign: 'center'
}}>
{key.status} [{lifecycleData.statusCounts[key.status]}] ({key.roundedPercentage})
</div>
</Tag>
))}
</div>
</Card>
<Card style={{marginTop: "5px"}} type='inner' title={t("job_lifecycle.titles.top_durations")}>
<Table size="small" pagination={false} columns={columns}
dataSource={lifecycleData.summations.sort((a, b) => b.value - a.value).slice(0, 3)}/>
</Card>
</div>
</LoadingSkeleton>
</Card>
);
}
export const JobLifecycleDashboardGQL = `
job_lifecycle: jobs(where: {
actual_in: {
_gte: "${moment().subtract(45, 'days').toISOString()}"
}
}) {
id
actual_in
} `;

View File

@@ -6,7 +6,6 @@ import { alphaSort } from "../../../utils/sorters";
import LoadingSkeleton from "../../loading-skeleton/loading-skeleton.component"; import LoadingSkeleton from "../../loading-skeleton/loading-skeleton.component";
import Dinero from "dinero.js"; import Dinero from "dinero.js";
import DashboardRefreshRequired from "../refresh-required.component"; import DashboardRefreshRequired from "../refresh-required.component";
import {pageLimit} from "../../../utils/config";
export default function DashboardMonthlyJobCosting({ data, ...cardProps }) { export default function DashboardMonthlyJobCosting({ data, ...cardProps }) {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -119,7 +118,7 @@ export default function DashboardMonthlyJobCosting({ data, ...cardProps }) {
<div style={{ height: "100%" }}> <div style={{ height: "100%" }}>
<Table <Table
onChange={handleTableChange} onChange={handleTableChange}
pagination={{ position: "top", defaultPageSize: pageLimit }} pagination={{ position: "top", defaultPageSize: 50 }}
columns={columns} columns={columns}
scroll={{ x: true, y: "calc(100% - 4em)" }} scroll={{ x: true, y: "calc(100% - 4em)" }}
rowKey="id" rowKey="id"

View File

@@ -3,265 +3,62 @@ import {
ExclamationCircleFilled, ExclamationCircleFilled,
PauseCircleOutlined, PauseCircleOutlined,
} from "@ant-design/icons"; } from "@ant-design/icons";
import { Card, Space, Switch, Table, Tooltip, Typography } from "antd"; import { Card, Space, Table, Tooltip } from "antd";
import moment from "moment"; import moment from "moment";
import React, { useState } from "react"; import React, { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { TimeFormatter } from "../../../utils/DateFormatter";
import { onlyUnique } from "../../../utils/arrayHelper";
import { alphaSort, dateSort } from "../../../utils/sorters";
import useLocalStorage from "../../../utils/useLocalStorage";
import ChatOpenButton from "../../chat-open-button/chat-open-button.component"; import ChatOpenButton from "../../chat-open-button/chat-open-button.component";
import OwnerNameDisplay, { import OwnerNameDisplay from "../../owner-name-display/owner-name-display.component";
OwnerNameDisplayFunction,
} from "../../owner-name-display/owner-name-display.component";
import DashboardRefreshRequired from "../refresh-required.component"; import DashboardRefreshRequired from "../refresh-required.component";
export default function DashboardScheduledInToday({ data, ...cardProps }) { export default function DashboardScheduledInToday({ data, ...cardProps }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [state, setState] = useState({ const [state, setState] = useState({
sortedInfo: {}, sortedInfo: {},
filteredInfo: {},
}); });
const [isTvModeScheduledIn, setIsTvModeScheduledIn] = useLocalStorage(
"isTvModeScheduledIn",
false
);
if (!data) return null; if (!data) return null;
if (!data.scheduled_in_today) if (!data.scheduled_in_today)
return <DashboardRefreshRequired {...cardProps} />; return <DashboardRefreshRequired {...cardProps} />;
const appt = []; // Flatten Data const appt = []; // Flatten Data
data.scheduled_in_today.forEach((item) => { data.scheduled_in_today.forEach((item) => {
if (item.job) { var i = {
var i = { canceled: item.canceled,
canceled: item.canceled, id: item.id,
id: item.id, alt_transport: item.job.alt_transport,
alt_transport: item.job.alt_transport, clm_no: item.job.clm_no,
clm_no: item.job.clm_no, jobid: item.job.jobid,
jobid: item.job.jobid, ins_co_nm: item.job.ins_co_nm,
joblines_body: item.job.joblines iouparent: item.job.iouparent,
.filter((l) => l.mod_lbr_ty !== "LAR") ownerid: item.job.ownerid,
.reduce((acc, val) => acc + val.mod_lb_hrs, 0), ownr_co_nm: item.job.ownr_co_nm,
joblines_ref: item.job.joblines ownr_ea: item.job.ownr_ea,
.filter((l) => l.mod_lbr_ty === "LAR") ownr_fn: item.job.ownr_fn,
.reduce((acc, val) => acc + val.mod_lb_hrs, 0), ownr_ln: item.job.ownr_ln,
ins_co_nm: item.job.ins_co_nm, ownr_ph1: item.job.ownr_ph1,
iouparent: item.job.iouparent, ownr_ph2: item.job.ownr_ph2,
ownerid: item.job.ownerid, production_vars: item.job.production_vars,
ownr_co_nm: item.job.ownr_co_nm, ro_number: item.job.ro_number,
ownr_ea: item.job.ownr_ea, suspended: item.job.suspended,
ownr_fn: item.job.ownr_fn, v_make_desc: item.job.v_make_desc,
ownr_ln: item.job.ownr_ln, v_model_desc: item.job.v_model_desc,
ownr_ph1: item.job.ownr_ph1, v_model_yr: item.job.v_model_yr,
ownr_ph2: item.job.ownr_ph2, v_vin: item.job.v_vin,
production_vars: item.job.production_vars, vehicleid: item.job.vehicleid,
ro_number: item.job.ro_number, note: item.note,
suspended: item.job.suspended, start: moment(item.start).format("hh:mm a"),
v_make_desc: item.job.v_make_desc, title: item.title,
v_model_desc: item.job.v_model_desc, };
v_model_yr: item.job.v_model_yr, appt.push(i);
v_vin: item.job.v_vin,
vehicleid: item.job.vehicleid,
note: item.note,
start: item.start,
title: item.title,
};
appt.push(i);
}
}); });
appt.sort(function (a, b) { appt.sort ( function (a, b) { return new Date(a.start) - new Date(b.start); });
return new moment(a.start) - new moment(b.start);
});
const tvFontSize = 16;
const tvFontWeight = "bold";
const tvColumns = [
{
title: t("appointments.fields.time"),
dataIndex: "start",
key: "start",
ellipsis: true,
sorter: (a, b) => dateSort(a.start, b.start),
sortOrder:
state.sortedInfo.columnKey === "start" && state.sortedInfo.order,
render: (text, record) => (
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
<TimeFormatter>{record.start}</TimeFormatter>
</span>
),
},
{
title: t("jobs.fields.ro_number"),
dataIndex: "ro_number",
key: "ro_number",
sorter: (a, b) => alphaSort(a.ro_number, b.ro_number),
sortOrder:
state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
render: (text, record) => (
<Link
to={"/manage/jobs/" + record.jobid}
onClick={(e) => e.stopPropagation()}
>
<Space>
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
{record.ro_number || t("general.labels.na")}
{record.production_vars && record.production_vars.alert ? (
<ExclamationCircleFilled className="production-alert" />
) : null}
{record.suspended && (
<PauseCircleOutlined style={{ color: "orangered" }} />
)}
{record.iouparent && (
<Tooltip title={t("jobs.labels.iou")}>
<BranchesOutlined style={{ color: "orangered" }} />
</Tooltip>
)}
</span>
</Space>
</Link>
),
},
{
title: t("jobs.fields.owner"),
dataIndex: "owner",
key: "owner",
ellipsis: true,
sorter: (a, b) =>
alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
sortOrder:
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
render: (text, record) => {
return record.ownerid ? (
<Link
to={"/manage/owners/" + record.ownerid}
onClick={(e) => e.stopPropagation()}
>
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
<OwnerNameDisplay ownerObject={record} />
</span>
</Link>
) : (
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
<OwnerNameDisplay ownerObject={record} />
</span>
);
},
},
{
title: t("jobs.fields.vehicle"),
dataIndex: "vehicle",
key: "vehicle",
ellipsis: true,
sorter: (a, b) =>
alphaSort(
`${a.v_model_yr || ""} ${a.v_make_desc || ""} ${
a.v_model_desc || ""
}`,
`${b.v_model_yr || ""} ${b.v_make_desc || ""} ${b.v_model_desc || ""}`
),
sortOrder:
state.sortedInfo.columnKey === "vehicle" && state.sortedInfo.order,
render: (text, record) => {
return record.vehicleid ? (
<Link
to={"/manage/vehicles/" + record.vehicleid}
onClick={(e) => e.stopPropagation()}
>
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
record.v_model_desc || ""
}`}
</span>
</Link>
) : (
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>{`${
record.v_model_yr || ""
} ${record.v_make_desc || ""} ${record.v_model_desc || ""}`}</span>
);
},
},
{
title: t("appointments.fields.alt_transport"),
dataIndex: "alt_transport",
key: "alt_transport",
ellipsis: true,
sorter: (a, b) => alphaSort(a.alt_transport, b.alt_transport),
sortOrder:
state.sortedInfo.columnKey === "alt_transport" &&
state.sortedInfo.order,
filters:
(appt &&
appt
.map((j) => j.alt_transport)
.filter(onlyUnique)
.map((s) => {
return {
text: s || "No Alt. Transport",
value: [s],
};
})
.sort((a, b) => alphaSort(a.text, b.text))) ||
[],
onFilter: (value, record) => value.includes(record.alt_transport),
render: (text, record) => (
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
{record.alt_transport}
</span>
),
},
{
title: t("jobs.fields.lab"),
dataIndex: "joblines_body",
key: "joblines_body",
sorter: (a, b) => a.joblines_body - b.joblines_body,
sortOrder:
state.sortedInfo.columnKey === "joblines_body" &&
state.sortedInfo.order,
align: "right",
render: (text, record) => (
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
{record.joblines_body.toFixed(1)}
</span>
),
},
{
title: t("jobs.fields.lar"),
dataIndex: "joblines_ref",
key: "joblines_ref",
sorter: (a, b) => a.joblines_ref - b.joblines_ref,
sortOrder:
state.sortedInfo.columnKey === "joblines_ref" && state.sortedInfo.order,
align: "right",
render: (text, record) => (
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
{record.joblines_ref.toFixed(1)}
</span>
),
},
];
const columns = [ const columns = [
{
title: t("appointments.fields.time"),
dataIndex: "start",
key: "start",
ellipsis: true,
sorter: (a, b) => dateSort(a.start, b.start),
sortOrder:
state.sortedInfo.columnKey === "start" && state.sortedInfo.order,
render: (text, record) => <TimeFormatter>{record.start}</TimeFormatter>,
},
{ {
title: t("jobs.fields.ro_number"), title: t("jobs.fields.ro_number"),
dataIndex: "ro_number", dataIndex: "ro_number",
key: "ro_number", key: "ro_number",
sorter: (a, b) => alphaSort(a.ro_number, b.ro_number),
sortOrder:
state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
render: (text, record) => ( render: (text, record) => (
<Link <Link
to={"/manage/jobs/" + record.jobid} to={"/manage/jobs/" + record.jobid}
@@ -289,10 +86,7 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
dataIndex: "owner", dataIndex: "owner",
key: "owner", key: "owner",
ellipsis: true, ellipsis: true,
sorter: (a, b) => responsive: ["md"],
alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
sortOrder:
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
render: (text, record) => { render: (text, record) => {
return record.ownerid ? ( return record.ownerid ? (
<Link <Link
@@ -309,16 +103,23 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
}, },
}, },
{ {
title: t("dashboard.labels.phone"), title: t("jobs.fields.ownr_ph1"),
dataIndex: "ownr_ph", dataIndex: "ownr_ph1",
key: "ownr_ph", key: "ownr_ph1",
ellipsis: true, ellipsis: true,
responsive: ["md"], responsive: ["md"],
render: (text, record) => ( render: (text, record) => (
<Space size="small" wrap> <ChatOpenButton phone={record.ownr_ph1} jobid={record.jobid} />
<ChatOpenButton phone={record.ownr_ph1} jobid={record.jobid} /> ),
<ChatOpenButton phone={record.ownr_ph2} jobid={record.jobid} /> },
</Space> {
title: t("jobs.fields.ownr_ph2"),
dataIndex: "ownr_ph2",
key: "ownr_ph2",
ellipsis: true,
responsive: ["md"],
render: (text, record) => (
<ChatOpenButton phone={record.ownr_ph2} jobid={record.jobid} />
), ),
}, },
{ {
@@ -328,7 +129,7 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
ellipsis: true, ellipsis: true,
responsive: ["md"], responsive: ["md"],
render: (text, record) => ( render: (text, record) => (
<a href={`mailto:${record.ownr_ea}`}>{record.ownr_ea}</a> <ChatOpenButton phone={record.ownr_ea} jobid={record.jobid} />
), ),
}, },
{ {
@@ -336,15 +137,6 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
dataIndex: "vehicle", dataIndex: "vehicle",
key: "vehicle", key: "vehicle",
ellipsis: true, ellipsis: true,
sorter: (a, b) =>
alphaSort(
`${a.v_model_yr || ""} ${a.v_make_desc || ""} ${
a.v_model_desc || ""
}`,
`${b.v_model_yr || ""} ${b.v_make_desc || ""} ${b.v_model_desc || ""}`
),
sortOrder:
state.sortedInfo.columnKey === "vehicle" && state.sortedInfo.order,
render: (text, record) => { render: (text, record) => {
return record.vehicleid ? ( return record.vehicleid ? (
<Link <Link
@@ -368,80 +160,38 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
key: "ins_co_nm", key: "ins_co_nm",
ellipsis: true, ellipsis: true,
responsive: ["md"], responsive: ["md"],
sorter: (a, b) => alphaSort(a.ins_co_nm, b.ins_co_nm), },
sortOrder: {
state.sortedInfo.columnKey === "ins_co_nm" && state.sortedInfo.order, title: t("appointments.fields.time"),
filters: dataIndex: "start",
(appt && key: "start",
appt ellipsis: true,
.map((j) => j.ins_co_nm) responsive: ["md"],
.filter(onlyUnique)
.map((s) => {
return {
text: s || "No Ins. Co.*",
value: [s],
};
})
.sort((a, b) => alphaSort(a.text, b.text))) ||
[],
onFilter: (value, record) => value.includes(record.ins_co_nm),
}, },
{ {
title: t("appointments.fields.alt_transport"), title: t("appointments.fields.alt_transport"),
dataIndex: "alt_transport", dataIndex: "alt_transport",
key: "alt_transport", key: "alt_transport",
ellipsis: true, ellipsis: true,
sorter: (a, b) => alphaSort(a.alt_transport, b.alt_transport), responsive: ["md"],
sortOrder:
state.sortedInfo.columnKey === "alt_transport" &&
state.sortedInfo.order,
filters:
(appt &&
appt
.map((j) => j.alt_transport)
.filter(onlyUnique)
.map((s) => {
return {
text: s || "No Alt. Transport",
value: [s],
};
})
.sort((a, b) => alphaSort(a.text, b.text))) ||
[],
onFilter: (value, record) => value.includes(record.alt_transport),
}, },
]; ];
const handleTableChange = (pagination, filters, sorter) => { const handleTableChange = (sorter) => {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter }); setState({ ...state, sortedInfo: sorter });
}; };
return ( return (
<Card <Card title={t("dashboard.titles.scheduledintoday", {date: moment().startOf("day").format("MM/DD/YYYY")})} {...cardProps}>
title={t("dashboard.titles.scheduledindate", {
date: moment().startOf("day").format("MM/DD/YYYY"),
})}
extra={
<Space>
<Typography.Text>{t("general.labels.tvmode")}</Typography.Text>
<Switch
onClick={() => setIsTvModeScheduledIn(!isTvModeScheduledIn)}
defaultChecked={isTvModeScheduledIn}
/>
</Space>
}
{...cardProps}
>
<div style={{ height: "100%" }}> <div style={{ height: "100%" }}>
<Table <Table
onChange={handleTableChange} onChange={handleTableChange}
pagination={false} pagination={{ position: "top", defaultPageSize: 50 }}
columns={isTvModeScheduledIn ? tvColumns : columns} columns={columns}
scroll={{ x: true, y: "calc(100% - 2em)" }} scroll={{ x: true, y: "calc(100% - 2em)" }}
rowKey="id" rowKey="id"
style={{ height: "85%" }} style={{ height: "85%" }}
dataSource={appt} dataSource={appt}
size={isTvModeScheduledIn ? "small" : "middle"}
/> />
</div> </div>
</Card> </Card>
@@ -460,10 +210,6 @@ export const DashboardScheduledInTodayGql = `
alt_transport alt_transport
clm_no clm_no
jobid: id jobid: id
joblines(where: {removed: {_eq: false}}) {
mod_lb_hrs
mod_lbr_ty
}
ins_co_nm ins_co_nm
iouparent iouparent
ownerid ownerid

View File

@@ -3,272 +3,36 @@ import {
ExclamationCircleFilled, ExclamationCircleFilled,
PauseCircleOutlined, PauseCircleOutlined,
} from "@ant-design/icons"; } from "@ant-design/icons";
import { Card, Space, Switch, Table, Tooltip, Typography } from "antd"; import { Card, Space, Table, Tooltip } from "antd";
import moment from "moment"; import moment from "moment";
import React, { useState } from "react"; import React, { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { TimeFormatter } from "../../../utils/DateFormatter";
import { onlyUnique } from "../../../utils/arrayHelper";
import { alphaSort, dateSort } from "../../../utils/sorters";
import useLocalStorage from "../../../utils/useLocalStorage";
import ChatOpenButton from "../../chat-open-button/chat-open-button.component"; import ChatOpenButton from "../../chat-open-button/chat-open-button.component";
import OwnerNameDisplay, { import OwnerNameDisplay from "../../owner-name-display/owner-name-display.component";
OwnerNameDisplayFunction,
} from "../../owner-name-display/owner-name-display.component";
import DashboardRefreshRequired from "../refresh-required.component"; import DashboardRefreshRequired from "../refresh-required.component";
export default function DashboardScheduledOutToday({ data, ...cardProps }) { export default function DashboardScheduledOutToday({ data, ...cardProps }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [state, setState] = useState({ const [state, setState] = useState({
sortedInfo: {}, sortedInfo: {},
filteredInfo: {},
}); });
const [isTvModeScheduledOut, setIsTvModeScheduledOut] = useLocalStorage(
"isTvModeScheduledOut",
false
);
if (!data) return null; if (!data) return null;
if (!data.scheduled_out_today) if (!data.scheduled_out_today)
return <DashboardRefreshRequired {...cardProps} />; return <DashboardRefreshRequired {...cardProps} />;
data.scheduled_out_today.forEach((item) => { data.scheduled_out_today.forEach((item) => {
item.joblines_body = item.joblines item.scheduled_completion= moment(item.scheduled_completion).format("hh:mm a")
? item.joblines
.filter((l) => l.mod_lbr_ty !== "LAR")
.reduce((acc, val) => acc + val.mod_lb_hrs, 0)
: 0;
item.joblines_ref = item.joblines
? item.joblines
.filter((l) => l.mod_lbr_ty === "LAR")
.reduce((acc, val) => acc + val.mod_lb_hrs, 0)
: 0;
}); });
data.scheduled_out_today.sort(function (a, b) { data.scheduled_out_today.sort(function (a, b) {
return new Date(a.scheduled_completion) - new Date(b.scheduled_completion); return new Date(a.scheduled_completion) - new Date(b.scheduled_completion);
}); });
const tvFontSize = 18;
const tvFontWeight = "bold";
const tvColumns = [
{
title: t("jobs.fields.scheduled_completion"),
dataIndex: "scheduled_completion",
key: "scheduled_completion",
ellipsis: true,
sorter: (a, b) =>
dateSort(a.scheduled_completion, b.scheduled_completion),
sortOrder:
state.sortedInfo.columnKey === "scheduled_completion" &&
state.sortedInfo.order,
render: (text, record) => (
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
<TimeFormatter>{record.scheduled_completion}</TimeFormatter>
</span>
),
},
{
title: t("jobs.fields.ro_number"),
dataIndex: "ro_number",
key: "ro_number",
sorter: (a, b) => alphaSort(a.ro_number, b.ro_number),
sortOrder:
state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
render: (text, record) => (
<Link
to={"/manage/jobs/" + record.jobid}
onClick={(e) => e.stopPropagation()}
>
<Space>
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
{record.ro_number || t("general.labels.na")}
{record.production_vars && record.production_vars.alert ? (
<ExclamationCircleFilled className="production-alert" />
) : null}
{record.suspended && (
<PauseCircleOutlined style={{ color: "orangered" }} />
)}
{record.iouparent && (
<Tooltip title={t("jobs.labels.iou")}>
<BranchesOutlined style={{ color: "orangered" }} />
</Tooltip>
)}
</span>
</Space>
</Link>
),
},
{
title: t("jobs.fields.owner"),
dataIndex: "owner",
key: "owner",
ellipsis: true,
sorter: (a, b) =>
alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
sortOrder:
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
render: (text, record) => {
return record.ownerid ? (
<Link
to={"/manage/owners/" + record.ownerid}
onClick={(e) => e.stopPropagation()}
>
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
<OwnerNameDisplay ownerObject={record} />
</span>
</Link>
) : (
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
<OwnerNameDisplay ownerObject={record} />
</span>
);
},
},
{
title: t("jobs.fields.vehicle"),
dataIndex: "vehicle",
key: "vehicle",
ellipsis: true,
sorter: (a, b) =>
alphaSort(
`${a.v_model_yr || ""} ${a.v_make_desc || ""} ${
a.v_model_desc || ""
}`,
`${b.v_model_yr || ""} ${b.v_make_desc || ""} ${b.v_model_desc || ""}`
),
sortOrder:
state.sortedInfo.columnKey === "vehicle" && state.sortedInfo.order,
render: (text, record) => {
return record.vehicleid ? (
<Link
to={"/manage/vehicles/" + record.vehicleid}
onClick={(e) => e.stopPropagation()}
>
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
record.v_model_desc || ""
}`}
</span>
</Link>
) : (
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>{`${
record.v_model_yr || ""
} ${record.v_make_desc || ""} ${record.v_model_desc || ""}`}</span>
);
},
},
{
title: t("appointments.fields.alt_transport"),
dataIndex: "alt_transport",
key: "alt_transport",
ellipsis: true,
sorter: (a, b) => alphaSort(a.alt_transport, b.alt_transport),
sortOrder:
state.sortedInfo.columnKey === "alt_transport" &&
state.sortedInfo.order,
filters:
(data.scheduled_out_today &&
data.scheduled_out_today
.map((j) => j.alt_transport)
.filter(onlyUnique)
.map((s) => {
return {
text: s || "No Alt. Transport*",
value: [s],
};
})
.sort((a, b) => alphaSort(a.text, b.text))) ||
[],
onFilter: (value, record) => value.includes(record.alt_transport),
render: (text, record) => (
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
{record.alt_transport}
</span>
),
},
{
title: t("jobs.fields.status"),
dataIndex: "status",
key: "status",
ellipsis: true,
sorter: (a, b) => alphaSort(a.alt_transport, b.alt_transport),
sortOrder:
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
filters:
(data.scheduled_out_today &&
data.scheduled_out_today
.map((j) => j.status)
.filter(onlyUnique)
.map((s) => {
return {
text: s || "No Status*",
value: [s],
};
})
.sort((a, b) => alphaSort(a.text, b.text))) ||
[],
onFilter: (value, record) => value.includes(record.status),
render: (text, record) => (
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
{record.status}
</span>
),
},
{
title: t("jobs.fields.lab"),
dataIndex: "joblines_body",
key: "joblines_body",
sorter: (a, b) => a.joblines_body - b.joblines_body,
sortOrder:
state.sortedInfo.columnKey === "joblines_body" &&
state.sortedInfo.order,
align: "right",
render: (text, record) => (
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
{record.joblines_body.toFixed(1)}
</span>
),
},
{
title: t("jobs.fields.lar"),
dataIndex: "joblines_ref",
key: "joblines_ref",
sorter: (a, b) => a.joblines_ref - b.joblines_ref,
sortOrder:
state.sortedInfo.columnKey === "joblines_ref" && state.sortedInfo.order,
align: "right",
render: (text, record) => (
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
{record.joblines_ref.toFixed(1)}
</span>
),
},
];
const columns = [ const columns = [
{
title: t("jobs.fields.scheduled_completion"),
dataIndex: "scheduled_completion",
key: "scheduled_completion",
ellipsis: true,
sorter: (a, b) =>
dateSort(a.scheduled_completion, b.scheduled_completion),
sortOrder:
state.sortedInfo.columnKey === "scheduled_completion" &&
state.sortedInfo.order,
render: (text, record) => (
<TimeFormatter>{record.scheduled_completion}</TimeFormatter>
),
},
{ {
title: t("jobs.fields.ro_number"), title: t("jobs.fields.ro_number"),
dataIndex: "ro_number", dataIndex: "ro_number",
key: "ro_number", key: "ro_number",
sorter: (a, b) => alphaSort(a.ro_number, b.ro_number),
sortOrder:
state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
render: (text, record) => ( render: (text, record) => (
<Link <Link
to={"/manage/jobs/" + record.jobid} to={"/manage/jobs/" + record.jobid}
@@ -296,10 +60,7 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) {
dataIndex: "owner", dataIndex: "owner",
key: "owner", key: "owner",
ellipsis: true, ellipsis: true,
sorter: (a, b) => responsive: ["md"],
alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
sortOrder:
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
render: (text, record) => { render: (text, record) => {
return record.ownerid ? ( return record.ownerid ? (
<Link <Link
@@ -316,16 +77,23 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) {
}, },
}, },
{ {
title: t("dashboard.labels.phone"), title: t("jobs.fields.ownr_ph1"),
dataIndex: "ownr_ph", dataIndex: "ownr_ph1",
key: "ownr_ph", key: "ownr_ph1",
ellipsis: true, ellipsis: true,
responsive: ["md"], responsive: ["md"],
render: (text, record) => ( render: (text, record) => (
<Space size="small" wrap> <ChatOpenButton phone={record.ownr_ph1} jobid={record.jobid} />
<ChatOpenButton phone={record.ownr_ph1} jobid={record.jobid} /> ),
<ChatOpenButton phone={record.ownr_ph2} jobid={record.jobid} /> },
</Space> {
title: t("jobs.fields.ownr_ph2"),
dataIndex: "ownr_ph2",
key: "ownr_ph2",
ellipsis: true,
responsive: ["md"],
render: (text, record) => (
<ChatOpenButton phone={record.ownr_ph2} jobid={record.jobid} />
), ),
}, },
{ {
@@ -335,7 +103,7 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) {
ellipsis: true, ellipsis: true,
responsive: ["md"], responsive: ["md"],
render: (text, record) => ( render: (text, record) => (
<a href={`mailto:${record.ownr_ea}`}>{record.ownr_ea}</a> <ChatOpenButton phone={record.ownr_ea} jobid={record.jobid} />
), ),
}, },
{ {
@@ -343,15 +111,6 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) {
dataIndex: "vehicle", dataIndex: "vehicle",
key: "vehicle", key: "vehicle",
ellipsis: true, ellipsis: true,
sorter: (a, b) =>
alphaSort(
`${a.v_model_yr || ""} ${a.v_make_desc || ""} ${
a.v_model_desc || ""
}`,
`${b.v_model_yr || ""} ${b.v_make_desc || ""} ${b.v_model_desc || ""}`
),
sortOrder:
state.sortedInfo.columnKey === "vehicle" && state.sortedInfo.order,
render: (text, record) => { render: (text, record) => {
return record.vehicleid ? ( return record.vehicleid ? (
<Link <Link
@@ -375,80 +134,43 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) {
key: "ins_co_nm", key: "ins_co_nm",
ellipsis: true, ellipsis: true,
responsive: ["md"], responsive: ["md"],
sorter: (a, b) => alphaSort(a.ins_co_nm, b.ins_co_nm), },
sortOrder: {
state.sortedInfo.columnKey === "ins_co_nm" && state.sortedInfo.order, title: t("jobs.fields.scheduled_completion"),
filters: dataIndex: "scheduled_completion",
(data.scheduled_out_today && key: "scheduled_completion",
data.scheduled_out_today ellipsis: true,
.map((j) => j.ins_co_nm) responsive: ["md"],
.filter(onlyUnique)
.map((s) => {
return {
text: s || "No Ins. Co.*",
value: [s],
};
})
.sort((a, b) => alphaSort(a.text, b.text))) ||
[],
onFilter: (value, record) => value.includes(record.ins_co_nm),
}, },
{ {
title: t("appointments.fields.alt_transport"), title: t("appointments.fields.alt_transport"),
dataIndex: "alt_transport", dataIndex: "alt_transport",
key: "alt_transport", key: "alt_transport",
ellipsis: true, ellipsis: true,
sorter: (a, b) => alphaSort(a.alt_transport, b.alt_transport), responsive: ["md"],
sortOrder:
state.sortedInfo.columnKey === "alt_transport" &&
state.sortedInfo.order,
filters:
(data.scheduled_out_today &&
data.scheduled_out_today
.map((j) => j.alt_transport)
.filter(onlyUnique)
.map((s) => {
return {
text: s || "No Alt. Transport*",
value: [s],
};
})
.sort((a, b) => alphaSort(a.text, b.text))) ||
[],
onFilter: (value, record) => value.includes(record.alt_transport),
}, },
]; ];
const handleTableChange = (pagination, filters, sorter) => { const handleTableChange = (sorter) => {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter }); setState({ ...state, sortedInfo: sorter });
}; };
return ( return (
<Card <Card
title={t("dashboard.titles.scheduledoutdate", { title={t("dashboard.titles.scheduledouttoday", {
date: moment().startOf("day").format("MM/DD/YYYY"), date: moment().startOf("day").format("MM/DD/YYYY"),
})} })}
extra={
<Space>
<Typography.Text>{t("general.labels.tvmode")}</Typography.Text>
<Switch
onClick={() => setIsTvModeScheduledOut(!isTvModeScheduledOut)}
defaultChecked={isTvModeScheduledOut}
/>
</Space>
}
{...cardProps} {...cardProps}
> >
<div style={{ height: "100%" }}> <div style={{ height: "100%" }}>
<Table <Table
onChange={handleTableChange} onChange={handleTableChange}
pagination={false} pagination={{ position: "top", defaultPageSize: 50 }}
columns={isTvModeScheduledOut ? tvColumns : columns} columns={columns}
scroll={{ x: true, y: "calc(100% - 2em)" }} scroll={{ x: true, y: "calc(100% - 2em)" }}
rowKey="id" rowKey="id"
style={{ height: "85%" }} style={{ height: "85%" }}
dataSource={data.scheduled_out_today} dataSource={data.scheduled_out_today}
size={isTvModeScheduledOut ? "small" : "middle"}
/> />
</div> </div>
</Card> </Card>
@@ -465,10 +187,6 @@ export const DashboardScheduledOutTodayGql = `
alt_transport alt_transport
clm_no clm_no
jobid: id jobid: id
joblines(where: {removed: {_eq: false}}) {
mod_lb_hrs
mod_lbr_ty
}
ins_co_nm ins_co_nm
iouparent iouparent
ownerid ownerid
@@ -481,7 +199,6 @@ export const DashboardScheduledOutTodayGql = `
production_vars production_vars
ro_number ro_number
scheduled_completion scheduled_completion
status
suspended suspended
v_make_desc v_make_desc
v_model_desc v_model_desc

View File

@@ -1,391 +1,386 @@
import Icon, {SyncOutlined} from "@ant-design/icons"; import Icon, { SyncOutlined } from "@ant-design/icons";
import {gql, useMutation, useQuery} from "@apollo/client"; import { gql, useMutation, useQuery } from "@apollo/client";
import {Button, Dropdown, Menu, notification, PageHeader, Space} from "antd"; import { Button, Dropdown, Menu, PageHeader, Space, notification } from "antd";
import i18next from "i18next"; import i18next from "i18next";
import _ from "lodash"; import _ from "lodash";
import moment from "moment"; import moment from "moment";
import React, {useState} from "react"; import React, { useState } from "react";
import {Responsive, WidthProvider} from "react-grid-layout"; import { Responsive, WidthProvider } from "react-grid-layout";
import {useTranslation} from "react-i18next"; import { useTranslation } from "react-i18next";
import {MdClose} from "react-icons/md"; import { MdClose } from "react-icons/md";
import {connect} from "react-redux"; import { connect } from "react-redux";
import {createStructuredSelector} from "reselect"; import { createStructuredSelector } from "reselect";
import {logImEXEvent} from "../../firebase/firebase.utils"; import { logImEXEvent } from "../../firebase/firebase.utils";
import {UPDATE_DASHBOARD_LAYOUT} from "../../graphql/user.queries"; import { UPDATE_DASHBOARD_LAYOUT } from "../../graphql/user.queries";
import {selectBodyshop, selectCurrentUser,} from "../../redux/user/user.selectors"; import {
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
import AlertComponent from "../alert/alert.component"; import AlertComponent from "../alert/alert.component";
import DashboardMonthlyEmployeeEfficiency, { import DashboardMonthlyEmployeeEfficiency, {
DashboardMonthlyEmployeeEfficiencyGql, DashboardMonthlyEmployeeEfficiencyGql,
} from "../dashboard-components/monthly-employee-efficiency/monthly-employee-efficiency.component"; } from "../dashboard-components/monthly-employee-efficiency/monthly-employee-efficiency.component";
import DashboardMonthlyJobCosting from "../dashboard-components/monthly-job-costing/monthly-job-costing.component"; import DashboardMonthlyJobCosting from "../dashboard-components/monthly-job-costing/monthly-job-costing.component";
import DashboardMonthlyLaborSales from "../dashboard-components/monthly-labor-sales/monthly-labor-sales.component"; import DashboardMonthlyLaborSales from "../dashboard-components/monthly-labor-sales/monthly-labor-sales.component";
import DashboardMonthlyPartsSales from "../dashboard-components/monthly-parts-sales/monthly-parts-sales.component"; import DashboardMonthlyPartsSales from "../dashboard-components/monthly-parts-sales/monthly-parts-sales.component";
import DashboardMonthlyRevenueGraph, { import DashboardMonthlyRevenueGraph, {
DashboardMonthlyRevenueGraphGql, DashboardMonthlyRevenueGraphGql,
} from "../dashboard-components/monthly-revenue-graph/monthly-revenue-graph.component"; } from "../dashboard-components/monthly-revenue-graph/monthly-revenue-graph.component";
import DashboardProjectedMonthlySales, { import DashboardProjectedMonthlySales, {
DashboardProjectedMonthlySalesGql, DashboardProjectedMonthlySalesGql,
} from "../dashboard-components/pojected-monthly-sales/projected-monthly-sales.component"; } from "../dashboard-components/pojected-monthly-sales/projected-monthly-sales.component";
import DashboardTotalProductionDollars import DashboardTotalProductionDollars from "../dashboard-components/total-production-dollars/total-production-dollars.component";
from "../dashboard-components/total-production-dollars/total-production-dollars.component";
import DashboardTotalProductionHours, { import DashboardTotalProductionHours, {
DashboardTotalProductionHoursGql, DashboardTotalProductionHoursGql,
} from "../dashboard-components/total-production-hours/total-production-hours.component"; } from "../dashboard-components/total-production-hours/total-production-hours.component";
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component"; import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
//Combination of the following: //Combination of the following:
// /node_modules/react-grid-layout/css/styles.css // /node_modules/react-grid-layout/css/styles.css
// /node_modules/react-resizable/css/styles.css // /node_modules/react-resizable/css/styles.css
import DashboardScheduledInToday, { import DashboardScheduledInToday, {
DashboardScheduledInTodayGql, DashboardScheduledInTodayGql,
} from "../dashboard-components/scheduled-in-today/scheduled-in-today.component"; } from "../dashboard-components/scheduled-in-today/scheduled-in-today.component";
import DashboardScheduledOutToday, { import DashboardScheduledOutToday, {
DashboardScheduledOutTodayGql, DashboardScheduledOutTodayGql,
} from "../dashboard-components/scheduled-out-today/scheduled-out-today.component"; } from "../dashboard-components/scheduled-out-today/scheduled-out-today.component";
import JobLifecycleDashboardComponent, {
JobLifecycleDashboardGQL
} from "../dashboard-components/job-lifecycle/job-lifecycle-dashboard.component";
import "./dashboard-grid.styles.scss"; import "./dashboard-grid.styles.scss";
import {GenerateDashboardData} from "./dashboard-grid.utils"; import { GenerateDashboardData } from "./dashboard-grid.utils";
const ResponsiveReactGridLayout = WidthProvider(Responsive); const ResponsiveReactGridLayout = WidthProvider(Responsive);
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser, currentUser: selectCurrentUser,
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language)) //setUserLanguage: language => dispatch(setUserLanguage(language))
}); });
export function DashboardGridComponent({currentUser, bodyshop}) { export function DashboardGridComponent({ currentUser, bodyshop }) {
const {t} = useTranslation(); const { t } = useTranslation();
const [state, setState] = useState({ const [state, setState] = useState({
...(bodyshop.associations[0].user.dashboardlayout ...(bodyshop.associations[0].user.dashboardlayout
? bodyshop.associations[0].user.dashboardlayout ? bodyshop.associations[0].user.dashboardlayout
: {items: [], layout: {}, layouts: []}), : { items: [], layout: {}, layouts: [] }),
});
const { loading, error, data, refetch } = useQuery(
createDashboardQuery(state),
{ fetchPolicy: "network-only", nextFetchPolicy: "network-only" }
);
const [updateLayout] = useMutation(UPDATE_DASHBOARD_LAYOUT);
const handleLayoutChange = async (layout, layouts) => {
logImEXEvent("dashboard_change_layout");
setState({ ...state, layout, layouts });
const result = await updateLayout({
variables: {
email: currentUser.email,
layout: { ...state, layout, layouts },
},
}); });
if (!!result.errors) {
notification["error"]({
message: t("dashboard.errors.updatinglayout", {
message: JSON.stringify(result.errors),
}),
});
}
};
const handleRemoveComponent = (key) => {
logImEXEvent("dashboard_remove_component", { name: key });
const idxToRemove = state.items.findIndex((i) => i.i === key);
const {loading, error, data, refetch} = useQuery( const items = _.cloneDeep(state.items);
createDashboardQuery(state),
{fetchPolicy: "network-only", nextFetchPolicy: "network-only"}
);
const [updateLayout] = useMutation(UPDATE_DASHBOARD_LAYOUT); items.splice(idxToRemove, 1);
setState({ ...state, items });
};
const handleLayoutChange = async (layout, layouts) => { const handleAddComponent = (e) => {
logImEXEvent("dashboard_change_layout"); logImEXEvent("dashboard_add_component", { name: e });
setState({
...state,
items: [
...state.items,
{
i: e.key,
x: (state.items.length * 2) % (state.cols || 12),
y: 99, // puts it at the bottom
w: componentList[e.key].w || 2,
h: componentList[e.key].h || 2,
},
],
});
};
setState({...state, layout, layouts}); const dashboarddata = React.useMemo(
() => GenerateDashboardData(data),
[data]
);
const existingLayoutKeys = state.items.map((i) => i.i);
const addComponentOverlay = (
<Menu onClick={handleAddComponent}>
{Object.keys(componentList).map((key) => (
<Menu.Item
key={key}
value={key}
disabled={existingLayoutKeys.includes(key)}
>
{componentList[key].label}
</Menu.Item>
))}
</Menu>
);
const result = await updateLayout({ if (error) return <AlertComponent message={error.message} type="error" />;
variables: {
email: currentUser.email, return (
layout: {...state, layout, layouts}, <div>
}, <PageHeader
}); extra={
if (!!result.errors) { <Space>
notification["error"]({ <Button onClick={() => refetch()}>
message: t("dashboard.errors.updatinglayout", { <SyncOutlined />
message: JSON.stringify(result.errors), </Button>
}), <Dropdown overlay={addComponentOverlay} trigger={["click"]}>
}); <Button>{t("dashboard.actions.addcomponent")}</Button>
</Dropdown>
</Space>
} }
}; />
const handleRemoveComponent = (key) => {
logImEXEvent("dashboard_remove_component", {name: key});
const idxToRemove = state.items.findIndex((i) => i.i === key);
const items = _.cloneDeep(state.items); <ResponsiveReactGridLayout
className="layout"
items.splice(idxToRemove, 1); breakpoints={{ lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }}
setState({...state, items}); cols={{ lg: 12, md: 10, sm: 6, xs: 4, xxs: 2 }}
}; width="100%"
layouts={state.layouts}
const handleAddComponent = (e) => { onLayoutChange={handleLayoutChange}
logImEXEvent("dashboard_add_component", {name: e}); // onBreakpointChange={onBreakpointChange}
setState({ >
...state, {state.items.map((item, index) => {
items: [ const TheComponent = componentList[item.i].component;
...state.items, return (
{ <div
i: e.key, key={item.i}
x: (state.items.length * 2) % (state.cols || 12), data-grid={{
y: 99, // puts it at the bottom ...item,
w: componentList[e.key].w || 2, minH: componentList[item.i].minH || 1,
h: componentList[e.key].h || 2, minW: componentList[item.i].minW || 1,
}, }}
],
});
};
const dashboarddata = React.useMemo(
() => GenerateDashboardData(data),
[data]
);
const existingLayoutKeys = state.items.map((i) => i.i);
const addComponentOverlay = (
<Menu onClick={handleAddComponent}>
{Object.keys(componentList).map((key) => (
<Menu.Item
key={key}
value={key}
disabled={existingLayoutKeys.includes(key)}
>
{componentList[key].label}
</Menu.Item>
))}
</Menu>
);
if (error) return <AlertComponent message={error.message} type="error"/>;
return (
<div>
<PageHeader
extra={
<Space>
<Button onClick={() => refetch()}>
<SyncOutlined/>
</Button>
<Dropdown overlay={addComponentOverlay} trigger={["click"]}>
<Button>{t("dashboard.actions.addcomponent")}</Button>
</Dropdown>
</Space>
}
/>
<ResponsiveReactGridLayout
className="layout"
breakpoints={{lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0}}
cols={{lg: 12, md: 10, sm: 6, xs: 4, xxs: 2}}
width="100%"
layouts={state.layouts}
onLayoutChange={handleLayoutChange}
// onBreakpointChange={onBreakpointChange}
> >
{state.items.map((item, index) => { <LoadingSkeleton loading={loading}>
const TheComponent = componentList[item.i].component; <Icon
return ( component={MdClose}
<div key={item.i}
key={item.i} style={{
data-grid={{ position: "absolute",
...item, zIndex: "2",
minH: componentList[item.i].minH || 1, right: ".25rem",
minW: componentList[item.i].minW || 1, top: ".25rem",
}} cursor: "pointer",
> }}
<LoadingSkeleton loading={loading}> onClick={() => handleRemoveComponent(item.i)}
<Icon />
component={MdClose} <TheComponent className="dashboard-card" data={dashboarddata} />
key={item.i} </LoadingSkeleton>
style={{ </div>
position: "absolute", );
zIndex: "2", })}
right: ".25rem", </ResponsiveReactGridLayout>
top: ".25rem", </div>
cursor: "pointer", );
}}
onClick={() => handleRemoveComponent(item.i)}
/>
<TheComponent className="dashboard-card" bodyshop={bodyshop} data={dashboarddata}/>
</LoadingSkeleton>
</div>
);
})}
</ResponsiveReactGridLayout>
</div>
);
} }
export default connect( export default connect(
mapStateToProps, mapStateToProps,
mapDispatchToProps mapDispatchToProps
)(DashboardGridComponent); )(DashboardGridComponent);
const componentList = { const componentList = {
ProductionDollars: { ProductionDollars: {
label: i18next.t("dashboard.titles.productiondollars"), label: i18next.t("dashboard.titles.productiondollars"),
component: DashboardTotalProductionDollars, component: DashboardTotalProductionDollars,
gqlFragment: null, gqlFragment: null,
w: 1, w: 1,
h: 1, h: 1,
minW: 2, minW: 2,
minH: 1, minH: 1,
}, },
ProductionHours: { ProductionHours: {
label: i18next.t("dashboard.titles.productionhours"), label: i18next.t("dashboard.titles.productionhours"),
component: DashboardTotalProductionHours, component: DashboardTotalProductionHours,
gqlFragment: DashboardTotalProductionHoursGql, gqlFragment: DashboardTotalProductionHoursGql,
w: 3, w: 3,
h: 1, h: 1,
minW: 3, minW: 3,
minH: 1, minH: 1,
}, },
ProjectedMonthlySales: { ProjectedMonthlySales: {
label: i18next.t("dashboard.titles.projectedmonthlysales"), label: i18next.t("dashboard.titles.projectedmonthlysales"),
component: DashboardProjectedMonthlySales, component: DashboardProjectedMonthlySales,
gqlFragment: DashboardProjectedMonthlySalesGql, gqlFragment: DashboardProjectedMonthlySalesGql,
w: 2, w: 2,
h: 1, h: 1,
minW: 2, minW: 2,
minH: 1, minH: 1,
}, },
MonthlyRevenueGraph: { MonthlyRevenueGraph: {
label: i18next.t("dashboard.titles.monthlyrevenuegraph"), label: i18next.t("dashboard.titles.monthlyrevenuegraph"),
component: DashboardMonthlyRevenueGraph, component: DashboardMonthlyRevenueGraph,
gqlFragment: DashboardMonthlyRevenueGraphGql, gqlFragment: DashboardMonthlyRevenueGraphGql,
w: 4, w: 4,
h: 2, h: 2,
minW: 4, minW: 4,
minH: 2, minH: 2,
}, },
MonthlyJobCosting: { MonthlyJobCosting: {
label: i18next.t("dashboard.titles.monthlyjobcosting"), label: i18next.t("dashboard.titles.monthlyjobcosting"),
component: DashboardMonthlyJobCosting, component: DashboardMonthlyJobCosting,
gqlFragment: null, gqlFragment: null,
minW: 6, minW: 6,
minH: 3, minH: 3,
w: 6, w: 6,
h: 3, h: 3,
}, },
MonthlyPartsSales: { MonthlyPartsSales: {
label: i18next.t("dashboard.titles.monthlypartssales"), label: i18next.t("dashboard.titles.monthlypartssales"),
component: DashboardMonthlyPartsSales, component: DashboardMonthlyPartsSales,
gqlFragment: null, gqlFragment: null,
minW: 2, minW: 2,
minH: 2, minH: 2,
w: 2, w: 2,
h: 2, h: 2,
}, },
MonthlyLaborSales: { MonthlyLaborSales: {
label: i18next.t("dashboard.titles.monthlylaborsales"), label: i18next.t("dashboard.titles.monthlylaborsales"),
component: DashboardMonthlyLaborSales, component: DashboardMonthlyLaborSales,
gqlFragment: null, gqlFragment: null,
minW: 2, minW: 2,
minH: 2, minH: 2,
w: 2, w: 2,
h: 2, h: 2,
}, },
// Typo in Efficency should be Efficiency, but changing it would reset users dashboard settings MonthlyEmployeeEfficency: {
MonthlyEmployeeEfficency: { label: i18next.t("dashboard.titles.monthlyemployeeefficiency"),
label: i18next.t("dashboard.titles.monthlyemployeeefficiency"), component: DashboardMonthlyEmployeeEfficiency,
component: DashboardMonthlyEmployeeEfficiency, gqlFragment: DashboardMonthlyEmployeeEfficiencyGql,
gqlFragment: DashboardMonthlyEmployeeEfficiencyGql, minW: 2,
minW: 2, minH: 2,
minH: 2, w: 2,
w: 2, h: 2,
h: 2, },
}, ScheduleInToday: {
ScheduleInToday: { label: i18next.t("dashboard.titles.scheduledintoday", {
label: i18next.t("dashboard.titles.scheduledintoday"), date: moment().startOf("day").format("MM/DD/YYYY"),
component: DashboardScheduledInToday, }),
gqlFragment: DashboardScheduledInTodayGql, component: DashboardScheduledInToday,
minW: 6, gqlFragment: DashboardScheduledInTodayGql,
minH: 2, minW: 10,
w: 10, minH: 2,
h: 3, w: 10,
}, h: 2,
ScheduleOutToday: { },
label: i18next.t("dashboard.titles.scheduledouttoday"), ScheduleOutToday: {
component: DashboardScheduledOutToday, label: i18next.t("dashboard.titles.scheduledouttoday", {
gqlFragment: DashboardScheduledOutTodayGql, date: moment().startOf("day").format("MM/DD/YYYY"),
minW: 6, }),
minH: 2, component: DashboardScheduledOutToday,
w: 10, gqlFragment: DashboardScheduledOutTodayGql,
h: 3, minW: 10,
}, minH: 2,
JobLifecycle: { w: 10,
label: i18next.t("dashboard.titles.joblifecycle"), h: 2,
component: JobLifecycleDashboardComponent, },
gqlFragment: JobLifecycleDashboardGQL,
minW: 6,
minH: 3,
w: 6,
h: 3,
},
}; };
const createDashboardQuery = (state) => { const createDashboardQuery = (state) => {
const componentBasedAdditions = const componentBasedAdditions =
state && state &&
Array.isArray(state.layout) && Array.isArray(state.layout) &&
state.layout state.layout
.map((item, index) => componentList[item.i].gqlFragment || "") .map((item, index) => componentList[item.i].gqlFragment || "")
.join(""); .join("");
return gql` return gql`
query QUERY_DASHBOARD_DETAILS { ${componentBasedAdditions || ""} query QUERY_DASHBOARD_DETAILS {
monthly_sales: jobs(where: {_and: [ ${componentBasedAdditions || ""}
{ voided: {_eq: false}}, monthly_sales: jobs(where: {_and: [
{date_invoiced: {_gte: "${moment() { voided: {_eq: false}},
{date_invoiced: {_gte: "${moment()
.startOf("month") .startOf("month")
.startOf("day") .startOf("day")
.toISOString()}"}}, {date_invoiced: {_lte: "${moment() .toISOString()}"}}, {date_invoiced: {_lte: "${moment()
.endOf("month") .endOf("month")
.endOf("day") .endOf("day")
.toISOString()}"}}]}) { .toISOString()}"}}]}) {
id id
ro_number ro_number
date_invoiced date_invoiced
job_totals job_totals
rate_la1 rate_la1
rate_la2 rate_la2
rate_la3 rate_la3
rate_la4 rate_la4
rate_laa rate_laa
rate_lab rate_lab
rate_lad rate_lad
rate_lae rate_lae
rate_laf rate_laf
rate_lag rate_lag
rate_lam rate_lam
rate_lar rate_lar
rate_las rate_las
rate_lau rate_lau
rate_ma2s rate_ma2s
rate_ma2t rate_ma2t
rate_ma3s rate_ma3s
rate_mabl rate_mabl
rate_macs rate_macs
rate_mahw rate_mahw
rate_mapa rate_mapa
rate_mash rate_mash
rate_matd rate_matd
joblines(where: { removed: { _eq: false } }) { joblines(where: { removed: { _eq: false } }) {
id id
mod_lbr_ty mod_lbr_ty
mod_lb_hrs mod_lb_hrs
act_price act_price
part_qty part_qty
part_type part_type
} }
} }
production_jobs: jobs(where: { inproduction: { _eq: true } }) { production_jobs: jobs(where: { inproduction: { _eq: true } }) {
id id
ro_number ro_number
ins_co_nm ins_co_nm
job_totals job_totals
joblines(where: { removed: { _eq: false } }) { joblines(where: { removed: { _eq: false } }) {
id id
mod_lbr_ty mod_lbr_ty
mod_lb_hrs mod_lb_hrs
act_price act_price
part_qty part_qty
part_type part_type
} }
labhrs: joblines_aggregate(where: { mod_lbr_ty: { _neq: "LAR" }, removed: { _eq: false } }) { labhrs: joblines_aggregate(where: { mod_lbr_ty: { _neq: "LAR" }, removed: { _eq: false } }) {
aggregate { aggregate {
sum { sum {
mod_lb_hrs mod_lb_hrs
} }
} }
} }
larhrs: joblines_aggregate(where: { mod_lbr_ty: { _eq: "LAR" }, removed: { _eq: false } }) { larhrs: joblines_aggregate(where: { mod_lbr_ty: { _eq: "LAR" }, removed: { _eq: false } }) {
aggregate { aggregate {
sum { sum {
mod_lb_hrs mod_lb_hrs
} }
} }
} }
} }
}`; }
`;
}; };

View File

@@ -128,7 +128,7 @@
height: 100%; height: 100%;
width: 100%; width: 100%;
.ant-card-body { .ant-card-body {
height: calc(100% - 2rem); height: 80%;
width: 100%; width: 100%;
// // background-color: red; // // background-color: red;
// height: 90%; // height: 90%;

View File

@@ -18,7 +18,7 @@ export default function DataLabel({
<div {...props} style={{ display: "flex" }}> <div {...props} style={{ display: "flex" }}>
<div <div
style={{ style={{
// flex: 2, flex: 2,
marginRight: ".2rem", marginRight: ".2rem",
}} }}
> >

View File

@@ -5,7 +5,6 @@ import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import {pageLimit} from "../../utils/config";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser //currentUser: selectCurrentUser
@@ -118,7 +117,7 @@ export function DmsAllocationsSummaryAp({ socket, bodyshop, billids, title }) {
} }
> >
<Table <Table
pagination={{ position: "top", defaultPageSize: pageLimit }} pagination={{ position: "top", defaultPageSize: 50 }}
columns={columns} columns={columns}
rowKey={(record) => `${record.InvoiceNumber}${record.Account}`} rowKey={(record) => `${record.InvoiceNumber}${record.Account}`}
dataSource={allocationsSummary} dataSource={allocationsSummary}

View File

@@ -6,7 +6,6 @@ import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import Dinero from "dinero.js"; import Dinero from "dinero.js";
import { SyncOutlined } from "@ant-design/icons"; import { SyncOutlined } from "@ant-design/icons";
import {pageLimit} from "../../utils/config";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser //currentUser: selectCurrentUser
@@ -95,7 +94,7 @@ export function DmsAllocationsSummary({ socket, bodyshop, jobId, title }) {
<Alert type="warning" message={t("jobs.labels.dms.disablebillwip")} /> <Alert type="warning" message={t("jobs.labels.dms.disablebillwip")} />
)} )}
<Table <Table
pagination={{ position: "top", defaultPageSize: pageLimit }} pagination={{ position: "top", defaultPageSize: 50 }}
columns={columns} columns={columns}
rowKey="center" rowKey="center"
dataSource={allocationsSummary} dataSource={allocationsSummary}

View File

@@ -191,13 +191,6 @@ export function DmsPostForm({ bodyshop, socket, job, logsRef }) {
> >
<Switch /> <Switch />
</Form.Item> </Form.Item>
<Form.Item
name="dms_model_override"
label={t("jobs.fields.dms.dms_model_override")}
initialValue={false}
>
<Switch />
</Form.Item>
</Space> </Space>
</div> </div>
)} )}

View File

@@ -109,7 +109,7 @@ export function EmailOverlayContainer({
] ]
: []), : []),
], ],
media: selectedMedia.filter((m) => m.isSelected).map((m) => m.fullsize), media: selectedMedia.filter((m) => m.isSelected).map((m) => m.src),
//attachments, //attachments,
}); });
notification["success"]({ message: t("emails.successes.sent") }); notification["success"]({ message: t("emails.successes.sent") });

View File

@@ -2,7 +2,7 @@ import { Button, Col, Collapse, Result, Row, Space } from "antd";
import React from "react"; import React from "react";
import { withTranslation } from "react-i18next"; import { withTranslation } from "react-i18next";
import { logImEXEvent } from "../../firebase/firebase.utils"; import { logImEXEvent } from "../../firebase/firebase.utils";
import * as Sentry from "@sentry/react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { import {
@@ -138,6 +138,7 @@ class ErrorBoundary extends React.Component {
} }
} }
export default Sentry.withErrorBoundary( export default connect(
connect(mapStateToProps, mapDispatchToProps)(withTranslation()(ErrorBoundary)) mapStateToProps,
); mapDispatchToProps
)(withTranslation()(ErrorBoundary));

View File

@@ -65,17 +65,8 @@ export function FormDatePicker({
}); });
} }
if (_a.isValid() && onChange) { if (_a.isValid() && onChange)
if (onlyFuture) { onChange(isDateOnly ? _a.format("YYYY-MM-DD") : _a);
if (moment().subtract(1, "day").isBefore(_a)) {
onChange(isDateOnly ? _a.format("YYYY-MM-DD") : _a);
} else {
onChange(isDateOnly ? moment().format("YYYY-MM-DD") : moment());
}
} else {
onChange(isDateOnly ? _a.format("YYYY-MM-DD") : _a);
}
}
}; };
return ( return (

View File

@@ -1,9 +1,9 @@
import React, { forwardRef } from "react"; import React, { forwardRef } from "react";
//import DatePicker from "react-datepicker"; //import DatePicker from "react-datepicker";
//import "react-datepicker/src/stylesheets/datepicker.scss"; //import "react-datepicker/src/stylesheets/datepicker.scss";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
import { TimePicker } from "antd"; import { TimePicker } from "antd";
import moment from "moment"; import moment from "moment";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
//To be used as a form element only. //To be used as a form element only.
const DateTimePicker = ( const DateTimePicker = (
@@ -26,21 +26,20 @@ const DateTimePicker = (
value={value} value={value}
onBlur={onBlur} onBlur={onBlur}
onChange={onChange} onChange={onChange}
onlyFuture={onlyFuture}
isDateOnly={false} isDateOnly={false}
/> />
<TimePicker <TimePicker
value={value ? moment(value) : null} value={value ? moment(value) : null}
{...(onlyFuture && { {...(onlyFuture && {
disabledDate: (d) => moment().isAfter(d), disabledDate: (d) => moment().isAfter(d),
})} })}
onChange={onChange} onChange={onChange}
showSecond={false} showSecond={false}
minuteStep={15} minuteStep={15}
onBlur={onBlur} onBlur={onBlur}
format="hh:mm a" format="hh:mm a"
{...restProps} {...restProps}
/> />
</div> </div>
); );

View File

@@ -3,13 +3,16 @@ import axios from "axios";
import _ from "lodash"; import _ from "lodash";
import React, { useState } from "react"; import React, { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom"; import { Link, useHistory } from "react-router-dom";
import PhoneNumberFormatter from "../../utils/PhoneFormatter"; import PhoneNumberFormatter from "../../utils/PhoneFormatter";
import OwnerNameDisplay, { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component"; import OwnerNameDisplay, {
OwnerNameDisplayFunction,
} from "../owner-name-display/owner-name-display.component";
import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component"; import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component";
export default function GlobalSearchOs() { export default function GlobalSearchOs() {
const { t } = useTranslation(); const { t } = useTranslation();
const history = useHistory();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [data, setData] = useState(false); const [data, setData] = useState(false);
@@ -18,7 +21,7 @@ export default function GlobalSearchOs() {
try { try {
setLoading(true); setLoading(true);
const searchData = await axios.post("/search", { const searchData = await axios.post("/search", {
search: v search: v,
}); });
const resultsByType = { const resultsByType = {
@@ -26,7 +29,7 @@ export default function GlobalSearchOs() {
jobs: [], jobs: [],
bills: [], bills: [],
owners: [], owners: [],
vehicles: [] vehicles: [],
}; };
searchData.data.hits.hits.forEach((hit) => { searchData.data.hits.hits.forEach((hit) => {
@@ -47,14 +50,15 @@ export default function GlobalSearchOs() {
<span> <span>
<OwnerNameDisplay ownerObject={job} /> <OwnerNameDisplay ownerObject={job} />
</span> </span>
<span>{`${job.v_model_yr || ""} ${job.v_make_desc || ""} ${job.v_model_desc || ""}`}</span> <span>{`${job.v_model_yr || ""} ${
job.v_make_desc || ""
} ${job.v_model_desc || ""}`}</span>
<span>{`${job.clm_no || ""}`}</span> <span>{`${job.clm_no || ""}`}</span>
<span>{`${job.plate_no || ""}`}</span>
</Space> </Space>
</Link> </Link>
) ),
}; };
}) }),
}, },
{ {
label: renderTitle(t("menus.header.search.owners")), label: renderTitle(t("menus.header.search.owners")),
@@ -64,39 +68,53 @@ export default function GlobalSearchOs() {
value: OwnerNameDisplayFunction(owner), value: OwnerNameDisplayFunction(owner),
label: ( label: (
<Link to={`/manage/owners/${owner.id}`}> <Link to={`/manage/owners/${owner.id}`}>
<Space size="small" split={<Divider type="vertical" />} wrap> <Space
size="small"
split={<Divider type="vertical" />}
wrap
>
<span> <span>
<OwnerNameDisplay ownerObject={owner} /> <OwnerNameDisplay ownerObject={owner} />
</span> </span>
<PhoneNumberFormatter>{owner.ownr_ph1}</PhoneNumberFormatter> <PhoneNumberFormatter>
<PhoneNumberFormatter>{owner.ownr_ph2}</PhoneNumberFormatter> {owner.ownr_ph1}
</PhoneNumberFormatter>
<PhoneNumberFormatter>
{owner.ownr_ph2}
</PhoneNumberFormatter>
</Space> </Space>
</Link> </Link>
) ),
}; };
}) }),
}, },
{ {
label: renderTitle(t("menus.header.search.vehicles")), label: renderTitle(t("menus.header.search.vehicles")),
options: resultsByType.vehicles.map((vehicle) => { options: resultsByType.vehicles.map((vehicle) => {
return { return {
key: vehicle.id, key: vehicle.id,
value: `${vehicle.v_model_yr || ""} ${vehicle.v_make_desc || ""} ${vehicle.v_model_desc || ""}`, value: `${vehicle.v_model_yr || ""} ${
vehicle.v_make_desc || ""
} ${vehicle.v_model_desc || ""}`,
label: ( label: (
<Link to={`/manage/vehicles/${vehicle.id}`}> <Link to={`/manage/vehicles/${vehicle.id}`}>
<Space size="small" split={<Divider type="vertical" />}> <Space size="small" split={<Divider type="vertical" />}>
<span> <span>
{`${vehicle.v_model_yr || ""} ${vehicle.v_make_desc || ""} ${vehicle.v_model_desc || ""}`} {`${vehicle.v_model_yr || ""} ${
vehicle.v_make_desc || ""
} ${vehicle.v_model_desc || ""}`}
</span> </span>
<span>{vehicle.plate_no || ""}</span> <span>{vehicle.plate_no || ""}</span>
<span> <span>
<VehicleVinDisplay>{vehicle.v_vin || ""}</VehicleVinDisplay> <VehicleVinDisplay>
{vehicle.v_vin || ""}
</VehicleVinDisplay>
</span> </span>
</Space> </Space>
</Link> </Link>
) ),
}; };
}) }),
}, },
{ {
label: renderTitle(t("menus.header.search.payments")), label: renderTitle(t("menus.header.search.payments")),
@@ -114,9 +132,9 @@ export default function GlobalSearchOs() {
<span>{payment.transactionid || ""}</span> <span>{payment.transactionid || ""}</span>
</Space> </Space>
</Link> </Link>
) ),
}; };
}) }),
}, },
{ {
label: renderTitle(t("menus.header.search.bills")), label: renderTitle(t("menus.header.search.bills")),
@@ -132,10 +150,10 @@ export default function GlobalSearchOs() {
<span>{bill.date}</span> <span>{bill.date}</span>
</Space> </Space>
</Link> </Link>
) ),
}; };
}) }),
} },
// { // {
// label: renderTitle(t("menus.header.search.phonebook")), // label: renderTitle(t("menus.header.search.phonebook")),
// options: resultsByType.search_phonebook.map((pb) => { // options: resultsByType.search_phonebook.map((pb) => {
@@ -177,7 +195,15 @@ export default function GlobalSearchOs() {
}; };
return ( return (
<AutoComplete options={data} onSearch={handleSearch} defaultActiveFirstOption onClear={() => setData([])}> <AutoComplete
options={data}
onSearch={handleSearch}
defaultActiveFirstOption
onSelect={(val, opt) => {
history.push(opt.label.props.to);
}}
onClear={() => setData([])}
>
<Input.Search <Input.Search
size="large" size="large"
placeholder={t("general.labels.globalsearch")} placeholder={t("general.labels.globalsearch")}

View File

@@ -3,19 +3,28 @@ import { AutoComplete, Divider, Input, Space } from "antd";
import _ from "lodash"; import _ from "lodash";
import React from "react"; import React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom"; import { Link, useHistory } from "react-router-dom";
import { GLOBAL_SEARCH_QUERY } from "../../graphql/search.queries"; import { GLOBAL_SEARCH_QUERY } from "../../graphql/search.queries";
import PhoneNumberFormatter from "../../utils/PhoneFormatter"; import PhoneNumberFormatter from "../../utils/PhoneFormatter";
import AlertComponent from "../alert/alert.component"; import AlertComponent from "../alert/alert.component";
import OwnerNameDisplay, { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component"; import OwnerNameDisplay, {
OwnerNameDisplayFunction,
} from "../owner-name-display/owner-name-display.component";
import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component"; import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component";
export default function GlobalSearch() { export default function GlobalSearch() {
const { t } = useTranslation(); const { t } = useTranslation();
const [callSearch, { loading, error, data }] = useLazyQuery(GLOBAL_SEARCH_QUERY); const history = useHistory();
const [callSearch, { loading, error, data }] =
useLazyQuery(GLOBAL_SEARCH_QUERY);
const executeSearch = (v) => { const executeSearch = (v) => {
if (v && v.variables.search && v.variables.search !== "" && v.variables.search.length >= 3) callSearch(v); if (
v &&
v.variables.search &&
v.variables.search !== "" &&
v.variables.search.length >= 3
)
callSearch(v);
}; };
const debouncedExecuteSearch = _.debounce(executeSearch, 750); const debouncedExecuteSearch = _.debounce(executeSearch, 750);
@@ -44,13 +53,15 @@ export default function GlobalSearch() {
<span> <span>
<OwnerNameDisplay ownerObject={job} /> <OwnerNameDisplay ownerObject={job} />
</span> </span>
<span>{`${job.v_model_yr || ""} ${job.v_make_desc || ""} ${job.v_model_desc || ""}`}</span> <span>{`${job.v_model_yr || ""} ${job.v_make_desc || ""} ${
job.v_model_desc || ""
}`}</span>
<span>{`${job.clm_no || ""}`}</span> <span>{`${job.clm_no || ""}`}</span>
</Space> </Space>
</Link> </Link>
) ),
}; };
}) }),
}, },
{ {
label: renderTitle(t("menus.header.search.owners")), label: renderTitle(t("menus.header.search.owners")),
@@ -64,35 +75,45 @@ export default function GlobalSearch() {
<span> <span>
<OwnerNameDisplay ownerObject={owner} /> <OwnerNameDisplay ownerObject={owner} />
</span> </span>
<PhoneNumberFormatter>{owner.ownr_ph1}</PhoneNumberFormatter> <PhoneNumberFormatter>
<PhoneNumberFormatter>{owner.ownr_ph2}</PhoneNumberFormatter> {owner.ownr_ph1}
</PhoneNumberFormatter>
<PhoneNumberFormatter>
{owner.ownr_ph2}
</PhoneNumberFormatter>
</Space> </Space>
</Link> </Link>
) ),
}; };
}) }),
}, },
{ {
label: renderTitle(t("menus.header.search.vehicles")), label: renderTitle(t("menus.header.search.vehicles")),
options: data.search_vehicles.map((vehicle) => { options: data.search_vehicles.map((vehicle) => {
return { return {
key: vehicle.id, key: vehicle.id,
value: `${vehicle.v_model_yr || ""} ${vehicle.v_make_desc || ""} ${vehicle.v_model_desc || ""}`, value: `${vehicle.v_model_yr || ""} ${
vehicle.v_make_desc || ""
} ${vehicle.v_model_desc || ""}`,
label: ( label: (
<Link to={`/manage/vehicles/${vehicle.id}`}> <Link to={`/manage/vehicles/${vehicle.id}`}>
<Space size="small" split={<Divider type="vertical" />}> <Space size="small" split={<Divider type="vertical" />}>
<span> <span>
{`${vehicle.v_model_yr || ""} ${vehicle.v_make_desc || ""} ${vehicle.v_model_desc || ""}`} {`${vehicle.v_model_yr || ""} ${
vehicle.v_make_desc || ""
} ${vehicle.v_model_desc || ""}`}
</span> </span>
<span>{vehicle.plate_no || ""}</span> <span>{vehicle.plate_no || ""}</span>
<span> <span>
<VehicleVinDisplay>{vehicle.v_vin || ""}</VehicleVinDisplay> <VehicleVinDisplay>
{vehicle.v_vin || ""}
</VehicleVinDisplay>
</span> </span>
</Space> </Space>
</Link> </Link>
) ),
}; };
}) }),
}, },
{ {
label: renderTitle(t("menus.header.search.payments")), label: renderTitle(t("menus.header.search.payments")),
@@ -110,9 +131,9 @@ export default function GlobalSearch() {
<span>{payment.transactionid || ""}</span> <span>{payment.transactionid || ""}</span>
</Space> </Space>
</Link> </Link>
) ),
}; };
}) }),
}, },
{ {
label: renderTitle(t("menus.header.search.bills")), label: renderTitle(t("menus.header.search.bills")),
@@ -128,35 +149,46 @@ export default function GlobalSearch() {
<span>{bill.date}</span> <span>{bill.date}</span>
</Space> </Space>
</Link> </Link>
) ),
}; };
}) }),
}, },
{ {
label: renderTitle(t("menus.header.search.phonebook")), label: renderTitle(t("menus.header.search.phonebook")),
options: data.search_phonebook.map((pb) => { options: data.search_phonebook.map((pb) => {
return { return {
key: pb.id, key: pb.id,
value: `${pb.firstname || ""} ${pb.lastname || ""} ${pb.company || ""}`, value: `${pb.firstname || ""} ${pb.lastname || ""} ${
pb.company || ""
}`,
label: ( label: (
<Link to={`/manage/phonebook?phonebookentry=${pb.id}`}> <Link to={`/manage/phonebook?phonebookentry=${pb.id}`}>
<Space size="small" split={<Divider type="vertical" />}> <Space size="small" split={<Divider type="vertical" />}>
<span>{`${pb.firstname || ""} ${pb.lastname || ""} ${pb.company || ""}`}</span> <span>{`${pb.firstname || ""} ${pb.lastname || ""} ${
pb.company || ""
}`}</span>
<PhoneNumberFormatter>{pb.phone1}</PhoneNumberFormatter> <PhoneNumberFormatter>{pb.phone1}</PhoneNumberFormatter>
<span>{pb.email}</span> <span>{pb.email}</span>
</Space> </Space>
</Link> </Link>
) ),
}; };
}) }),
} },
] ]
: []; : [];
if (error) return <AlertComponent message={error.message} type="error" />; if (error) return <AlertComponent message={error.message} type="error" />;
return ( return (
<AutoComplete options={options} onSearch={handleSearch} defaultActiveFirstOption> <AutoComplete
options={options}
onSearch={handleSearch}
defaultActiveFirstOption
onSelect={(val, opt) => {
history.push(opt.label.props.to);
}}
>
<Input.Search <Input.Search
size="large" size="large"
placeholder={t("general.labels.globalsearch")} placeholder={t("general.labels.globalsearch")}

View File

@@ -1,9 +1,10 @@
import { useTreatments } from "@splitsoftware/splitio-react";
import Icon, { import Icon, {
BankFilled, BankFilled,
BarChartOutlined, BarChartOutlined,
CarFilled, CarFilled,
CheckCircleOutlined,
ClockCircleFilled, ClockCircleFilled,
CheckCircleOutlined,
DashboardFilled, DashboardFilled,
DollarCircleFilled, DollarCircleFilled,
ExportOutlined, ExportOutlined,
@@ -11,6 +12,7 @@ import Icon, {
FileAddFilled, FileAddFilled,
FileAddOutlined, FileAddOutlined,
FileFilled, FileFilled,
//GlobalOutlined,
HomeFilled, HomeFilled,
ImportOutlined, ImportOutlined,
LineChartOutlined, LineChartOutlined,
@@ -22,39 +24,52 @@ import Icon, {
TeamOutlined, TeamOutlined,
ToolFilled, ToolFilled,
UnorderedListOutlined, UnorderedListOutlined,
UserOutlined UserOutlined,
} from "@ant-design/icons"; } from "@ant-design/icons";
import { useTreatments } from "@splitsoftware/splitio-react";
import { Layout, Menu } from "antd"; import { Layout, Menu } from "antd";
import React from "react"; import React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { BsKanban } from "react-icons/bs"; import { BsKanban } from "react-icons/bs";
import { FaCalendarAlt, FaCarCrash, FaCreditCard, FaFileInvoiceDollar } from "react-icons/fa"; import {
FaCalendarAlt,
FaCarCrash,
FaCreditCard,
FaFileInvoiceDollar,
} from "react-icons/fa";
import { GiPayMoney, GiPlayerTime, GiSettingsKnobs } from "react-icons/gi"; import { GiPayMoney, GiPlayerTime, GiSettingsKnobs } from "react-icons/gi";
import { IoBusinessOutline } from "react-icons/io5"; import { IoBusinessOutline } from "react-icons/io5";
import { RiSurveyLine } from "react-icons/ri"; import { RiSurveyLine } from "react-icons/ri";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { selectRecentItems, selectSelectedHeader } from "../../redux/application/application.selectors"; import {
selectRecentItems,
selectSelectedHeader,
} from "../../redux/application/application.selectors";
import { setModalContext } from "../../redux/modals/modals.actions"; import { setModalContext } from "../../redux/modals/modals.actions";
import { signOutStart } from "../../redux/user/user.actions"; import { signOutStart } from "../../redux/user/user.actions";
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors"; import {
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser, currentUser: selectCurrentUser,
recentItems: selectRecentItems, recentItems: selectRecentItems,
selectedHeader: selectSelectedHeader, selectedHeader: selectSelectedHeader,
bodyshop: selectBodyshop bodyshop: selectBodyshop,
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
setBillEnterContext: (context) => dispatch(setModalContext({ context: context, modal: "billEnter" })), setBillEnterContext: (context) =>
setTimeTicketContext: (context) => dispatch(setModalContext({ context: context, modal: "timeTicket" })), dispatch(setModalContext({ context: context, modal: "billEnter" })),
setPaymentContext: (context) => dispatch(setModalContext({ context: context, modal: "payment" })), setTimeTicketContext: (context) =>
setReportCenterContext: (context) => dispatch(setModalContext({ context: context, modal: "reportCenter" })), dispatch(setModalContext({ context: context, modal: "timeTicket" })),
setPaymentContext: (context) =>
dispatch(setModalContext({ context: context, modal: "payment" })),
setReportCenterContext: (context) =>
dispatch(setModalContext({ context: context, modal: "reportCenter" })),
signOutStart: () => dispatch(signOutStart()), signOutStart: () => dispatch(signOutStart()),
setCardPaymentContext: (context) => dispatch(setModalContext({ context: context, modal: "cardPayment" }))
}); });
function Header({ function Header({
@@ -68,27 +83,20 @@ function Header({
setPaymentContext, setPaymentContext,
setReportCenterContext, setReportCenterContext,
recentItems, recentItems,
setCardPaymentContext
}) { }) {
const { Simple_Inventory } = useTreatments(["Simple_Inventory"], {}, bodyshop && bodyshop.imexshopid); const { Simple_Inventory } = useTreatments(
const { DmsAp } = useTreatments(["DmsAp"], {}, bodyshop && bodyshop.imexshopid); ["Simple_Inventory"],
const { ImEXPay } = useTreatments(["ImEXPay"], {}, bodyshop && bodyshop.imexshopid); {},
bodyshop && bodyshop.imexshopid
);
const { DmsAp } = useTreatments(
["DmsAp"],
{},
bodyshop && bodyshop.imexshopid
);
const { t } = useTranslation(); const { t } = useTranslation();
const deleteBetaCookie = () => {
const cookieExists = document.cookie.split("; ").some((row) => row.startsWith(`betaSwitchImex=`));
if (cookieExists) {
const domain = window.location.hostname.split(".").slice(-2).join(".");
document.cookie = `betaSwitchImex=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; domain=.${domain}`;
console.log(`betaSwitchImex cookie deleted`);
} else {
console.log(`betaSwitchImex cookie does not exist`);
}
};
deleteBetaCookie();
return ( return (
<Layout.Header> <Layout.Header>
<Menu <Menu
@@ -105,7 +113,11 @@ function Header({
<Menu.Item key="schedule" icon={<Icon component={FaCalendarAlt} />}> <Menu.Item key="schedule" icon={<Icon component={FaCalendarAlt} />}>
<Link to="/manage/schedule">{t("menus.header.schedule")}</Link> <Link to="/manage/schedule">{t("menus.header.schedule")}</Link>
</Menu.Item> </Menu.Item>
<Menu.SubMenu key="jobssubmenu" icon={<Icon component={FaCarCrash} />} title={t("menus.header.jobs")}> <Menu.SubMenu
key="jobssubmenu"
icon={<Icon component={FaCarCrash} />}
title={t("menus.header.jobs")}
>
<Menu.Item key="activejobs" icon={<FileFilled />}> <Menu.Item key="activejobs" icon={<FileFilled />}>
<Link to="/manage/jobs">{t("menus.header.activejobs")}</Link> <Link to="/manage/jobs">{t("menus.header.activejobs")}</Link>
</Menu.Item> </Menu.Item>
@@ -116,7 +128,9 @@ function Header({
<Link to="/manage/partsqueue">{t("menus.header.parts-queue")}</Link> <Link to="/manage/partsqueue">{t("menus.header.parts-queue")}</Link>
</Menu.Item> </Menu.Item>
<Menu.Item key="availablejobs" icon={<ImportOutlined />}> <Menu.Item key="availablejobs" icon={<ImportOutlined />}>
<Link to="/manage/available">{t("menus.header.availablejobs")}</Link> <Link to="/manage/available">
{t("menus.header.availablejobs")}
</Link>
</Menu.Item> </Menu.Item>
<Menu.Item key="newjob" icon={<FileAddOutlined />}> <Menu.Item key="newjob" icon={<FileAddOutlined />}>
<Link to="/manage/jobs/new">{t("menus.header.newjob")}</Link> <Link to="/manage/jobs/new">{t("menus.header.newjob")}</Link>
@@ -127,17 +141,25 @@ function Header({
</Menu.Item> </Menu.Item>
<Menu.Divider key="div2" /> <Menu.Divider key="div2" />
<Menu.Item key="productionlist" icon={<ScheduleOutlined />}> <Menu.Item key="productionlist" icon={<ScheduleOutlined />}>
<Link to="/manage/production/list">{t("menus.header.productionlist")}</Link> <Link to="/manage/production/list">
{t("menus.header.productionlist")}
</Link>
</Menu.Item> </Menu.Item>
<Menu.Item key="productionboard" icon={<Icon component={BsKanban} />}> <Menu.Item key="productionboard" icon={<Icon component={BsKanban} />}>
<Link to="/manage/production/board">{t("menus.header.productionboard")}</Link> <Link to="/manage/production/board">
{t("menus.header.productionboard")}
</Link>
</Menu.Item> </Menu.Item>
<Menu.Divider key="div3" /> <Menu.Divider key="div3" />
<Menu.Item key="scoreboard" icon={<LineChartOutlined />}> <Menu.Item key="scoreboard" icon={<LineChartOutlined />}>
<Link to="/manage/scoreboard">{t("menus.header.scoreboard")}</Link> <Link to="/manage/scoreboard">{t("menus.header.scoreboard")}</Link>
</Menu.Item> </Menu.Item>
</Menu.SubMenu> </Menu.SubMenu>
<Menu.SubMenu key="customers" icon={<UserOutlined />} title={t("menus.header.customers")}> <Menu.SubMenu
key="customers"
icon={<UserOutlined />}
title={t("menus.header.customers")}
>
<Menu.Item key="owners" icon={<TeamOutlined />}> <Menu.Item key="owners" icon={<TeamOutlined />}>
<Link to="/manage/owners">{t("menus.header.owners")}</Link> <Link to="/manage/owners">{t("menus.header.owners")}</Link>
</Menu.Item> </Menu.Item>
@@ -145,19 +167,36 @@ function Header({
<Link to="/manage/vehicles">{t("menus.header.vehicles")}</Link> <Link to="/manage/vehicles">{t("menus.header.vehicles")}</Link>
</Menu.Item> </Menu.Item>
</Menu.SubMenu> </Menu.SubMenu>
<Menu.SubMenu key="ccs" icon={<CarFilled />} title={t("menus.header.courtesycars")}> <Menu.SubMenu
key="ccs"
icon={<CarFilled />}
title={t("menus.header.courtesycars")}
>
<Menu.Item key="courtesycarsall" icon={<CarFilled />}> <Menu.Item key="courtesycarsall" icon={<CarFilled />}>
<Link to="/manage/courtesycars">{t("menus.header.courtesycars-all")}</Link> <Link to="/manage/courtesycars">
{t("menus.header.courtesycars-all")}
</Link>
</Menu.Item> </Menu.Item>
<Menu.Item key="contracts" icon={<FileFilled />}> <Menu.Item key="contracts" icon={<FileFilled />}>
<Link to="/manage/courtesycars/contracts">{t("menus.header.courtesycars-contracts")}</Link> <Link to="/manage/courtesycars/contracts">
{t("menus.header.courtesycars-contracts")}
</Link>
</Menu.Item> </Menu.Item>
<Menu.Item key="newcontract" icon={<FileAddFilled />}> <Menu.Item key="newcontract" icon={<FileAddFilled />}>
<Link to="/manage/courtesycars/contracts/new">{t("menus.header.courtesycars-newcontract")}</Link> <Link to="/manage/courtesycars/contracts/new">
{t("menus.header.courtesycars-newcontract")}
</Link>
</Menu.Item> </Menu.Item>
</Menu.SubMenu> </Menu.SubMenu>
<Menu.SubMenu key="accounting" icon={<DollarCircleFilled />} title={t("menus.header.accounting")}> <Menu.SubMenu
<Menu.Item key="bills" icon={<Icon component={FaFileInvoiceDollar} />}> key="accounting"
icon={<DollarCircleFilled />}
title={t("menus.header.accounting")}
>
<Menu.Item
key="bills"
icon={<Icon component={FaFileInvoiceDollar} />}
>
<Link to="/manage/bills">{t("menus.header.bills")}</Link> <Link to="/manage/bills">{t("menus.header.bills")}</Link>
</Menu.Item> </Menu.Item>
<Menu.Item <Menu.Item
@@ -166,7 +205,7 @@ function Header({
onClick={() => { onClick={() => {
setBillEnterContext({ setBillEnterContext({
actions: {}, actions: {},
context: {} context: {},
}); });
}} }}
> >
@@ -175,8 +214,13 @@ function Header({
{Simple_Inventory.treatment === "on" && ( {Simple_Inventory.treatment === "on" && (
<> <>
<Menu.Divider key="div4" /> <Menu.Divider key="div4" />
<Menu.Item key="inventory" icon={<Icon component={FaFileInvoiceDollar} />}> <Menu.Item
<Link to="/manage/inventory">{t("menus.header.inventory")}</Link> key="inventory"
icon={<Icon component={FaFileInvoiceDollar} />}
>
<Link to="/manage/inventory">
{t("menus.header.inventory")}
</Link>
</Menu.Item> </Menu.Item>
</> </>
)} )}
@@ -189,30 +233,18 @@ function Header({
onClick={() => { onClick={() => {
setPaymentContext({ setPaymentContext({
actions: {}, actions: {},
context: null context: null,
}); });
}} }}
icon={<Icon component={FaCreditCard} />} icon={<Icon component={FaCreditCard} />}
> >
{t("menus.header.enterpayment")} {t("menus.header.enterpayment")}
</Menu.Item> </Menu.Item>
{ImEXPay.treatment === "on" && (
<Menu.Item
key="entercardpayments"
onClick={() => {
setCardPaymentContext({
actions: {},
context: {}
});
}}
icon={<Icon component={FaCreditCard} />}
>
{t("menus.header.entercardpayment")}
</Menu.Item>
)}
<Menu.Divider key="div5" /> <Menu.Divider key="div5" />
<Menu.Item key="timetickets" icon={<FieldTimeOutlined />}> <Menu.Item key="timetickets" icon={<FieldTimeOutlined />}>
<Link to="/manage/timetickets">{t("menus.header.timetickets")}</Link> <Link to="/manage/timetickets">
{t("menus.header.timetickets")}
</Link>
</Menu.Item> </Menu.Item>
<Menu.Item <Menu.Item
key="entertimetickets" key="entertimetickets"
@@ -220,34 +252,48 @@ function Header({
onClick={() => { onClick={() => {
setTimeTicketContext({ setTimeTicketContext({
actions: {}, actions: {},
context: { context: {},
created_by: currentUser.displayName
? currentUser.email.concat(" | ", currentUser.displayName)
: currentUser.email
}
}); });
}} }}
> >
{t("menus.header.entertimeticket")} {t("menus.header.entertimeticket")}
</Menu.Item> </Menu.Item>
<Menu.Divider key="div6" /> <Menu.Divider key="div6" />
<Menu.SubMenu key="accountingexport" title={t("menus.header.export")} icon={<ExportOutlined />}> <Menu.SubMenu
key="accountingexport"
title={t("menus.header.export")}
icon={<ExportOutlined />}
>
<Menu.Item key="receivables"> <Menu.Item key="receivables">
<Link to="/manage/accounting/receivables">{t("menus.header.accounting-receivables")}</Link> <Link to="/manage/accounting/receivables">
{t("menus.header.accounting-receivables")}
</Link>
</Menu.Item> </Menu.Item>
{(!((bodyshop && bodyshop.cdk_dealerid) || (bodyshop && bodyshop.pbs_serialnumber)) || {(!(
(bodyshop && bodyshop.cdk_dealerid) ||
(bodyshop && bodyshop.pbs_serialnumber)
) ||
DmsAp.treatment === "on") && ( DmsAp.treatment === "on") && (
<Menu.Item key="payables"> <Menu.Item key="payables">
<Link to="/manage/accounting/payables">{t("menus.header.accounting-payables")}</Link> <Link to="/manage/accounting/payables">
{t("menus.header.accounting-payables")}
</Link>
</Menu.Item> </Menu.Item>
)} )}
{!((bodyshop && bodyshop.cdk_dealerid) || (bodyshop && bodyshop.pbs_serialnumber)) && ( {!(
(bodyshop && bodyshop.cdk_dealerid) ||
(bodyshop && bodyshop.pbs_serialnumber)
) && (
<Menu.Item key="payments"> <Menu.Item key="payments">
<Link to="/manage/accounting/payments">{t("menus.header.accounting-payments")}</Link> <Link to="/manage/accounting/payments">
{t("menus.header.accounting-payments")}
</Link>
</Menu.Item> </Menu.Item>
)} )}
<Menu.Item key="export-logs"> <Menu.Item key="export-logs">
<Link to="/manage/accounting/exportlogs">{t("menus.header.export-logs")}</Link> <Link to="/manage/accounting/exportlogs">
{t("menus.header.export-logs")}
</Link>
</Menu.Item> </Menu.Item>
</Menu.SubMenu> </Menu.SubMenu>
</Menu.SubMenu> </Menu.SubMenu>
@@ -255,11 +301,19 @@ function Header({
<Link to="/manage/phonebook">{t("menus.header.phonebook")}</Link> <Link to="/manage/phonebook">{t("menus.header.phonebook")}</Link>
</Menu.Item> </Menu.Item>
<Menu.Item key="temporarydocs" icon={<PaperClipOutlined />}> <Menu.Item key="temporarydocs" icon={<PaperClipOutlined />}>
<Link to="/manage/temporarydocs">{t("menus.header.temporarydocs")}</Link> <Link to="/manage/temporarydocs">
{t("menus.header.temporarydocs")}
</Link>
</Menu.Item> </Menu.Item>
<Menu.SubMenu key="shopsubmenu" title={t("menus.header.shop")} icon={<SettingOutlined />}> <Menu.SubMenu
key="shopsubmenu"
title={t("menus.header.shop")}
icon={<SettingOutlined />}
>
<Menu.Item key="shop" icon={<Icon component={GiSettingsKnobs} />}> <Menu.Item key="shop" icon={<Icon component={GiSettingsKnobs} />}>
<Link to="/manage/shop?tab=info">{t("menus.header.shop_config")}</Link> <Link to="/manage/shop?tab=info">
{t("menus.header.shop_config")}
</Link>
</Menu.Item> </Menu.Item>
<Menu.Item key="dashboard" icon={<DashboardFilled />}> <Menu.Item key="dashboard" icon={<DashboardFilled />}>
<Link to="/manage/dashboard">{t("menus.header.dashboard")}</Link> <Link to="/manage/dashboard">{t("menus.header.dashboard")}</Link>
@@ -270,20 +324,32 @@ function Header({
onClick={() => { onClick={() => {
setReportCenterContext({ setReportCenterContext({
actions: {}, actions: {},
context: {} context: {},
}); });
}} }}
> >
{t("menus.header.reportcenter")} {t("menus.header.reportcenter")}
</Menu.Item> </Menu.Item>
<Menu.Item key="shop-vendors" icon={<Icon component={IoBusinessOutline} />}> <Menu.Item
<Link to="/manage/shop/vendors">{t("menus.header.shop_vendors")}</Link> key="shop-vendors"
icon={<Icon component={IoBusinessOutline} />}
>
<Link to="/manage/shop/vendors">
{t("menus.header.shop_vendors")}
</Link>
</Menu.Item> </Menu.Item>
<Menu.Item key="shop-csi" icon={<Icon component={RiSurveyLine} />}> <Menu.Item key="shop-csi" icon={<Icon component={RiSurveyLine} />}>
<Link to="/manage/shop/csi">{t("menus.header.shop_csi")}</Link> <Link to="/manage/shop/csi">{t("menus.header.shop_csi")}</Link>
</Menu.Item> </Menu.Item>
</Menu.SubMenu> </Menu.SubMenu>
<Menu.SubMenu key="user" title={currentUser.displayName || currentUser.email || t("general.labels.unknown")}> <Menu.SubMenu
key="user"
title={
currentUser.displayName ||
currentUser.email ||
t("general.labels.unknown")
}
>
<Menu.Item key="signout" danger onClick={() => signOutStart()}> <Menu.Item key="signout" danger onClick={() => signOutStart()}>
{t("user.actions.signout")} {t("user.actions.signout")}
</Menu.Item> </Menu.Item>

View File

@@ -11,7 +11,6 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter"; import CurrencyFormatter from "../../utils/CurrencyFormatter";
import InventoryBillRo from "../inventory-bill-ro/inventory-bill-ro.component"; import InventoryBillRo from "../inventory-bill-ro/inventory-bill-ro.component";
import InventoryLineDelete from "../inventory-line-delete/inventory-line-delete.component"; import InventoryLineDelete from "../inventory-line-delete/inventory-line-delete.component";
import {pageLimit} from "../../utils/config";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser //currentUser: selectCurrentUser
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
@@ -214,7 +213,7 @@ export function JobsList({
loading={loading} loading={loading}
pagination={{ pagination={{
position: "top", position: "top",
pageSize: pageLimit, pageSize: 25,
current: parseInt(page || 1), current: parseInt(page || 1),
total: total, total: total,
}} }}

View File

@@ -11,7 +11,6 @@ import {
} from "../../redux/application/application.actions"; } from "../../redux/application/application.actions";
import AlertComponent from "../alert/alert.component"; import AlertComponent from "../alert/alert.component";
import InventoryListPaginated from "./inventory-list.component"; import InventoryListPaginated from "./inventory-list.component";
import {pageLimit} from "../../utils/config";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
//bodyshop: selectBodyshop, //bodyshop: selectBodyshop,
@@ -33,8 +32,8 @@ export function InventoryList({ setBreadcrumbs, setSelectedHeader }) {
nextFetchPolicy: "network-only", nextFetchPolicy: "network-only",
variables: { variables: {
search: search || "", search: search || "",
offset: page ? (page - 1) * pageLimit : 0, offset: page ? (page - 1) * 25 : 0,
limit: pageLimit, limit: 25,
consumedIsNull: showall === "true" ? null : true, consumedIsNull: showall === "true" ? null : true,
order: [ order: [
{ {

View File

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

View File

@@ -4,7 +4,6 @@ import {
Divider, Divider,
Dropdown, Dropdown,
Form, Form,
Input,
Menu, Menu,
notification, notification,
Popover, Popover,
@@ -30,13 +29,11 @@ import { GenerateDocument } from "../../utils/RenderTemplate";
import { TemplateList } from "../../utils/TemplateConstants"; import { TemplateList } from "../../utils/TemplateConstants";
import ChatOpenButton from "../chat-open-button/chat-open-button.component"; import ChatOpenButton from "../chat-open-button/chat-open-button.component";
import DataLabel from "../data-label/data-label.component"; import DataLabel from "../data-label/data-label.component";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import ScheduleManualEvent from "../schedule-manual-event/schedule-manual-event.component"; import ScheduleManualEvent from "../schedule-manual-event/schedule-manual-event.component";
import ScheduleAtChange from "./job-at-change.component"; import ScheduleAtChange from "./job-at-change.component";
import ScheduleEventColor from "./schedule-event.color.component"; import ScheduleEventColor from "./schedule-event.color.component";
import ScheduleEventNote from "./schedule-event.note.component"; import ScheduleEventNote from "./schedule-event.note.component";
import { useMutation } from "@apollo/client"; import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import { UPDATE_APPOINTMENT } from "../../graphql/appointments.queries";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
@@ -61,44 +58,16 @@ export function ScheduleEventComponent({
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const history = useHistory(); const history = useHistory();
const searchParams = queryString.parse(useLocation().search); const searchParams = queryString.parse(useLocation().search);
const [updateAppointment] = useMutation(UPDATE_APPOINTMENT);
const [title, setTitle] = useState(event.title);
const blockContent = (
<Space direction="vertical" wrap>
<Input
value={title}
onChange={(e) => setTitle(e.currentTarget.value)}
onBlur={async () => {
await updateAppointment({
variables: {
appid: event.id,
app: {
title: title,
},
},
optimisticResponse: {
update_appointments: {
__typename: "appointments_mutation_response",
returning: [
{
...event,
title: title,
__typename: "appointments",
},
],
},
},
});
}}
/>
const blockContent = (
<div>
<Button <Button
onClick={() => handleCancel({ id: event.id })} onClick={() => handleCancel({ id: event.id })}
disabled={event.arrived} disabled={event.arrived}
> >
{t("appointments.actions.unblock")} {t("appointments.actions.cancel")}
</Button> </Button>
</Space> </div>
); );
const popoverContent = ( const popoverContent = (
@@ -239,56 +208,46 @@ export function ScheduleEventComponent({
<Button>{t("appointments.actions.sendreminder")}</Button> <Button>{t("appointments.actions.sendreminder")}</Button>
</Dropdown> </Dropdown>
) : null} ) : null}
{event.arrived ? ( <Popover
trigger="click"
disabled={event.arrived}
content={
<Form
layout="vertical"
onFinish={({ lost_sale_reason }) => {
handleCancel({ id: event.id, lost_sale_reason });
}}
>
<Form.Item
name="lost_sale_reason"
label={t("jobs.fields.lost_sale_reason")}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<Select
options={bodyshop.md_lost_sale_reasons.map((lsr) => ({
label: lsr,
value: lsr,
}))}
/>
</Form.Item>
<Button htmlType="submit">
{t("appointments.actions.cancel")}
</Button>
</Form>
}
>
<Button <Button
// onClick={() => handleCancel(event.id)} // onClick={() => handleCancel(event.id)}
disabled={event.arrived} disabled={event.arrived}
> >
{t("appointments.actions.cancel")} {t("appointments.actions.cancel")}
</Button> </Button>
) : ( </Popover>
<Popover
trigger="click"
disabled={event.arrived}
content={
<Form
layout="vertical"
onFinish={({ lost_sale_reason }) => {
handleCancel({ id: event.id, lost_sale_reason });
}}
>
<Form.Item
name="lost_sale_reason"
label={t("jobs.fields.lost_sale_reason")}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<Select
options={bodyshop.md_lost_sale_reasons.map((lsr) => ({
label: lsr,
value: lsr,
}))}
/>
</Form.Item>
<Button htmlType="submit">
{t("appointments.actions.cancel")}
</Button>
</Form>
}
>
<Button
// onClick={() => handleCancel(event.id)}
disabled={event.arrived}
>
{t("appointments.actions.cancel")}
</Button>
</Popover>
)}
{event.isintake ? ( {event.isintake ? (
<Button <Button
disabled={event.arrived} disabled={event.arrived}

View File

@@ -2,16 +2,12 @@ import { useMutation } from "@apollo/client";
import { notification } from "antd"; import { notification } from "antd";
import React from "react"; import React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useDispatch } from "react-redux";
import { logImEXEvent } from "../../firebase/firebase.utils"; import { logImEXEvent } from "../../firebase/firebase.utils";
import { CANCEL_APPOINTMENT_BY_ID } from "../../graphql/appointments.queries"; import { CANCEL_APPOINTMENT_BY_ID } from "../../graphql/appointments.queries";
import { UPDATE_JOB } from "../../graphql/jobs.queries"; import { UPDATE_JOB } from "../../graphql/jobs.queries";
import { insertAuditTrail } from "../../redux/application/application.actions";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import ScheduleEventComponent from "./schedule-event.component"; import ScheduleEventComponent from "./schedule-event.component";
export default function ScheduleEventContainer({ bodyshop, event, refetch }) { export default function ScheduleEventContainer({ bodyshop, event, refetch }) {
const dispatch = useDispatch();
const { t } = useTranslation(); const { t } = useTranslation();
const [cancelAppointment] = useMutation(CANCEL_APPOINTMENT_BY_ID); const [cancelAppointment] = useMutation(CANCEL_APPOINTMENT_BY_ID);
const [updateJob] = useMutation(UPDATE_JOB); const [updateJob] = useMutation(UPDATE_JOB);
@@ -38,25 +34,16 @@ export default function ScheduleEventContainer({ bodyshop, event, refetch }) {
const jobUpdate = await updateJob({ const jobUpdate = await updateJob({
variables: { variables: {
jobId: event.job.id, jobId: event.job.id,
job: { job: {
date_scheduled: null, date_scheduled: null,
scheduled_in: null, scheduled_in: null,
scheduled_completion: null, scheduled_completion: null,
lost_sale_reason, lost_sale_reason,
date_lost_sale: new Date(),
status: bodyshop.md_ro_statuses.default_imported, status: bodyshop.md_ro_statuses.default_imported,
}, },
}, },
}); });
if (!jobUpdate.errors) {
dispatch(
insertAuditTrail({
jobid: event.job.id,
operation: AuditTrailMapping.appointmentcancel(lost_sale_reason),
type: "appointmentcancel",
})
);
}
if (!!jobUpdate.errors) { if (!!jobUpdate.errors) {
notification["error"]({ notification["error"]({
message: t("jobs.errors.updating", { message: t("jobs.errors.updating", {

View File

@@ -1,6 +1,5 @@
import { useMutation } from "@apollo/client"; import { useMutation } from "@apollo/client";
import { Button, Card, Form, Input, notification, Switch } from "antd"; import { Button, Card, Form, Input, notification, Switch } from "antd";
import moment from "moment-business-days";
import queryString from "query-string"; import queryString from "query-string";
import React, { useState } from "react"; import React, { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -13,23 +12,24 @@ import {
MARK_LATEST_APPOINTMENT_ARRIVED, MARK_LATEST_APPOINTMENT_ARRIVED,
} from "../../../../graphql/appointments.queries"; } from "../../../../graphql/appointments.queries";
import { UPDATE_JOB } from "../../../../graphql/jobs.queries"; import { UPDATE_JOB } from "../../../../graphql/jobs.queries";
import { UPDATE_OWNER } from "../../../../graphql/owners.queries";
import { insertAuditTrail } from "../../../../redux/application/application.actions";
import { import {
selectBodyshop, selectBodyshop,
selectCurrentUser, selectCurrentUser,
} from "../../../../redux/user/user.selectors"; } from "../../../../redux/user/user.selectors";
import AuditTrailMapping from "../../../../utils/AuditTrailMappings";
import ConfigFormComponents from "../../../config-form-components/config-form-components.component"; import ConfigFormComponents from "../../../config-form-components/config-form-components.component";
import DateTimePicker from "../../../form-date-time-picker/form-date-time-picker.component"; import DateTimePicker from "../../../form-date-time-picker/form-date-time-picker.component";
import moment from "moment-business-days";
import { insertAuditTrail } from "../../../../redux/application/application.actions";
import AuditTrailMapping from "../../../../utils/AuditTrailMappings";
import { UPDATE_OWNER } from "../../../../graphql/owners.queries";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
currentUser: selectCurrentUser, currentUser: selectCurrentUser,
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation, type }) => insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation, type })), dispatch(insertAuditTrail({ jobid, operation })),
}); });
export function JobChecklistForm({ export function JobChecklistForm({
@@ -183,7 +183,6 @@ export function JobChecklistForm({
(type === "intake" && bodyshop.md_ro_statuses.default_arrived) || (type === "intake" && bodyshop.md_ro_statuses.default_arrived) ||
(type === "deliver" && bodyshop.md_ro_statuses.default_delivered) (type === "deliver" && bodyshop.md_ro_statuses.default_delivered)
), ),
type: "jobchecklist",
}); });
} else { } else {
notification["error"]({ notification["error"]({
@@ -231,7 +230,6 @@ export function JobChecklistForm({
)), )),
scheduled_delivery: scheduled_delivery:
job.scheduled_delivery && moment(job.scheduled_delivery), job.scheduled_delivery && moment(job.scheduled_delivery),
production_vars: job.production_vars,
}), }),
...(type === "deliver" && { ...(type === "deliver" && {
removeFromProduction: true, removeFromProduction: true,

View File

@@ -3,7 +3,6 @@ import React, { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { alphaSort } from "../../utils/sorters"; import { alphaSort } from "../../utils/sorters";
import Dinero from "dinero.js"; import Dinero from "dinero.js";
import {pageLimit} from "../../utils/config";
export default function JobCostingPartsTable({ data, summaryData }) { export default function JobCostingPartsTable({ data, summaryData }) {
const [searchText, setSearchText] = useState(""); const [searchText, setSearchText] = useState("");
const [state, setState] = useState({ const [state, setState] = useState({
@@ -99,7 +98,7 @@ export default function JobCostingPartsTable({ data, summaryData }) {
x: "50%", //y: "40rem" x: "50%", //y: "40rem"
}} }}
onChange={handleTableChange} onChange={handleTableChange}
pagination={{ position: "top", defaultPageSize: pageLimit }} pagination={{ position: "top", defaultPageSize: 50 }}
columns={columns} columns={columns}
rowKey="id" rowKey="id"
dataSource={filteredData} dataSource={filteredData}

View File

@@ -7,31 +7,21 @@ import { connect } from "react-redux";
import { useHistory } from "react-router"; import { useHistory } from "react-router";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { UPDATE_JOB_LINES_IOU } from "../../graphql/jobs-lines.queries"; import { UPDATE_JOB_LINES_IOU } from "../../graphql/jobs-lines.queries";
import { selectTechnician } from "../../redux/tech/tech.selectors";
import { import {
selectBodyshop, selectBodyshop,
selectCurrentUser, selectCurrentUser,
} from "../../redux/user/user.selectors"; } from "../../redux/user/user.selectors";
import { CreateIouForJob } from "../jobs-detail-header-actions/jobs-detail-header-actions.duplicate.util"; import { CreateIouForJob } from "../jobs-detail-header-actions/jobs-detail-header-actions.duplicate.util";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
currentUser: selectCurrentUser, currentUser: selectCurrentUser,
technician: selectTechnician,
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language)) //setUserLanguage: language => dispatch(setUserLanguage(language))
}); });
export default connect(mapStateToProps, mapDispatchToProps)(JobCreateIOU); export default connect(mapStateToProps, mapDispatchToProps)(JobCreateIOU);
export function JobCreateIOU({ export function JobCreateIOU({ bodyshop, currentUser, job, selectedJobLines }) {
bodyshop,
currentUser,
job,
selectedJobLines,
technician,
}) {
const { t } = useTranslation(); const { t } = useTranslation();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const client = useApolloClient(); const client = useApolloClient();
@@ -89,19 +79,13 @@ export function JobCreateIOU({
title={t("jobs.labels.createiouwarning")} title={t("jobs.labels.createiouwarning")}
onConfirm={handleCreateIou} onConfirm={handleCreateIou}
disabled={ disabled={
!selectedJobLines || !selectedJobLines || selectedJobLines.length === 0 || !job.converted
selectedJobLines.length === 0 ||
!job.converted ||
technician
} }
> >
<Button <Button
loading={loading} loading={loading}
disabled={ disabled={
!selectedJobLines || !selectedJobLines || selectedJobLines.length === 0 || !job.converted
selectedJobLines.length === 0 ||
!job.converted ||
technician
} }
> >
{t("jobs.actions.createiou")} {t("jobs.actions.createiou")}

View File

@@ -1,37 +1,15 @@
import { Card } from "antd";
import React from "react"; import React from "react";
import { useTranslation } from "react-i18next"; import { Card } from "antd";
import { connect } from "react-redux";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { selectTechnician } from "../../redux/tech/tech.selectors";
const mapStateToProps = createStructuredSelector({ export default function JobDetailCardTemplate({
technician: selectTechnician,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(JobDetailCardTemplate);
export function JobDetailCardTemplate({
loading, loading,
title, title,
extraLink, extraLink,
technician,
...otherProps ...otherProps
}) { }) {
const { t } = useTranslation();
let extra; let extra;
if (extraLink && !technician) if (extraLink) extra = { extra: <Link to={extraLink}>More</Link> };
extra = {
extra: <Link to={extraLink}>{t("jobs.labels.cards.more")}</Link>,
};
return ( return (
<Card <Card
size="small" size="small"

View File

@@ -18,8 +18,10 @@ export default function JobDetailCardsTotalsComponent({ loading, data }) {
/> />
<Statistic <Statistic
className="imex-flex-row__margin-large" className="imex-flex-row__margin-large"
title={t("jobs.fields.customerowing")} title={t("jobs.fields.ded_amt")}
value={Dinero(data.job_totals.totals.custPayable.total).toFormat()} value={Dinero({
amount: Math.round((data.ded_amt || 0) * 100),
}).toFormat()}
/> />
<Statistic <Statistic
className="imex-flex-row__margin-large" className="imex-flex-row__margin-large"

View File

@@ -1,26 +1,14 @@
import { useQuery } from "@apollo/client"; import { useQuery } from "@apollo/client";
import { Col, Row, Skeleton, Timeline, Typography } from "antd"; import { Col, Divider, Row, Skeleton, Space, Timeline, Typography } from "antd";
import React from "react"; import React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { GET_JOB_LINE_ORDERS } from "../../graphql/jobs.queries"; import { GET_JOB_LINE_ORDERS } from "../../graphql/jobs.queries";
import { selectTechnician } from "../../redux/tech/tech.selectors.js";
import CurrencyFormatter from "../../utils/CurrencyFormatter"; import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { DateFormatter } from "../../utils/DateFormatter"; import { DateFormatter } from "../../utils/DateFormatter";
import AlertComponent from "../alert/alert.component"; import AlertComponent from "../alert/alert.component";
import BillDetailEditcontainer from "../bill-detail-edit/bill-detail-edit.container.jsx";
const mapStateToProps = createStructuredSelector({ export default function JobLinesExpander({ jobline, jobid }) {
technician: selectTechnician,
});
const mapDispatchToProps = (dispatch) => ({});
export default connect(mapStateToProps, mapDispatchToProps)(JobLinesExpander);
export function JobLinesExpander({ jobline, jobid, technician }) {
const { t } = useTranslation(); const { t } = useTranslation();
const { loading, error, data } = useQuery(GET_JOB_LINE_ORDERS, { const { loading, error, data } = useQuery(GET_JOB_LINE_ORDERS, {
fetchPolicy: "network-only", fetchPolicy: "network-only",
@@ -43,31 +31,15 @@ export function JobLinesExpander({ jobline, jobid, technician }) {
{data.parts_order_lines.length > 0 ? ( {data.parts_order_lines.length > 0 ? (
data.parts_order_lines.map((line) => ( data.parts_order_lines.map((line) => (
<Timeline.Item key={line.id}> <Timeline.Item key={line.id}>
<Row wrap> <Space split={<Divider type="vertical" />} wrap>
<Col span={4}> <Link
{!technician ? ( to={`/manage/jobs/${jobid}?partsorderid=${line.parts_order.id}`}
<Link >
to={`/manage/jobs/${jobid}?partsorderid=${line.parts_order.id}`} {line.parts_order.order_number}
> </Link>
{line.parts_order.order_number} <DateFormatter>{line.parts_order.order_date}</DateFormatter>
</Link> {line.parts_order.vendor.name}
) : ( </Space>
`${line.parts_order.order_number}`
)}
</Col>
<Col span={4}>
<DateFormatter>{line.parts_order.order_date}</DateFormatter>
</Col>
<Col span={4}>{line.parts_order.vendor.name}</Col>
{line.backordered_eta ? (
<Col span={4}>
<span>
{`${t("parts_orders.fields.backordered_eta")}: `}
<DateFormatter>{line.backordered_eta}</DateFormatter>
</span>
</Col>
) : null}
</Row>
</Timeline.Item> </Timeline.Item>
)) ))
) : ( ) : (
@@ -79,22 +51,17 @@ export function JobLinesExpander({ jobline, jobid, technician }) {
</Col> </Col>
<Col md={24} lg={12}> <Col md={24} lg={12}>
<Typography.Title level={4}>{t("bills.labels.bills")}</Typography.Title> <Typography.Title level={4}>{t("bills.labels.bills")}</Typography.Title>
<BillDetailEditcontainer />
<Timeline> <Timeline>
{data.billlines.length > 0 ? ( {data.billlines.length > 0 ? (
data.billlines.map((line) => ( data.billlines.map((line) => (
<Timeline.Item key={line.id}> <Timeline.Item key={line.id}>
<Row wrap> <Row wrap>
<Col span={4}> <Col span={4}>
{!technician ? ( <Link
<Link to={`/manage/jobs/${jobid}?tab=partssublet&billid=${line.bill.id}`}
to={`/manage/jobs/${jobid}?tab=partssublet&billid=${line.bill.id}`} >
> {line.bill.invoice_number}
{line.bill.invoice_number} </Link>
</Link>
) : (
`${line.bill.invoice_number}`
)}
</Col> </Col>
<Col span={4}> <Col span={4}>
<span> <span>
@@ -104,7 +71,7 @@ export function JobLinesExpander({ jobline, jobid, technician }) {
</Col> </Col>
<Col span={4}> <Col span={4}>
<span> <span>
{`${t("billlines.fields.actual_cost")}: `} {`${t("billlines.fields.actual_cost")}: `}
<CurrencyFormatter>{line.actual_cost}</CurrencyFormatter> <CurrencyFormatter>{line.actual_cost}</CurrencyFormatter>
</span> </span>
</Col> </Col>
@@ -116,7 +83,9 @@ export function JobLinesExpander({ jobline, jobid, technician }) {
</Timeline.Item> </Timeline.Item>
)) ))
) : ( ) : (
<Timeline.Item>{t("bills.labels.nobilllines")}</Timeline.Item> <Timeline.Item>
{t("parts_orders.labels.notyetordered")}
</Timeline.Item>
)} )}
</Timeline> </Timeline>
</Col> </Col>

View File

@@ -1,12 +1,12 @@
import { import {
DeleteFilled, DeleteFilled,
EditFilled,
FilterFilled, FilterFilled,
HomeOutlined,
MinusCircleTwoTone,
PlusCircleTwoTone,
SyncOutlined, SyncOutlined,
WarningFilled, WarningFilled,
EditFilled,
PlusCircleTwoTone,
MinusCircleTwoTone,
HomeOutlined,
} from "@ant-design/icons"; } from "@ant-design/icons";
import { useMutation } from "@apollo/client"; import { useMutation } from "@apollo/client";
import { import {
@@ -20,8 +20,6 @@ import {
Tag, Tag,
} from "antd"; } from "antd";
import axios from "axios"; import axios from "axios";
import _ from "lodash";
import moment from "moment";
import React, { useState } from "react"; import React, { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
@@ -30,19 +28,23 @@ import { DELETE_JOB_LINE_BY_PK } from "../../graphql/jobs-lines.queries";
import { selectJobReadOnly } from "../../redux/application/application.selectors"; import { selectJobReadOnly } from "../../redux/application/application.selectors";
import { setModalContext } from "../../redux/modals/modals.actions"; import { setModalContext } from "../../redux/modals/modals.actions";
import { selectTechnician } from "../../redux/tech/tech.selectors"; import { selectTechnician } from "../../redux/tech/tech.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { onlyUnique } from "../../utils/arrayHelper"; import { onlyUnique } from "../../utils/arrayHelper";
import CurrencyFormatter from "../../utils/CurrencyFormatter"; import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { alphaSort } from "../../utils/sorters"; import { alphaSort } from "../../utils/sorters";
import JobCreateIOU from "../job-create-iou/job-create-iou.component";
import JobLineConvertToLabor from "../job-line-convert-to-labor/job-line-convert-to-labor.component";
import JobLineLocationPopup from "../job-line-location-popup/job-line-location-popup.component"; import JobLineLocationPopup from "../job-line-location-popup/job-line-location-popup.component";
import JobLineNotePopup from "../job-line-note-popup/job-line-note-popup.component"; import JobLineNotePopup from "../job-line-note-popup/job-line-note-popup.component";
import JobLineStatusPopup from "../job-line-status-popup/job-line-status-popup.component"; import JobLineStatusPopup from "../job-line-status-popup/job-line-status-popup.component";
import JobLinesBillRefernece from "../job-lines-bill-reference/job-lines-bill-reference.component"; import JobLinesBillRefernece from "../job-lines-bill-reference/job-lines-bill-reference.component";
import PartsOrderDrawer from "../parts-order-list-table/parts-order-list-table-drawer.component"; // import AllocationsAssignmentContainer from "../allocations-assignment/allocations-assignment.container";
// import AllocationsBulkAssignmentContainer from "../allocations-bulk-assignment/allocations-bulk-assignment.container";
// import AllocationsEmployeeLabelContainer from "../allocations-employee-label/allocations-employee-label.container";
import PartsOrderModalContainer from "../parts-order-modal/parts-order-modal.container"; import PartsOrderModalContainer from "../parts-order-modal/parts-order-modal.container";
import _ from "lodash";
import JobCreateIOU from "../job-create-iou/job-create-iou.component";
import JobLinesExpander from "./job-lines-expander.component"; import JobLinesExpander from "./job-lines-expander.component";
import { selectBodyshop } from "../../redux/user/user.selectors";
import moment from "moment";
import JobLineConvertToLabor from "../job-line-convert-to-labor/job-line-convert-to-labor.component";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
@@ -55,8 +57,6 @@ const mapDispatchToProps = (dispatch) => ({
dispatch(setModalContext({ context: context, modal: "jobLineEdit" })), dispatch(setModalContext({ context: context, modal: "jobLineEdit" })),
setPartsOrderContext: (context) => setPartsOrderContext: (context) =>
dispatch(setModalContext({ context: context, modal: "partsOrder" })), dispatch(setModalContext({ context: context, modal: "partsOrder" })),
setPartsReceiveContext: (context) =>
dispatch(setModalContext({ context: context, modal: "partsReceive" })),
setBillEnterContext: (context) => setBillEnterContext: (context) =>
dispatch(setModalContext({ context: context, modal: "billEnter" })), dispatch(setModalContext({ context: context, modal: "billEnter" })),
}); });
@@ -66,7 +66,6 @@ export function JobLinesComponent({
jobRO, jobRO,
technician, technician,
setPartsOrderContext, setPartsOrderContext,
setPartsReceiveContext,
loading, loading,
refetch, refetch,
jobLines, jobLines,
@@ -75,8 +74,6 @@ export function JobLinesComponent({
setJobLineEditContext, setJobLineEditContext,
form, form,
setBillEnterContext, setBillEnterContext,
billsQuery,
handlePartsOrderOnRowClick,
}) { }) {
const [deleteJobLine] = useMutation(DELETE_JOB_LINE_BY_PK); const [deleteJobLine] = useMutation(DELETE_JOB_LINE_BY_PK);
@@ -344,7 +341,7 @@ export function JobLinesComponent({
key: "actions", key: "actions",
render: (text, record) => ( render: (text, record) => (
<Space> <Space>
{(record.manual_line || jobIsPrivate) && !technician && ( {(record.manual_line || jobIsPrivate) && (
<> <>
<Button <Button
disabled={jobRO} disabled={jobRO}
@@ -404,7 +401,7 @@ export function JobLinesComponent({
const markedTypes = [e.key]; const markedTypes = [e.key];
if (e.key === "PAN") markedTypes.push("PAP"); if (e.key === "PAN") markedTypes.push("PAP");
if (e.key === "PAS") markedTypes.push("PASL"); if (e.key === "PAS") markedTypes.push("PASL");
setSelectedLines((selectedLines) => setSelectedLines(
_.uniq([ _.uniq([
...selectedLines, ...selectedLines,
...jobLines.filter((item) => markedTypes.includes(item.part_type)), ...jobLines.filter((item) => markedTypes.includes(item.part_type)),
@@ -427,14 +424,6 @@ export function JobLinesComponent({
return ( return (
<div> <div>
<PartsOrderModalContainer /> <PartsOrderModalContainer />
{!technician && (
<PartsOrderDrawer
job={job}
billsQuery={billsQuery}
handleOnRowClick={handlePartsOrderOnRowClick}
setPartsReceiveContext={setPartsReceiveContext}
/>
)}
<PageHeader <PageHeader
title={t("jobs.labels.estimatelines")} title={t("jobs.labels.estimatelines")}
extra={ extra={
@@ -625,17 +614,8 @@ export function JobLinesComponent({
onSelectAll: (selected, selectedRows, changeRows) => { onSelectAll: (selected, selectedRows, changeRows) => {
setSelectedLines(selectedRows); setSelectedLines(selectedRows);
}, },
onSelect: (record, selected, selectedRows, nativeEvent) => { onSelect: (record, selected, selectedRows, nativeEvent) =>
if (selected) { setSelectedLines(selectedRows),
setSelectedLines((selectedLines) =>
_.uniqBy([...selectedLines, record], "id")
);
} else {
setSelectedLines((selectedLines) =>
selectedLines.filter((l) => l.id !== record.id)
);
}
},
}} }}
/> />
</div> </div>

View File

@@ -1,15 +1,6 @@
import React, { useMemo, useState } from "react"; import React, { useMemo, useState } from "react";
import JobLinesComponent from "./job-lines.component"; import JobLinesComponent from "./job-lines.component";
function JobLinesContainer({ function JobLinesContainer({ job, joblines, refetch, form, ...rest }) {
job,
joblines,
billsQuery,
handleBillOnRowClick,
handlePartsOrderOnRowClick,
refetch,
form,
...rest
}) {
const [searchText, setSearchText] = useState(""); const [searchText, setSearchText] = useState("");
const jobLines = useMemo(() => { const jobLines = useMemo(() => {
@@ -46,9 +37,6 @@ function JobLinesContainer({
<JobLinesComponent <JobLinesComponent
refetch={refetch} refetch={refetch}
jobLines={jobLines} jobLines={jobLines}
billsQuery={billsQuery}
handleBillOnRowClick={handleBillOnRowClick}
handlePartsOrderOnRowClick={handlePartsOrderOnRowClick}
setSearchText={setSearchText} setSearchText={setSearchText}
job={job} job={job}
form={form} form={form}

View File

@@ -23,6 +23,7 @@ export function JobEmployeeAssignments({
jobRO, jobRO,
body, body,
refinish, refinish,
prep, prep,
csr, csr,
handleAdd, handleAdd,
@@ -77,7 +78,7 @@ export function JobEmployeeAssignments({
setVisibility(false); setVisibility(false);
}} }}
> >
{t("allocations.actions.assign")} Assign
</Button> </Button>
<Button onClick={() => setVisibility(false)}>Close</Button> <Button onClick={() => setVisibility(false)}>Close</Button>
</Space> </Space>

View File

@@ -14,8 +14,8 @@ const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser //currentUser: selectCurrentUser
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation, type }) => insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation, type })), dispatch(insertAuditTrail({ jobid, operation })),
}); });
export default connect( export default connect(
mapStateToProps, mapStateToProps,
@@ -43,13 +43,12 @@ export function JobEmployeeAssignmentsContainer({
}); });
if (refetch) refetch(); if (refetch) refetch();
if (!!!result.errors) { insertAuditTrail({
insertAuditTrail({ jobid: job.id,
jobid: job.id, operation: AuditTrailMapping.jobassignmentchange(operation, name),
operation: AuditTrailMapping.jobassignmentchange(operation, name), });
type: "jobassignmentchange",
}); if (!!result.errors) {
} else {
notification["error"]({ notification["error"]({
message: t("jobs.errors.assigning", { message: t("jobs.errors.assigning", {
message: JSON.stringify(result.errors), message: JSON.stringify(result.errors),
@@ -67,19 +66,17 @@ export function JobEmployeeAssignmentsContainer({
variables: { jobId: job.id, job: { [empAssignment]: null } }, variables: { jobId: job.id, job: { [empAssignment]: null } },
}); });
if (!!!result.errors) { if (!!result.errors) {
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.jobassignmentremoved(operation),
type: "jobassignmentremoved",
});
} else {
notification["error"]({ notification["error"]({
message: t("jobs.errors.assigning", { message: t("jobs.errors.assigning", {
message: JSON.stringify(result.errors), message: JSON.stringify(result.errors),
}), }),
}); });
} }
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.jobassignmentremoved(operation),
});
setLoading(false); setLoading(false);
}; };

View File

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

View File

@@ -23,18 +23,14 @@ import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { UPDATE_JOB_LINE } from "../../graphql/jobs-lines.queries"; import { UPDATE_JOB_LINE } from "../../graphql/jobs-lines.queries";
import { insertAuditTrail } from "../../redux/application/application.actions"; import { insertAuditTrail } from "../../redux/application/application.actions";
import { selectTechnician } from "../../redux/tech/tech.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings"; import AuditTrailMapping from "../../utils/AuditTrailMappings";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
technician: selectTechnician, //currentUser: selectCurrentUser
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation, type }) => insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation, type })), dispatch(insertAuditTrail({ jobid, operation })),
}); });
export default connect( export default connect(
mapStateToProps, mapStateToProps,
mapDispatchToProps mapDispatchToProps
@@ -45,7 +41,6 @@ export function JobLineConvertToLabor({
jobline, jobline,
job, job,
insertAuditTrail, insertAuditTrail,
technician,
...otherBtnProps ...otherBtnProps
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -118,7 +113,6 @@ export function JobLineConvertToLabor({
hours: calculateAdjustment({ mod_lbr_ty, job, jobline }).toFixed(1), hours: calculateAdjustment({ mod_lbr_ty, job, jobline }).toFixed(1),
mod_lbr_ty, mod_lbr_ty,
}), }),
type: "jobmodifylbradj",
}); });
setLoading(false); setLoading(false);
setVisibility(false); setVisibility(false);
@@ -227,7 +221,7 @@ export function JobLineConvertToLabor({
return ( return (
<> <>
{children} {children}
{jobline.act_price !== 0 && !technician && ( {jobline.act_price !== 0 && (
<Popover <Popover
disabled={jobline.convertedtolbr} disabled={jobline.convertedtolbr}
content={overlay} content={overlay}

View File

@@ -1,6 +1,6 @@
import { useMutation } from "@apollo/client";
import { notification, Select } from "antd"; import { notification, Select } from "antd";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { useMutation } from "@apollo/client";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
@@ -56,10 +56,8 @@ export function JobLineLocationPopup({ bodyshop, jobline, disabled }) {
<LoadingSpinner loading={loading}> <LoadingSpinner loading={loading}>
<Select <Select
autoFocus autoFocus
allowClear
dropdownMatchSelectWidth={100} dropdownMatchSelectWidth={100}
value={location} value={location}
onClear={() => setLocation(null)}
onSelect={handleChange} onSelect={handleChange}
onBlur={handleSave} onBlur={handleSave}
> >

View File

@@ -8,7 +8,7 @@ export default function JobLinesBillRefernece({ jobline }) {
return ( return (
<div style={{ color: subletRequired && "tomato" }}> <div style={{ color: subletRequired && "tomato" }}>
{subletRequired && <WarningFilled />} {subletRequired && <WarningFilled />}
{`${billLine.actual_price.toFixed(2)} x ${billLine.quantity} (${ {`${(billLine.actual_price * billLine.quantity).toFixed(2)} (${
billLine.bill.vendor.name billLine.bill.vendor.name
} #${billLine.bill.invoice_number})`} } #${billLine.bill.invoice_number})`}
</div> </div>

View File

@@ -1,26 +1,19 @@
import { EditFilled } from "@ant-design/icons";
import { Button, Card, Space, Table } from "antd"; import { Button, Card, Space, Table } from "antd";
import { EditFilled } from "@ant-design/icons";
import Dinero from "dinero.js"; import Dinero from "dinero.js";
import React, { useMemo, useState } from "react"; import React, { useMemo, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { selectJobReadOnly } from "../../redux/application/application.selectors"; import { selectJobReadOnly } from "../../redux/application/application.selectors";
import {
openChatByPhone,
setMessage,
} from "../../redux/messaging/messaging.actions";
import { setModalContext } from "../../redux/modals/modals.actions"; import { setModalContext } from "../../redux/modals/modals.actions";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter"; import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { DateFormatter } from "../../utils/DateFormatter"; import { DateFormatter } from "../../utils/DateFormatter";
import { TemplateList } from "../../utils/TemplateConstants";
import { alphaSort, dateSort } from "../../utils/sorters"; import { alphaSort, dateSort } from "../../utils/sorters";
import { TemplateList } from "../../utils/TemplateConstants";
import DataLabel from "../data-label/data-label.component"; import DataLabel from "../data-label/data-label.component";
import PaymentExpandedRowComponent from "../payment-expanded-row/payment-expanded-row.component";
import PaymentsGenerateLink from "../payments-generate-link/payments-generate-link.component";
import PrintWrapperComponent from "../print-wrapper/print-wrapper.component"; import PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
import { useTreatments } from "@splitsoftware/splitio-react";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
@@ -30,34 +23,20 @@ const mapStateToProps = createStructuredSelector({
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
setPaymentContext: (context) => setPaymentContext: (context) =>
dispatch(setModalContext({ context: context, modal: "payment" })), dispatch(setModalContext({ context: context, modal: "payment" })),
setCardPaymentContext: (context) =>
dispatch(setModalContext({ context: context, modal: "cardPayment" })),
openChatByPhone: (phone) => dispatch(openChatByPhone(phone)),
setMessage: (text) => dispatch(setMessage(text)),
}); });
export function JobPayments({ export function JobPayments({
job, job,
jobRO, jobRO,
bodyshop, bodyshop,
setMessage,
openChatByPhone,
setPaymentContext, setPaymentContext,
setCardPaymentContext,
refetch, refetch,
}) { }) {
const { ImEXPay } = useTreatments(
["ImEXPay"],
{},
bodyshop && bodyshop.imexshopid
);
const { t } = useTranslation(); const { t } = useTranslation();
const [state, setState] = useState({ const [state, setState] = useState({
sortedInfo: {}, sortedInfo: {},
filteredInfo: {}, filteredInfo: {},
}); });
const columns = [ const columns = [
{ {
title: t("payments.fields.date"), title: t("payments.fields.date"),
@@ -170,21 +149,6 @@ export function JobPayments({
title={t("payments.labels.title")} title={t("payments.labels.title")}
extra={ extra={
<Space wrap> <Space wrap>
{ImEXPay.treatment === "on" && (
<>
<Button
onClick={() =>
setCardPaymentContext({
actions: { refetch },
context: { jobid: job.id, balance },
})
}
>
{t("menus.header.entercardpayment")}
</Button>
<PaymentsGenerateLink job={job} />
</>
)}
<Button <Button
disabled={!job.converted} disabled={!job.converted}
onClick={() => onClick={() =>
@@ -196,7 +160,6 @@ export function JobPayments({
> >
{t("menus.header.enterpayment")} {t("menus.header.enterpayment")}
</Button> </Button>
<DataLabel <DataLabel
valueStyle={{ color: balance.getAmount() !== 0 ? "red" : "green" }} valueStyle={{ color: balance.getAmount() !== 0 ? "red" : "green" }}
label={t("payments.labels.balance")} label={t("payments.labels.balance")}
@@ -215,11 +178,6 @@ export function JobPayments({
scroll={{ scroll={{
x: true, x: true,
}} }}
expandable={{
expandedRowRender: (record) => (
<PaymentExpandedRowComponent record={record} bodyshop={bodyshop} />
),
}}
summary={() => ( summary={() => (
<> <>
<Table.Summary.Row> <Table.Summary.Row>

View File

@@ -82,7 +82,7 @@ export default function JobReconciliationBillsTable({
state.sortedInfo.order, state.sortedInfo.order,
render: (text, record) => ( render: (text, record) => (
<Checkbox checked={record.bill.is_credit_memo} /> <Checkbox disabled checked={record.bill.is_credit_memo} />
), ),
}, },
]; ];

View File

@@ -1,16 +1,16 @@
import { useMutation, useLazyQuery } from "@apollo/client";
import { CheckCircleOutlined } from "@ant-design/icons"; import { CheckCircleOutlined } from "@ant-design/icons";
import { useLazyQuery, useMutation } from "@apollo/client";
import { import {
Button, Button,
Card, Card,
Form, Form,
InputNumber, InputNumber,
notification,
Popover, Popover,
Space, Space,
notification,
} from "antd"; } from "antd";
import moment from "moment"; import moment from "moment";
import React, { useEffect, useState } from "react"; import React, { useState, useEffect } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { logImEXEvent } from "../../firebase/firebase.utils"; import { logImEXEvent } from "../../firebase/firebase.utils";
import { import {
@@ -50,7 +50,6 @@ export default function ScoreboardAddButton({
const handleFinish = async (values) => { const handleFinish = async (values) => {
logImEXEvent("job_close_add_to_scoreboard"); logImEXEvent("job_close_add_to_scoreboard");
values.date = moment(values.date).format("YYYY-MM-DD");
setLoading(true); setLoading(true);
let result; let result;
@@ -178,7 +177,7 @@ export default function ScoreboardAddButton({
return acc + job.lbr_adjustments[val]; return acc + job.lbr_adjustments[val];
}, 0); }, 0);
form.setFieldsValue({ form.setFieldsValue({
date: moment(), date: new moment(),
bodyhrs: Math.round(v.bodyhrs * 10) / 10, bodyhrs: Math.round(v.bodyhrs * 10) / 10,
painthrs: Math.round(v.painthrs * 10) / 10, painthrs: Math.round(v.painthrs * 10) / 10,
}); });

View File

@@ -33,9 +33,7 @@ const JobSearchSelect = (
useLazyQuery(SEARCH_JOBS_BY_ID_FOR_AUTOCOMPLETE); useLazyQuery(SEARCH_JOBS_BY_ID_FOR_AUTOCOMPLETE);
const executeSearch = (v) => { const executeSearch = (v) => {
console.log(v); if (v && v !== "") callSearch(v);
if (v && v.variables?.search !== "" && v.variables.search.length >= 2)
callSearch(v);
}; };
const debouncedExecuteSearch = _.debounce(executeSearch, 500); const debouncedExecuteSearch = _.debounce(executeSearch, 500);

View File

@@ -14,8 +14,8 @@ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation, type }) => insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation, type })), dispatch(insertAuditTrail({ jobid, operation })),
}); });
export default connect(mapStateToProps, mapDispatchToProps)(JobsAdminStatus); export default connect(mapStateToProps, mapDispatchToProps)(JobsAdminStatus);
@@ -32,7 +32,6 @@ export function JobsAdminStatus({ insertAuditTrail, bodyshop, job }) {
insertAuditTrail({ insertAuditTrail({
jobid: job.id, jobid: job.id,
operation: AuditTrailMapping.admin_jobstatuschange(status), operation: AuditTrailMapping.admin_jobstatuschange(status),
type: "admin_jobstatuschange",
}); });
// refetch(); // refetch();
}) })
@@ -54,14 +53,12 @@ export function JobsAdminStatus({ insertAuditTrail, bodyshop, job }) {
); );
return ( return (
<> <Dropdown overlay={statusmenu} trigger={["click"]} key="changestatus">
<Dropdown overlay={statusmenu} trigger={["click"]} key="changestatus"> <Button shape="round">
<Button shape="round"> <span>{job.status}</span>
<span>{job.status}</span>
<DownCircleFilled /> <DownCircleFilled />
</Button> </Button>
</Dropdown> </Dropdown>
</>
); );
} }

View File

@@ -1,27 +1,26 @@
import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
import { useMutation } from "@apollo/client"; import { useMutation } from "@apollo/client";
import { Button, Form, notification } from "antd"; import { Button, Form, notification } from "antd";
import moment from "moment";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { UPDATE_JOB } from "../../graphql/jobs.queries"; import { UPDATE_JOB } from "../../graphql/jobs.queries";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component"; import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component";
import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component"; import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import moment from "moment";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { insertAuditTrail } from "../../redux/application/application.actions"; import { insertAuditTrail } from "../../redux/application/application.actions";
import { DateTimeFormat } from "./../../utils/DateFormatter";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser //currentUser: selectCurrentUser
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation, type }) => insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation, type })), dispatch(insertAuditTrail({ jobid, operation })),
}); });
export default connect( export default connect(
@@ -39,8 +38,8 @@ export function JobsAdminDatesChange({ insertAuditTrail, job }) {
setLoading(true); setLoading(true);
const result = await updateJob({ const result = await updateJob({
variables: { jobId: job.id, job: values }, variables: { jobId: job.id, job: values },
refetchQueries: ["GET_JOB_BY_PK"], refetchQueries: ['GET_JOB_BY_PK'],
awaitRefetchQueries: true, awaitRefetchQueries:true
}); });
const changedAuditFields = form.getFieldsValue( const changedAuditFields = form.getFieldsValue(
@@ -54,10 +53,9 @@ export function JobsAdminDatesChange({ insertAuditTrail, job }) {
operation: AuditTrailMapping.admin_jobfieldchange( operation: AuditTrailMapping.admin_jobfieldchange(
key, key,
changedAuditFields[key] instanceof moment changedAuditFields[key] instanceof moment
? DateTimeFormat(changedAuditFields[key]) ? moment(changedAuditFields[key]).format("MM/DD/YYYY hh:mm a")
: changedAuditFields[key] : changedAuditFields[key]
), ),
type: "admin_jobfieldchange",
}); });
}); });
@@ -128,10 +126,7 @@ export function JobsAdminDatesChange({ insertAuditTrail, job }) {
<Form.Item label={t("jobs.fields.actual_in")} name="actual_in"> <Form.Item label={t("jobs.fields.actual_in")} name="actual_in">
<DateTimePicker /> <DateTimePicker />
</Form.Item> </Form.Item>
<Form.Item <Form.Item label={t("jobs.fields.date_repairstarted")} name="date_repairstarted">
label={t("jobs.fields.date_repairstarted")}
name="date_repairstarted"
>
<DateTimePicker /> <DateTimePicker />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
@@ -178,15 +173,6 @@ export function JobsAdminDatesChange({ insertAuditTrail, job }) {
> >
<DateTimePicker /> <DateTimePicker />
</Form.Item> </Form.Item>
<Form.Item label={t("jobs.fields.date_void")} name="date_void">
<DateTimePicker />
</Form.Item>
<Form.Item
label={t("jobs.fields.date_lost_sale")}
name="date_lost_sale"
>
<DateTimePicker />
</Form.Item>
</LayoutFormRow> </LayoutFormRow>
</Form> </Form>

View File

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

View File

@@ -1,30 +1,26 @@
import { useMutation } from "@apollo/client"; import { useMutation } from "@apollo/client";
import { Button, Space, notification } from "antd"; import { Button, notification } from "antd";
import { gql } from "@apollo/client";
import React, { useState } from "react"; import React, { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import moment from "moment";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
import {
MARK_JOB_AS_EXPORTED,
MARK_JOB_AS_UNINVOICED,
MARK_JOB_FOR_REEXPORT,
} from "../../graphql/jobs.queries";
import { insertAuditTrail } from "../../redux/application/application.actions";
import { import {
selectBodyshop, selectBodyshop,
selectCurrentUser, selectCurrentUser,
} from "../../redux/user/user.selectors"; } from "../../redux/user/user.selectors";
import moment from "moment";
import AuditTrailMapping from "../../utils/AuditTrailMappings"; import AuditTrailMapping from "../../utils/AuditTrailMappings";
import { insertAuditTrail } from "../../redux/application/application.actions";
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
currentUser: selectCurrentUser, currentUser: selectCurrentUser,
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation, type }) => insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation, type })), dispatch(insertAuditTrail({ jobid, operation })),
}); });
export default connect( export default connect(
mapStateToProps, mapStateToProps,
@@ -40,18 +36,58 @@ export function JobAdminMarkReexport({
const { t } = useTranslation(); const { t } = useTranslation();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [insertExportLog] = useMutation(INSERT_EXPORT_LOG); const [insertExportLog] = useMutation(INSERT_EXPORT_LOG);
const [markJobForReexport] = useMutation(gql`
mutation MARK_JOB_FOR_REEXPORT($jobId: uuid!) {
update_jobs_by_pk(
pk_columns: { id: $jobId }
_set: { date_exported: null
status: "${bodyshop.md_ro_statuses.default_invoiced}"
}
) {
id
date_exported
status
date_invoiced
}
}
`);
const [markJobForReexport] = useMutation(MARK_JOB_FOR_REEXPORT); const [markJobExported] = useMutation(gql`
const [markJobExported] = useMutation(MARK_JOB_AS_EXPORTED); mutation MARK_JOB_AS_EXPORTED($jobId: uuid!, $date_exported: timestamptz!) {
const [markJobUninvoiced] = useMutation(MARK_JOB_AS_UNINVOICED); update_jobs_by_pk(
pk_columns: { id: $jobId }
_set: { date_exported: $date_exported
status: "${bodyshop.md_ro_statuses.default_exported}"
}
) {
id
date_exported
date_invoiced
status
}
}
`);
const [markJobUninvoiced] = useMutation(gql`
mutation MARK_JOB_AS_UNINVOICED($jobId: uuid!, ) {
update_jobs_by_pk(
pk_columns: { id: $jobId }
_set: { date_exported: null
date_invoiced: null
status: "${bodyshop.md_ro_statuses.default_delivered}"
}
) {
id
date_exported
date_invoiced
status
}
}
`);
const handleMarkForExport = async () => { const handleMarkForExport = async () => {
setLoading(true); setLoading(true);
const result = await markJobForReexport({ const result = await markJobForReexport({
variables: { variables: { jobId: job.id },
jobId: job.id,
default_invoiced: bodyshop.md_ro_statuses.default_invoiced,
},
}); });
if (!result.errors) { if (!result.errors) {
@@ -59,7 +95,6 @@ export function JobAdminMarkReexport({
insertAuditTrail({ insertAuditTrail({
jobid: job.id, jobid: job.id,
operation: AuditTrailMapping.admin_jobmarkforreexport(), operation: AuditTrailMapping.admin_jobmarkforreexport(),
type: "admin_jobmarkforreexport",
}); });
} else { } else {
notification["error"]({ notification["error"]({
@@ -74,11 +109,7 @@ export function JobAdminMarkReexport({
const handleMarkExported = async () => { const handleMarkExported = async () => {
setLoading(true); setLoading(true);
const result = await markJobExported({ const result = await markJobExported({
variables: { variables: { jobId: job.id, date_exported: moment() },
jobId: job.id,
date_exported: moment(),
default_exported: bodyshop.md_ro_statuses.default_exported,
},
}); });
await insertExportLog({ await insertExportLog({
@@ -100,7 +131,6 @@ export function JobAdminMarkReexport({
insertAuditTrail({ insertAuditTrail({
jobid: job.id, jobid: job.id,
operation: AuditTrailMapping.admin_jobmarkexported(), operation: AuditTrailMapping.admin_jobmarkexported(),
type: "admin_jobmarkexported",
}); });
} else { } else {
notification["error"]({ notification["error"]({
@@ -115,19 +145,11 @@ export function JobAdminMarkReexport({
const handleUninvoice = async () => { const handleUninvoice = async () => {
setLoading(true); setLoading(true);
const result = await markJobUninvoiced({ const result = await markJobUninvoiced({
variables: { variables: { jobId: job.id },
jobId: job.id,
default_delivered: bodyshop.md_ro_statuses.default_delivered,
},
}); });
if (!result.errors) { if (!result.errors) {
notification["success"]({ message: t("jobs.successes.save") }); notification["success"]({ message: t("jobs.successes.save") });
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.admin_jobuninvoice(),
type: "admin_jobuninvoice",
});
} else { } else {
notification["error"]({ notification["error"]({
message: t("jobs.errors.saving", { message: t("jobs.errors.saving", {
@@ -140,29 +162,27 @@ export function JobAdminMarkReexport({
return ( return (
<> <>
<Space wrap> <Button
<Button loading={loading}
loading={loading} disabled={!job.date_exported}
disabled={!job.date_exported} onClick={handleMarkForExport}
onClick={handleMarkForExport} >
> {t("jobs.labels.markforreexport")}
{t("jobs.labels.markforreexport")} </Button>
</Button> <Button
<Button loading={loading}
loading={loading} disabled={job.date_exported}
disabled={job.date_exported} onClick={handleMarkExported}
onClick={handleMarkExported} >
> {t("jobs.actions.markasexported")}
{t("jobs.actions.markasexported")} </Button>
</Button> <Button
<Button loading={loading}
loading={loading} disabled={!job.date_invoiced || job.date_exported}
disabled={!job.date_invoiced || job.date_exported} onClick={handleUninvoice}
onClick={handleUninvoice} >
> {t("jobs.actions.uninvoice")}
{t("jobs.actions.uninvoice")} </Button>
</Button>
</Space>
</> </>
); );
} }

View File

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

View File

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

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