Merged in test (pull request #53)

Prepare for IO Beta
This commit is contained in:
Patrick Fic
2021-05-11 21:45:42 +00:00
454 changed files with 30063 additions and 66208 deletions

View File

@@ -0,0 +1 @@
client_max_body_size 15M;

View File

@@ -20,3 +20,5 @@ npx deadfile ./src/index.js --exclude build templates
cd client && yarn build && cd build && scp -r \*\* imex@prod-tor1.imex.online:~/bodyshop/client/build && cd .. &&cd ..
gq https://bodyshop-dev-db.herokuapp.com/v1/graphql -H "X-Hasura-Admin-Secret: Dev-BodyShopAppBySnaptSoftware\!" --introspect > schema.graphql
npx hasura migrate apply --endpoint https://db.test.bodyshop.app/ --admin-secret 'Test-ImEXOnlineBySnaptSoftware!'

View File

@@ -1,54 +0,0 @@
**Required items**
-Bodyshop Record
..\*Include the statuses file in the format of:
```json
{
"statuses": [
"Open",
"Scheduled",
"Arrived",
"Repair Plan",
"Parts",
"Body",
"Prep",
"Paint",
"Reassembly",
"Sublet",
"Detail",
"Completed",
"Delivered",
"Invoiced",
"Exported"
],
"open_statuses": [
"Open",
"Scheduled",
"Arrived",
"Repair Plan",
"Parts",
"Body",
"Prep",
"Paint",
"Reassembly",
"Sublet",
"Detail",
"Completed"
],
"default_arrived": "Arrived",
"default_exported": "Exported",
"default_imported": "Open",
"default_invoiced": "Invoiced",
"default_completed": "Completed",
"default_delivered": "Delivered",
"default_scheduled": "Scheduled"
}
```
--\* Set the region for the shop.
-Counter Record - type: ronum
Create an in house vendor record and add it to the bodyshop record.

View File

@@ -1,8 +0,0 @@
**S3 Folder Structure**
/{shopID}/{jobID}/imagename.ext
**Logic**
1. Get a presigned URL by hitting our own express server with a unique key.
2. Use this presigned URL to upload an individual file.
3. Store the key + the bucket name to the documents record.
4.

View File

@@ -1,4 +0,0 @@
**Jobs Business Logic**
**Converting to RO**
Job will convert to an RO if the converted flag is set to true, and the RO number is null or empty. It will query the counters table for the type of 'ro_num', increment it, and return the old value to paste into the RO.

View File

@@ -0,0 +1,24 @@
Postman Method:
POST https://db.imex.online/v1alpha1/pg_dump
Set x-hasura-admin-secret header.
Body is RAW JSON:
```
{
"opts": ["-O", "-x", "--schema-only", "--schema", "public"],
"clean_output": true
}
```
Save output.
Manually export hasura metadata on production.
Go to new instance.
Run SQL
```
CREATE EXTENSION pg_trgm
```
Run SQL from PG Dump
Import hasura metadata.

View File

@@ -3,13 +3,14 @@
ssh-keygen -t rsa -C "your_email@example.com"
Copy the new key to clipboard:
* Windows: clip < id_rsa.pub
* Linux: sudo apt-get install xclip
xclip -sel clip < ~/.ssh/id_rsa.pub
* Mac: pbcopy < ~/.ssh/id_rsa.pub
* Manual Copy: cat ~/.ssh/id_rsa.pub
Add the SSH key to the drop creation screen.
- Windows: clip < id_rsa.pub
- Linux: sudo apt-get install xclip
xclip -sel clip < ~/.ssh/id_rsa.pub
- Mac: pbcopy < ~/.ssh/id_rsa.pub
- Manual Copy: cat ~/.ssh/id_rsa.pub
Add the SSH key to the drop creation screen.
1. Create a new user to replace root user
1. # adduser imex
@@ -19,7 +20,7 @@ Add the SSH key to the drop creation screen.
5. $ chmod 700 ~/.ssh
6. $ nano ~/.ssh/authorized_keys
7. Add the copied SSH key and save.
8. $ chmod 600 ~/.ssh/authorized_keys #Restrict access to authorized keys.
8. $ chmod 600 ~/.ssh/authorized_keys #Restrict access to authorized keys.
2. Setup the Firewall
1. $ sudo ufw allow OpenSSH.
2. $ sudo ufw enable
@@ -32,44 +33,47 @@ Add the SSH key to the drop creation screen.
2. Nginx Http: Opens only port 80 (normal, unencrypted web traffic)
3. Nginx Https: Opens only port 443 (TLS/SSL encrypted traffic)
5. Should now be able to go to IP and see nginx responding with a blank page.
6. Install NodeJs
4. Install NodeJs
1. $ curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash -
2. $ sudo apt install nodejs
3. $ node --version
7. Clone Source Code
1. $ git clone git@bitbucket.org:snaptsoft/bodyshop.git //Requires SSH setup.
3. $ node --version
5. Clone Source Code
1. $ git clone git@bitbucket.org:snaptsoft/bodyshop.git //Requires SSH setup.
2. $ cd bodyshop && npm install //Install all server dependencies.
8. Setup PM2
1. $ npm install pm2 -g //Had to be run as root.
6. Setup PM2
1. $ npm install pm2 -g //Had to be run as root.
2. $ pm2 start ecosystem.config.js
3. $ pm2 startup ubuntu //Ensure it starts when server does.
9. Alter Nginx config
3. $ pm2 startup ubuntu //Ensure it starts when server does.
7. Alter Nginx config
1. sudo nano /etc/nginx/sites-available/default
2. //Add Appropriate server names to the file. www. and non-www.
3. Add the following inside the location of the server block:
proxy_pass http://localhost:5000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
10. Install Certbot
4. $ sudo add-apt-repository ppa:certbot/certbot //Potential issue on ubuntu 20.04
5. $ sudo apt-get update
6. $ sudo apt install python-certbot-nginx
7. $ sudo nano /etc/nginx/sites-available/default
8. Find the existing server_name line and replace the underscore with your domain name:
...
server_name example.com www.example.com;
...
9. $ sudo nginx -t //Verify syntax.
10. $ sudo systemctl reload nginx
11. Generate Certificate
11. $ sudo certbot --nginx -d example.com -d www.example.com //Follow prompts.
12. $ sudo certbot renew --dry-run //Dry run to test auto renewal.
3. Add the following inside the location of the server block: (Remove the 404 bit.)
proxy_pass http://localhost:5000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
8. Install Certbot
9. $ sudo add-apt-repository ppa:certbot/certbot //Potential issue on ubuntu 20.04
10. $ sudo apt-get update
11. $ sudo apt install python-certbot-nginx
12. $ sudo nano /etc/nginx/sites-available/default
13. Find the existing server_name line and replace the underscore with your domain name:
...
server_name example.com www.example.com;
...
14. $ sudo nginx -t //Verify syntax.
15. $ sudo systemctl reload nginx
##AWS INSTRUCTIONS
$ sudo snap install core; sudo snap refresh core
$ sudo snap install --classic certbot
$ sudo ln -s /snap/bin/certbot /usr/bin/certbot
16. Generate Certificate
17. $ sudo certbot --nginx -d example.com -d www.example.com //Follow prompts.
18. $ sudo certbot renew --dry-run //Dry run to test auto renewal.
ADding Yarn
curl -sL https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
sudo apt-get update && sudo apt-get install yarn
echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
sudo apt-get update && sudo apt-get install yarn

View File

@@ -3,24 +3,25 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@apollo/client": "^3.2.1",
"@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.0.4",
"@testing-library/user-event": "^12.1.6",
"@apollo/client": "^3.3.15",
"@testing-library/jest-dom": "^5.11.10",
"@testing-library/react": "^11.2.6",
"@testing-library/user-event": "^13.1.5",
"@types/prop-types": "^15.7.3",
"apollo-boost": "^0.4.9",
"apollo-link-context": "^1.0.20",
"apollo-link-logger": "^1.2.3",
"apollo-link-logger": "^2.0.0",
"dotenv": "^8.2.0",
"firebase": "^7.21.0",
"firebase": "^8.4.1",
"graphql": "^15.4.0",
"prop-types": "^15.7.2",
"ra-data-hasura-graphql": "^0.1.12",
"ra-data-hasura-graphql": "^0.1.13",
"react": "^17.0.1",
"react-admin": "^3.8.5",
"react-admin": "^3.14.4",
"react-dom": "^17.0.1",
"react-icons": "^3.11.0",
"react-scripts": "4.0.0"
"react-icons": "^4.2.0",
"react-scripts": "4.0.3",
"sass": "^1.32.10"
},
"scripts": {
"start": "set PORT=3001 && react-scripts start",

View File

@@ -13,6 +13,7 @@ import {
ShowGuesser,
} from "react-admin";
import { FaFileInvoiceDollar } from "react-icons/fa";
import CircularProgress from "@material-ui/core/CircularProgress";
import { auth } from "../../firebase/admin-firebase-utils";
import authProvider from "../auth-provider/auth-provider";
import JoblinesCreate from "../joblines/joblines.create";
@@ -31,6 +32,7 @@ const httpLink = new HttpLink({
// 'Authorization': `Bearer xxxx`,
},
});
const authLink = setContext((_, { headers }) => {
return (
auth.currentUser &&
@@ -85,7 +87,11 @@ class AdminRoot extends Component {
const { dataProvider } = this.state;
if (!dataProvider) {
return <div>Loading</div>;
return (
<div>
<CircularProgress />
</div>
);
}
return (

View File

@@ -2,45 +2,26 @@ import React from "react";
//@ts-ignore
import {
AutocompleteInput,
BooleanInput,
Edit,
FormTab,
NumberInput,
ReferenceInput,
SelectInput,
SimpleForm,
TabbedForm,
TextInput,
} from "react-admin";
const JobsEdit = (props) => (
<Edit {...props}>
<TabbedForm margin="normal" variant="standard">
<FormTab label="Record Info">
<div
style={{
columns: "3 auto",
// display: "flex",
width: "100%",
// justifyContent: "space-around",
}}
>
<TextInput disabled fullWidth source="id" />
<TextInput disabled fullWidth source="created_at" />
<TextInput disabled fullWidth source="updated_at" />
</div>
</FormTab>
<TabbedForm>
<FormTab label="Job Info">
<div
style={{
columns: "3 auto",
// display: "flex",
width: "100%",
// justifyContent: "space-around",
}}
>
<SimpleForm>
<ReferenceInput label="Shopid" source="shopid" reference="bodyshops">
<SelectInput disabled optionText="shopname" />
</ReferenceInput>
<TextInput fullWidth source="ro_number" />
<TextInput source="ro_number" />
<ReferenceInput label="Owner ID" source="ownerid" reference="owners">
<AutocompleteInput
matchSuggestion={(filter, choice) =>
@@ -64,7 +45,7 @@ const JobsEdit = (props) => (
<ReferenceInput label="Shopid" source="shopid" reference="bodyshops">
<SelectInput disabled optionText="shopname" />
</ReferenceInput>
<TextInput fullWidth source="ro_number" />
<TextInput source="ro_number" />
<ReferenceInput label="Owner ID" source="ownerid" reference="owners">
<AutocompleteInput
matchSuggestion={(filter, choice) =>
@@ -85,254 +66,248 @@ const JobsEdit = (props) => (
>
<SelectInput optionText="v_vin" />
</ReferenceInput>
<TextInput fullWidth source="inproduction" />
<TextInput fullWidth source="converted" />
</div>
<BooleanInput source="inproduction" />
<BooleanInput source="converted" />
<TextInput disabled source="id" />
<TextInput disabled source="created_at" />
<TextInput disabled source="updated_at" />
</SimpleForm>
</FormTab>
<FormTab label="Labor Rates">
<div
style={{
columns: "3 auto",
// display: "flex",
width: "100%",
// justifyContent: "space-around",
}}
>
<NumberInput fullWidth source="labor_rate_id" />
<NumberInput fullWidth source="labor_rate_desc" />
<NumberInput fullWidth source="rate_lab" />
<NumberInput fullWidth source="rate_lad" />
<NumberInput fullWidth source="rate_lae" />
<NumberInput fullWidth source="rate_lar" />
<NumberInput fullWidth source="rate_las" />
<NumberInput fullWidth source="rate_laf" />
<NumberInput fullWidth source="rate_lam" />
<NumberInput fullWidth source="rate_lag" />
<NumberInput fullWidth source="rate_atp" />
<NumberInput fullWidth source="rate_lau" />
<NumberInput fullWidth source="rate_la1" />
<NumberInput fullWidth source="rate_la2" />
<NumberInput fullWidth source="rate_la3" />
<NumberInput fullWidth source="rate_la4" />
<NumberInput fullWidth source="rate_mapa" />
<NumberInput fullWidth source="rate_mash" />
<NumberInput fullWidth source="rate_mahw" />
<NumberInput fullWidth source="rate_ma2s" />
<NumberInput fullWidth source="rate_ma3s" />
<NumberInput fullWidth source="rate_ma2t" />
<NumberInput fullWidth source="rate_mabl" />
<NumberInput fullWidth source="rate_macs" />
<NumberInput fullWidth source="rate_matd" />
<NumberInput fullWidth source="federal_tax_rate" />
<NumberInput fullWidth source="state_tax_rate" />
<NumberInput fullWidth source="local_tax_rate" />
</div>
<NumberInput source="labor_rate_id" />
<NumberInput source="labor_rate_desc" />
<NumberInput source="rate_lab" />
<NumberInput source="rate_lad" />
<NumberInput source="rate_lae" />
<NumberInput source="rate_lar" />
<NumberInput source="rate_las" />
<NumberInput source="rate_laf" />
<NumberInput source="rate_lam" />
<NumberInput source="rate_lag" />
<NumberInput source="rate_atp" />
<NumberInput source="rate_lau" />
<NumberInput source="rate_la1" />
<NumberInput source="rate_la2" />
<NumberInput source="rate_la3" />
<NumberInput source="rate_la4" />
<NumberInput source="rate_mapa" />
<NumberInput source="rate_mash" />
<NumberInput source="rate_mahw" />
<NumberInput source="rate_ma2s" />
<NumberInput source="rate_ma3s" />
<NumberInput source="rate_ma2t" />
<NumberInput source="rate_mabl" />
<NumberInput source="rate_macs" />
<NumberInput source="rate_matd" />
<NumberInput source="federal_tax_rate" />
<NumberInput source="state_tax_rate" />
<NumberInput source="local_tax_rate" />
</FormTab>
<FormTab label="Dates">
<TextInput fullWidth source="scheduled_in" />
<TextInput fullWidth source="actual_in" />
<TextInput fullWidth source="scheduled_completion" />
<TextInput fullWidth source="actual_completion" />
<TextInput fullWidth source="scheduled_delivery" />
<TextInput fullWidth source="actual_delivery" />
<TextInput fullWidth source="invoice_date" />
<TextInput fullWidth source="date_estimated" />
<TextInput fullWidth source="date_open" />
<TextInput fullWidth source="date_scheduled" />
<TextInput fullWidth source="date_invoiced" />
<TextInput fullWidth source="date_exported" />
<TextInput source="scheduled_in" />
<TextInput source="actual_in" />
<TextInput source="scheduled_completion" />
<TextInput source="actual_completion" />
<TextInput source="scheduled_delivery" />
<TextInput source="actual_delivery" />
<TextInput source="invoice_date" />
<TextInput source="date_estimated" />
<TextInput source="date_open" />
<TextInput source="date_scheduled" />
<TextInput source="date_invoiced" />
<TextInput source="date_exported" />
</FormTab>
<FormTab label="Insurance info">
<TextInput fullWidth source="est_co_nm" />
<TextInput fullWidth source="est_addr1" />
<TextInput fullWidth source="est_addr2" />
<TextInput fullWidth source="est_city" />
<TextInput fullWidth source="est_st" />
<TextInput fullWidth source="est_zip" />
<TextInput fullWidth source="est_ctry" />
<TextInput fullWidth source="est_ph1" />
<TextInput fullWidth source="est_ea" />
<TextInput fullWidth source="est_ct_ln" />
<TextInput fullWidth source="est_ct_fn" />
<TextInput fullWidth source="regie_number" />
<TextInput fullWidth source="statusid" />
<TextInput fullWidth source="ins_co_id" />
<TextInput fullWidth source="ins_co_nm" />
<TextInput fullWidth source="ins_addr1" />
<TextInput fullWidth source="ins_addr2" />
<TextInput fullWidth source="ins_city" />
<TextInput fullWidth source="ins_st" />
<TextInput fullWidth source="ins_zip" />
<TextInput fullWidth source="ins_ctry" />
<TextInput fullWidth source="ins_ph1" />
<TextInput fullWidth source="ins_ph1x" />
<TextInput fullWidth source="ins_ph2" />
<TextInput fullWidth source="ins_ph2x" />
<TextInput fullWidth source="ins_fax" />
<TextInput fullWidth source="ins_faxx" />
<TextInput fullWidth source="ins_ct_ln" />
<TextInput fullWidth source="ins_ct_fn" />
<TextInput fullWidth source="ins_title" />
<TextInput fullWidth source="ins_ct_ph" />
<TextInput fullWidth source="ins_ct_phx" />
<TextInput fullWidth source="ins_ea" />
<TextInput fullWidth source="ins_memo" />
<TextInput fullWidth source="policy_no" />
<TextInput fullWidth source="ded_amt" />
<TextInput fullWidth source="ded_status" />
<TextInput fullWidth source="asgn_no" />
<TextInput fullWidth source="asgn_date" />
<TextInput fullWidth source="asgn_type" />
<TextInput fullWidth source="clm_no" />
<TextInput fullWidth source="clm_ofc_id" />
<TextInput fullWidth source="agt_co_id" />
<TextInput fullWidth source="agt_co_nm" />
<TextInput fullWidth source="agt_addr1" />
<TextInput fullWidth source="agt_addr2" />
<TextInput fullWidth source="agt_city" />
<TextInput fullWidth source="agt_st" />
<TextInput fullWidth source="agt_zip" />
<TextInput fullWidth source="agt_ctry" />
<TextInput fullWidth source="agt_ph1" />
<TextInput fullWidth source="agt_ph1x" />
<TextInput fullWidth source="agt_ph2" />
<TextInput fullWidth source="agt_ph2x" />
<TextInput fullWidth source="agt_fax" />
<TextInput fullWidth source="agt_faxx" />
<TextInput fullWidth source="agt_ct_ln" />
<TextInput fullWidth source="agt_ct_fn" />
<TextInput fullWidth source="agt_ct_ph" />
<TextInput fullWidth source="agt_ct_phx" />
<TextInput fullWidth source="agt_ea" />
<TextInput fullWidth source="agt_lic_no" />
<TextInput fullWidth source="loss_type" />
<TextInput fullWidth source="loss_desc" />
<TextInput fullWidth source="theft_ind" />
<TextInput fullWidth source="cat_no" />
<TextInput fullWidth source="tlos_ind" />
<TextInput fullWidth source="ciecaid" />
<TextInput fullWidth source="loss_date" />
<TextInput fullWidth source="clm_ofc_nm" />
<TextInput fullWidth source="clm_addr1" />
<TextInput fullWidth source="clm_addr2" />
<TextInput fullWidth source="clm_city" />
<TextInput fullWidth source="clm_st" />
<TextInput fullWidth source="clm_zip" />
<TextInput fullWidth source="clm_ctry" />
<TextInput fullWidth source="clm_ph1" />
<TextInput fullWidth source="clm_ph1x" />
<TextInput fullWidth source="clm_ph2" />
<TextInput fullWidth source="clm_ph2x" />
<TextInput fullWidth source="clm_fax" />
<TextInput fullWidth source="clm_faxx" />
<TextInput fullWidth source="clm_ct_ln" />
<TextInput fullWidth source="clm_ct_fn" />
<TextInput fullWidth source="clm_title" />
<TextInput fullWidth source="clm_ct_ph" />
<TextInput fullWidth source="clm_ct_phx" />
<TextInput fullWidth source="clm_ea" />
<TextInput fullWidth source="payee_nms" />
<TextInput fullWidth source="pay_type" />
<TextInput fullWidth source="pay_date" />
<TextInput fullWidth source="pay_chknm" />
<TextInput fullWidth source="pay_amt" />
<TextInput source="est_co_nm" />
<TextInput source="est_addr1" />
<TextInput source="est_addr2" />
<TextInput source="est_city" />
<TextInput source="est_st" />
<TextInput source="est_zip" />
<TextInput source="est_ctry" />
<TextInput source="est_ph1" />
<TextInput source="est_ea" />
<TextInput source="est_ct_ln" />
<TextInput source="est_ct_fn" />
<TextInput source="regie_number" />
<TextInput source="statusid" />
<TextInput source="ins_co_id" />
<TextInput source="ins_co_nm" />
<TextInput source="ins_addr1" />
<TextInput source="ins_addr2" />
<TextInput source="ins_city" />
<TextInput source="ins_st" />
<TextInput source="ins_zip" />
<TextInput source="ins_ctry" />
<TextInput source="ins_ph1" />
<TextInput source="ins_ph1x" />
<TextInput source="ins_ph2" />
<TextInput source="ins_ph2x" />
<TextInput source="ins_fax" />
<TextInput source="ins_faxx" />
<TextInput source="ins_ct_ln" />
<TextInput source="ins_ct_fn" />
<TextInput source="ins_title" />
<TextInput source="ins_ct_ph" />
<TextInput source="ins_ct_phx" />
<TextInput source="ins_ea" />
<TextInput source="ins_memo" />
<TextInput source="policy_no" />
<TextInput source="ded_amt" />
<TextInput source="ded_status" />
<TextInput source="asgn_no" />
<TextInput source="asgn_date" />
<TextInput source="asgn_type" />
<TextInput source="clm_no" />
<TextInput source="clm_ofc_id" />
<TextInput source="agt_co_id" />
<TextInput source="agt_co_nm" />
<TextInput source="agt_addr1" />
<TextInput source="agt_addr2" />
<TextInput source="agt_city" />
<TextInput source="agt_st" />
<TextInput source="agt_zip" />
<TextInput source="agt_ctry" />
<TextInput source="agt_ph1" />
<TextInput source="agt_ph1x" />
<TextInput source="agt_ph2" />
<TextInput source="agt_ph2x" />
<TextInput source="agt_fax" />
<TextInput source="agt_faxx" />
<TextInput source="agt_ct_ln" />
<TextInput source="agt_ct_fn" />
<TextInput source="agt_ct_ph" />
<TextInput source="agt_ct_phx" />
<TextInput source="agt_ea" />
<TextInput source="agt_lic_no" />
<TextInput source="loss_type" />
<TextInput source="loss_desc" />
<TextInput source="theft_ind" />
<TextInput source="cat_no" />
<TextInput source="tlos_ind" />
<TextInput source="ciecaid" />
<TextInput source="loss_date" />
<TextInput source="clm_ofc_nm" />
<TextInput source="clm_addr1" />
<TextInput source="clm_addr2" />
<TextInput source="clm_city" />
<TextInput source="clm_st" />
<TextInput source="clm_zip" />
<TextInput source="clm_ctry" />
<TextInput source="clm_ph1" />
<TextInput source="clm_ph1x" />
<TextInput source="clm_ph2" />
<TextInput source="clm_ph2x" />
<TextInput source="clm_fax" />
<TextInput source="clm_faxx" />
<TextInput source="clm_ct_ln" />
<TextInput source="clm_ct_fn" />
<TextInput source="clm_title" />
<TextInput source="clm_ct_ph" />
<TextInput source="clm_ct_phx" />
<TextInput source="clm_ea" />
<TextInput source="payee_nms" />
<TextInput source="pay_type" />
<TextInput source="pay_date" />
<TextInput source="pay_chknm" />
<TextInput source="pay_amt" />
</FormTab>
<FormTab label="Owner Data on Job">
<TextInput fullWidth source="cust_pr" />
<TextInput fullWidth source="insd_ln" />
<TextInput fullWidth source="insd_fn" />
<TextInput fullWidth source="insd_title" />
<TextInput fullWidth source="insd_co_nm" />
<TextInput fullWidth source="insd_addr1" />
<TextInput fullWidth source="insd_addr2" />
<TextInput fullWidth source="insd_city" />
<TextInput fullWidth source="insd_st" />
<TextInput fullWidth source="insd_zip" />
<TextInput fullWidth source="insd_ctry" />
<TextInput fullWidth source="insd_ph1" />
<TextInput fullWidth source="insd_ph1x" />
<TextInput fullWidth source="insd_ph2" />
<TextInput fullWidth source="insd_ph2x" />
<TextInput fullWidth source="insd_fax" />
<TextInput fullWidth source="insd_faxx" />
<TextInput fullWidth source="insd_ea" />
<TextInput fullWidth source="ownr_ln" />
<TextInput fullWidth source="ownr_fn" />
<TextInput fullWidth source="ownr_title" />
<TextInput fullWidth source="ownr_co_nm" />
<TextInput fullWidth source="ownr_addr1" />
<TextInput fullWidth source="ownr_addr2" />
<TextInput fullWidth source="ownr_city" />
<TextInput fullWidth source="ownr_st" />
<TextInput fullWidth source="ownr_zip" />
<TextInput fullWidth source="ownr_ctry" />
<TextInput fullWidth source="ownr_ph1" />
<TextInput fullWidth source="ownr_ph1x" />
<TextInput fullWidth source="ownr_ph2" />
<TextInput fullWidth source="ownr_ph2x" />
<TextInput fullWidth source="ownr_fax" />
<TextInput fullWidth source="ownr_faxx" />
<TextInput fullWidth source="ownr_ea" />
<TextInput source="cust_pr" />
<TextInput source="insd_ln" />
<TextInput source="insd_fn" />
<TextInput source="insd_title" />
<TextInput source="insd_co_nm" />
<TextInput source="insd_addr1" />
<TextInput source="insd_addr2" />
<TextInput source="insd_city" />
<TextInput source="insd_st" />
<TextInput source="insd_zip" />
<TextInput source="insd_ctry" />
<TextInput source="insd_ph1" />
<TextInput source="insd_ph1x" />
<TextInput source="insd_ph2" />
<TextInput source="insd_ph2x" />
<TextInput source="insd_fax" />
<TextInput source="insd_faxx" />
<TextInput source="insd_ea" />
<TextInput source="ownr_ln" />
<TextInput source="ownr_fn" />
<TextInput source="ownr_title" />
<TextInput source="ownr_co_nm" />
<TextInput source="ownr_addr1" />
<TextInput source="ownr_addr2" />
<TextInput source="ownr_city" />
<TextInput source="ownr_st" />
<TextInput source="ownr_zip" />
<TextInput source="ownr_ctry" />
<TextInput source="ownr_ph1" />
<TextInput source="ownr_ph1x" />
<TextInput source="ownr_ph2" />
<TextInput source="ownr_ph2x" />
<TextInput source="ownr_fax" />
<TextInput source="ownr_faxx" />
<TextInput source="ownr_ea" />
</FormTab>
<FormTab label="Financial">
<TextInput fullWidth source="clm_total" />
<TextInput fullWidth source="owner_owing" />
<TextInput source="clm_total" />
<TextInput source="owner_owing" />
</FormTab>
<FormTab label="Other">
<TextInput fullWidth source="area_of_damage" />
<TextInput fullWidth source="loss_cat" />
<TextInput fullWidth source="special_coverage_policy" />
<TextInput fullWidth source="csr" />
<TextInput fullWidth source="po_number" />
<TextInput fullWidth source="unit_number" />
<TextInput fullWidth source="kmin" />
<TextInput fullWidth source="kmout" />
<TextInput fullWidth source="referral_source" />
<TextInput fullWidth source="selling_dealer" />
<TextInput fullWidth source="servicing_dealer" />
<TextInput fullWidth source="servicing_dealer_contact" />
<TextInput fullWidth source="selling_dealer_contact" />
<TextInput fullWidth source="depreciation_taxes" />
<TextInput fullWidth source="federal_tax_payable" />
<TextInput fullWidth source="other_amount_payable" />
<TextInput fullWidth source="towing_payable" />
<TextInput fullWidth source="storage_payable" />
<TextInput fullWidth source="adjustment_bottom_line" />
<TextInput fullWidth source="tax_pstthr" />
<TextInput fullWidth source="tax_tow_rt" />
<TextInput fullWidth source="tax_sub_rt" />
<TextInput fullWidth source="tax_paint_mat_rt" />
<TextInput fullWidth source="tax_levies_rt" />
<TextInput fullWidth source="tax_prethr" />
<TextInput fullWidth source="tax_thramt" />
<TextInput fullWidth source="tax_str_rt" />
<TextInput fullWidth source="tax_lbr_rt" />
<TextInput fullWidth source="adj_g_disc" />
<TextInput fullWidth source="adj_towdis" />
<TextInput fullWidth source="adj_strdis" />
<TextInput fullWidth source="tax_predis" />
<TextInput fullWidth source="rate_laa" />
<TextInput fullWidth source="status" />
<TextInput fullWidth source="cieca_stl" />
<TextInput fullWidth source="g_bett_amt" />
<TextInput fullWidth source="cieca_ttl" />
<TextInput fullWidth source="plate_no" />
<TextInput fullWidth source="plate_st" />
<TextInput fullWidth source="v_vin" />
<TextInput fullWidth source="v_model_yr" />
<TextInput fullWidth source="v_model_desc" />
<TextInput fullWidth source="v_make_desc" />
<TextInput fullWidth source="v_color" />
<TextInput fullWidth source="parts_tax_rates" />
<TextInput fullWidth source="job_totals" />
<TextInput fullWidth source="production_vars" />
<TextInput fullWidth source="intakechecklist" />
<TextInput fullWidth source="invoice_allocation" />
<TextInput fullWidth source="kanbanparent" />
<TextInput fullWidth source="employee_body" />
<TextInput fullWidth source="employee_refinish" />
<TextInput fullWidth source="employee_prep" />
<TextInput source="area_of_damage" />
<TextInput source="loss_cat" />
<TextInput source="special_coverage_policy" />
<TextInput source="csr" />
<TextInput source="po_number" />
<TextInput source="unit_number" />
<TextInput source="kmin" />
<TextInput source="kmout" />
<TextInput source="referral_source" />
<TextInput source="selling_dealer" />
<TextInput source="servicing_dealer" />
<TextInput source="servicing_dealer_contact" />
<TextInput source="selling_dealer_contact" />
<TextInput source="depreciation_taxes" />
<TextInput source="federal_tax_payable" />
<TextInput source="other_amount_payable" />
<TextInput source="towing_payable" />
<TextInput source="storage_payable" />
<TextInput source="adjustment_bottom_line" />
<TextInput source="tax_pstthr" />
<TextInput source="tax_tow_rt" />
<TextInput source="tax_sub_rt" />
<TextInput source="tax_paint_mat_rt" />
<TextInput source="tax_levies_rt" />
<TextInput source="tax_prethr" />
<TextInput source="tax_thramt" />
<TextInput source="tax_str_rt" />
<TextInput source="tax_lbr_rt" />
<TextInput source="adj_g_disc" />
<TextInput source="adj_towdis" />
<TextInput source="adj_strdis" />
<TextInput source="tax_predis" />
<TextInput source="rate_laa" />
<TextInput source="status" />
<TextInput source="cieca_stl" />
<TextInput source="g_bett_amt" />
<TextInput source="cieca_ttl" />
<TextInput source="plate_no" />
<TextInput source="plate_st" />
<TextInput source="v_vin" />
<TextInput source="v_model_yr" />
<TextInput source="v_model_desc" />
<TextInput source="v_make_desc" />
<TextInput source="v_color" />
<TextInput source="parts_tax_rates" />
<TextInput source="job_totals" />
<TextInput source="production_vars" />
<TextInput source="intakechecklist" />
<TextInput source="invoice_allocation" />
<TextInput source="kanbanparent" />
<TextInput source="employee_body" />
<TextInput source="employee_refinish" />
<TextInput source="employee_prep" />
</FormTab>
</TabbedForm>
</Edit>

View File

@@ -8,31 +8,35 @@ import {
ReferenceField,
SelectInput,
TextField,
TextInput
TextInput,
} from "react-admin";
import { QUERY_ALL_SHOPS } from "../../graphql/admin.shop.queries";
const JobsList = (props) => (
<List filters={<JobsFilter />} {...props}>
<Datagrid rowClick="edit">
<TextField source="id" />
<ReferenceField source="shopid" reference="bodyshops">
<TextField source="id" label="Job ID" />
<ReferenceField source="shopid" reference="bodyshops" label="Shop Name">
<TextField source="shopname" />
</ReferenceField>
<TextField source="ro_number" />
<TextField source="ro_number" label="RO Number" />
<TextField source="ownr_fn" />
<TextField source="ownr_ln" />
<TextField source="ownr_co_nm" />
<TextField source="ownr_fn" label="Owner FN" />
<TextField source="ownr_ln" label="Owner LN" />
<TextField source="ownr_co_nm" label="Owner CO" />
<ReferenceField source="ownerid" reference="owners">
<ReferenceField source="ownerid" reference="owners" label="Owner Record">
<TextField source="id" />
</ReferenceField>
<TextField source="v_model_yr" />
<TextField source="v_make_desc" />
<TextField source="v_model_desc" />
<TextField source="v_model_yr" label="Year" />
<TextField source="v_make_desc" label="Make" />
<TextField source="v_model_desc" label="Model" />
<ReferenceField source="vehicleid" reference="vehicles">
<ReferenceField
source="vehicleid"
reference="vehicles"
label="Vehicle ID"
>
<TextField source="id" />
</ReferenceField>
</Datagrid>
@@ -47,13 +51,13 @@ const JobsFilter = (props) => {
return (
<Filter {...props}>
<TextInput label="RO Number" source="ro_number" />
<TextInput label="Job ID" source="id" />
<SelectInput
source="shopid"
label="Bodyshop"
choices={data.bodyshops.map((b) => {
return { id: b.id, name: b.shopname };
})}
alwaysOn
allowEmpty={false}
/>
</Filter>
);

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

25134
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,43 +4,44 @@
"private": true,
"proxy": "http://localhost:5000",
"dependencies": {
"@apollo/client": "^3.3.11",
"@apollo/client": "^3.3.14",
"@craco/craco": "^6.1.1",
"@fingerprintjs/fingerprintjs": "^3.0.6",
"@fingerprintjs/fingerprintjs": "^3.1.0",
"@lourenci/react-kanban": "^2.1.0",
"@sentry/react": "^6.2.0",
"@sentry/tracing": "^6.2.0",
"@sentry/react": "^6.2.5",
"@sentry/tracing": "^6.2.5",
"@stripe/react-stripe-js": "^1.4.0",
"@stripe/stripe-js": "^1.12.1",
"@tanem/react-nprogress": "^3.0.57",
"antd": "^4.13.1",
"@tanem/react-nprogress": "^3.0.62",
"antd": "^4.15.1",
"apollo-link-logger": "^2.0.0",
"axios": "^0.21.1",
"craco-less": "^1.17.1",
"dinero.js": "^1.8.1",
"dotenv": "^8.2.0",
"firebase": "^8.2.10",
"env-cmd": "^10.1.0",
"firebase": "^8.4.1",
"graphql": "^15.5.0",
"i18next": "^19.8.9",
"i18next": "^20.2.1",
"i18next-browser-languagedetector": "^6.0.1",
"jsoneditor": "^9.1.10",
"jsoneditor": "^9.3.1",
"jsreport-browser-client-dist": "^1.3.0",
"libphonenumber-js": "^1.9.12",
"logrocket": "^1.0.13",
"libphonenumber-js": "^1.9.16",
"logrocket": "^1.0.15",
"moment-business-days": "^1.2.0",
"phone": "^2.4.21",
"preval.macro": "^5.0.0",
"prop-types": "^15.7.2",
"query-string": "^6.14.0",
"query-string": "^7.0.0",
"react": "^17.0.1",
"react-big-calendar": "^0.32.0",
"react-big-calendar": "^0.33.2",
"react-color": "^2.19.3",
"react-dom": "^17.0.1",
"react-drag-listview": "^0.1.8",
"react-grid-gallery": "^0.5.5",
"react-i18next": "^11.8.9",
"react-i18next": "^11.8.13",
"react-icons": "^4.2.0",
"react-number-format": "^4.4.4",
"react-number-format": "^4.5.5",
"react-redux": "^7.2.2",
"react-resizable": "^1.11.1",
"react-router-dom": "^5.2.0",
@@ -53,26 +54,27 @@
"redux-state-sync": "^3.1.2",
"reselect": "^4.0.0",
"sass": "^1.32.8",
"styled-components": "^5.2.0",
"styled-components": "^5.2.3",
"subscriptions-transport-ws": "^0.9.18",
"web-vitals": "^0.2.4",
"workbox-background-sync": "^5.1.3",
"workbox-broadcast-update": "^5.1.3",
"workbox-cacheable-response": "^5.1.3",
"workbox-core": "^5.1.3",
"workbox-expiration": "^5.1.3",
"workbox-google-analytics": "^5.1.3",
"workbox-navigation-preload": "^5.1.3",
"workbox-precaching": "^5.1.3",
"workbox-range-requests": "^5.1.3",
"workbox-routing": "^5.1.3",
"workbox-strategies": "^5.1.3",
"workbox-streams": "^5.1.3"
"web-vitals": "^1.1.1",
"workbox-background-sync": "^6.1.5",
"workbox-broadcast-update": "^6.1.5",
"workbox-cacheable-response": "^6.1.5",
"workbox-core": "^6.1.5",
"workbox-expiration": "^6.1.5",
"workbox-google-analytics": "^6.1.5",
"workbox-navigation-preload": "^6.1.5",
"workbox-precaching": "^6.1.5",
"workbox-range-requests": "^6.1.5",
"workbox-routing": "^6.1.5",
"workbox-strategies": "^6.1.5",
"workbox-streams": "^6.1.5"
},
"scripts": {
"analyze": "source-map-explorer 'build/static/js/*.js'",
"start": "craco start",
"build": "REACT_APP_GIT_SHA=`git rev-parse --short HEAD` craco build",
"build:test": "env-cmd -f .env.test npm run build",
"buildcra": "REACT_APP_GIT_SHA=`git rev-parse --short HEAD` react-scripts build",
"build-deploy": "REACT_APP_GIT_SHA=`git rev-parse --short HEAD` craco build && s3cmd sync build/* s3://imex-online-production && echo '🚀 Deployed!'",
"test": "craco test",

View File

@@ -7,17 +7,25 @@ import React from "react";
import GlobalLoadingBar from "../components/global-loading-bar/global-loading-bar.component";
import client from "../utils/GraphQLClient";
import App from "./App";
import { useTranslation } from "react-i18next";
moment.locale("en-US");
if (process.env.NODE_ENV === "production") LogRocket.init("gvfvfw/bodyshopapp");
export default function AppContainer() {
const { t } = useTranslation();
return (
<ApolloProvider client={client}>
<ConfigProvider
//componentSize="small"
input={{ autoComplete: "new-password" }}
locale={enLocale}
form={{
validateMessages: {
// eslint-disable-next-line no-template-curly-in-string
required: t("general.validation.required", { label: "${label}" }),
},
}}
>
<GlobalLoadingBar />
<App />

View File

@@ -3,7 +3,7 @@ import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { alphaSort } from "../../utils/sorters";
import { alphaSort, dateSort } from "../../utils/sorters";
import PayableExportButton from "../payable-export-button/payable-export-button.component";
import PayableExportAll from "../payable-export-all-button/payable-export-all-button.component";
import { DateFormatter } from "../../utils/DateFormatter";
@@ -80,7 +80,7 @@ export default function AccountingPayablesTableComponent({ loading, bills }) {
dataIndex: "date",
key: "date",
sorter: (a, b) => a.date - b.date,
sorter: (a, b) => dateSort(a.date, b.date),
sortOrder:
state.sortedInfo.columnKey === "date" && state.sortedInfo.order,
render: (text, record) => <DateFormatter>{record.date}</DateFormatter>,

View File

@@ -6,7 +6,7 @@ import { logImEXEvent } from "../../firebase/firebase.utils";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { alphaSort } from "../../utils/sorters";
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";
export default function AccountingReceivablesTableComponent({ loading, jobs }) {
const { t } = useTranslation();

View File

@@ -1,3 +1,4 @@
import { DeleteFilled } from "@ant-design/icons";
import { useMutation } from "@apollo/client";
import { Button, notification, Popconfirm } from "antd";
import React, { useState } from "react";
@@ -57,7 +58,7 @@ export default function BillDeleteButton({ bill }) {
// onClick={handleDelete}
loading={loading}
>
{t("general.actions.delete")}
<DeleteFilled />
</Button>
</Popconfirm>
</RbacWrapper>

View File

@@ -1,5 +1,13 @@
import { useMutation, useQuery } from "@apollo/client";
import { Button, Drawer, Form, Grid, PageHeader, Popconfirm } from "antd";
import {
Button,
Drawer,
Form,
Grid,
PageHeader,
Popconfirm,
Space,
} from "antd";
import moment from "moment";
import queryString from "query-string";
import React, { useEffect, useState } from "react";
@@ -14,8 +22,25 @@ import AlertComponent from "../alert/alert.component";
import BillFormContainer from "../bill-form/bill-form.container";
import JobDocumentsGallery from "../jobs-documents-gallery/jobs-documents-gallery.container";
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
import BillReeportButtonComponent from "../bill-reexport-button/bill-reexport-button.component";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { setModalContext } from "../../redux/modals/modals.actions";
export default function BillDetailEditcontainer() {
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
});
const mapDispatchToProps = (dispatch) => ({
setPartsOrderContext: (context) =>
dispatch(setModalContext({ context: context, modal: "partsOrder" })),
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(BillDetailEditcontainer);
export function BillDetailEditcontainer({ setPartsOrderContext }) {
const search = queryString.parse(useLocation().search);
const history = useHistory();
const { t } = useTranslation();
@@ -34,9 +59,9 @@ export default function BillDetailEditcontainer() {
xs: "100%",
sm: "100%",
md: "100%",
lg: "80%",
lg: "100%",
xl: "80%",
xxl: "70%",
xxl: "80%",
};
const drawerPercentage = selectedBreakpoint
? bpoints[selectedBreakpoint[0]]
@@ -117,13 +142,13 @@ export default function BillDetailEditcontainer() {
};
useEffect(() => {
if (search.billid) {
if (search.billid && data) {
form.resetFields();
}
}, [form, search.billid]);
}, [form, search.billid, data]);
if (error) return <AlertComponent message={error.message} type="error" />;
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;
@@ -145,23 +170,56 @@ export default function BillDetailEditcontainer() {
`${data.bills_by_pk.invoice_number} - ${data.bills_by_pk.vendor.name}`
}
extra={
<Popconfirm
visible={visible}
onConfirm={() => form.submit()}
onCancel={() => setVisible(false)}
okButtonProps={{ loading: updateLoading }}
title={t("bills.labels.editadjwarning")}
>
<Space>
<Button
htmlType="submit"
disabled={exported}
onClick={handleSave}
loading={updateLoading}
type="primary"
disabled={data.bills_by_pk.is_credit_memo}
onClick={() => {
delete search.billid;
history.push({ search: queryString.stringify(search) });
setPartsOrderContext({
actions: {},
context: {
jobId: data.bills_by_pk.jobid,
vendorId: data.bills_by_pk.vendorid,
returnFromBill: data.bills_by_pk.id,
invoiceNumber: data.bills_by_pk.invoice_number,
linesToOrder: data.bills_by_pk.billlines.map((i) => {
return {
line_desc: i.line_desc,
// db_price: i.actual_price,
act_price: i.actual_price,
cost: i.actual_cost,
quantity: i.quantity,
joblineid: i.joblineid,
};
}),
isReturn: true,
},
});
}}
>
{t("general.actions.save")}
{t("bills.actions.return")}
</Button>
</Popconfirm>
<Popconfirm
visible={visible}
onConfirm={() => form.submit()}
onCancel={() => setVisible(false)}
okButtonProps={{ loading: updateLoading }}
title={t("bills.labels.editadjwarning")}
>
<Button
htmlType="submit"
disabled={exported}
onClick={handleSave}
loading={updateLoading}
type="primary"
>
{t("general.actions.save")}
</Button>
</Popconfirm>
<BillReeportButtonComponent bill={data && data.bills_by_pk} />
</Space>
}
/>
<Form

View File

@@ -147,16 +147,6 @@ function BillEnterModalContainer({
})
);
// await updateJobLines({
// variables: {
// ids: remainingValues.billlines
// .filter((il) => il.joblineid !== "noline")
// .map((li) => li.joblineid),
// status: bodyshop.md_order_statuses.default_received || "Received*",
// location: location,
// },
// });
/////////////////////////
if (upload && upload.length > 0) {
//insert Each of the documents?
@@ -191,7 +181,10 @@ function BillEnterModalContainer({
};
const handleCancel = () => {
toggleModalVisible();
const r = window.confirm(t("general.labels.cancel"));
if (r === true) {
toggleModalVisible();
}
};
useEffect(() => {
@@ -218,6 +211,8 @@ function BillEnterModalContainer({
useEffect(() => {
if (billEnterModal.visible) {
form.setFieldsValue(formValues);
} else {
form.resetFields();
}
}, [billEnterModal.visible, form, formValues]);
@@ -227,6 +222,7 @@ function BillEnterModalContainer({
width={"90%"}
visible={billEnterModal.visible}
okText={t("general.actions.save")}
keyboard="false"
onOk={() => form.submit()}
onCancel={handleCancel}
afterClose={() => form.resetFields()}
@@ -259,7 +255,7 @@ function BillEnterModalContainer({
onFinishFailed={() => {
setEnterAgain(false);
}}
initialValues={formValues}
// initialValues={formValues}
>
<BillFormContainer
form={form}

View File

@@ -82,7 +82,7 @@ export function BillFormComponent({
rules={[
{
required: true,
message: t("general.validation.required"),
//message: t("general.validation.required"),
},
]}
>
@@ -104,7 +104,7 @@ export function BillFormComponent({
rules={[
{
required: true,
message: t("general.validation.required"),
//message: t("general.validation.required"),
},
]}
>
@@ -124,7 +124,7 @@ export function BillFormComponent({
rules={[
{
required: true,
message: t("general.validation.required"),
//message: t("general.validation.required"),
},
({ getFieldValue }) => ({
async validator(rule, value) {
@@ -165,7 +165,7 @@ export function BillFormComponent({
rules={[
{
required: true,
message: t("general.validation.required"),
//message: t("general.validation.required"),
},
]}
>
@@ -184,7 +184,7 @@ export function BillFormComponent({
rules={[
{
required: true,
message: t("general.validation.required"),
//message: t("general.validation.required"),
},
]}
>
@@ -202,23 +202,99 @@ export function BillFormComponent({
</LayoutFormRow>
<LayoutFormRow>
<Form.Item
span={3}
label={t("bills.fields.federal_tax_rate")}
name="federal_tax_rate"
>
<CurrencyInput min={0} disabled={disabled} />
</Form.Item>
<Form.Item
span={3}
label={t("bills.fields.state_tax_rate")}
name="state_tax_rate"
>
<CurrencyInput min={0} disabled={disabled} />
</Form.Item>
<Form.Item
span={3}
label={t("bills.fields.local_tax_rate")}
name="local_tax_rate"
>
<CurrencyInput min={0} />
</Form.Item>
<Form.Item shouldUpdate span={15}>
{() => {
const values = form.getFieldsValue([
"billlines",
"total",
"federal_tax_rate",
"state_tax_rate",
"local_tax_rate",
]);
let totals;
if (
!!values.total &&
!!values.billlines &&
values.billlines.length > 0
)
totals = CalculateBillTotal(values);
if (!!totals)
return (
<div>
<Space wrap>
<Statistic
title={t("bills.labels.subtotal")}
value={totals.subtotal.toFormat()}
precision={2}
/>
<Statistic
title={t("bills.labels.federal_tax")}
value={totals.federalTax.toFormat()}
precision={2}
/>
<Statistic
title={t("bills.labels.state_tax")}
value={totals.stateTax.toFormat()}
precision={2}
/>
<Statistic
title={t("bills.labels.local_tax")}
value={totals.localTax.toFormat()}
precision={2}
/>
<Statistic
title={t("bills.labels.entered_total")}
value={totals.enteredTotal.toFormat()}
precision={2}
/>
<Statistic
title={t("bills.labels.bill_total")}
value={totals.invoiceTotal.toFormat()}
precision={2}
/>
<Statistic
title={t("bills.labels.discrepancy")}
valueStyle={{
color:
totals.discrepancy.getAmount() === 0
? "green"
: "red",
}}
value={totals.discrepancy.toFormat()}
precision={2}
/>
</Space>
{form.getFieldValue("is_credit_memo") ? (
<AlertComponent
type="warning"
message={t("bills.labels.enteringcreditmemo")}
/>
) : null}
</div>
);
return null;
}}
</Form.Item>
</LayoutFormRow>
<Divider orientation="left">{t("bills.labels.bill_lines")}</Divider>
<BillFormLines
@@ -244,77 +320,6 @@ export function BillFormComponent({
<Button>Click to upload</Button>
</Upload>
</Form.Item>
<Form.Item shouldUpdate>
{() => {
const values = form.getFieldsValue([
"billlines",
"total",
"federal_tax_rate",
"state_tax_rate",
"local_tax_rate",
]);
let totals;
if (
!!values.total &&
!!values.billlines &&
values.billlines.length > 0
)
totals = CalculateBillTotal(values);
if (!!totals)
return (
<div>
<Space split={<Divider type="vertical" />}>
<Statistic
title={t("bills.labels.subtotal")}
value={totals.subtotal.toFormat()}
precision={2}
/>
<Statistic
title={t("bills.labels.federal_tax")}
value={totals.federalTax.toFormat()}
precision={2}
/>
<Statistic
title={t("bills.labels.state_tax")}
value={totals.stateTax.toFormat()}
precision={2}
/>
<Statistic
title={t("bills.labels.local_tax")}
value={totals.localTax.toFormat()}
precision={2}
/>
<Statistic
title={t("bills.labels.entered_total")}
value={totals.enteredTotal.toFormat()}
precision={2}
/>
<Statistic
title={t("bills.labels.bill_total")}
value={totals.invoiceTotal.toFormat()}
precision={2}
/>
<Statistic
title={t("bills.labels.discrepancy")}
valueStyle={{
color:
totals.discrepancy.getAmount() === 0 ? "green" : "red",
}}
value={totals.discrepancy.toFormat()}
precision={2}
/>
</Space>
{form.getFieldValue("is_credit_memo") ? (
<AlertComponent
type="warning"
message={t("bills.labels.enteringcreditmemo")}
/>
) : null}
</div>
);
return null;
}}
</Form.Item>
</div>
);
}

View File

@@ -1,4 +1,4 @@
import { WarningOutlined } from "@ant-design/icons";
import { DeleteFilled, WarningOutlined } from "@ant-design/icons";
import {
Button,
Form,
@@ -36,375 +36,397 @@ export function BillEnterModalLinesComponent({
const { t } = useTranslation();
const { setFieldsValue, getFieldsValue, getFieldValue } = form;
const columns = [
{
title: t("billlines.fields.jobline"),
dataIndex: "joblineid",
editable: true,
width: "10%",
formItemProps: (field) => {
return {
key: `${field.index}joblinename`,
name: [field.name, "joblineid"],
rules: [
{
required: true,
message: t("general.validation.required"),
},
],
};
},
formInput: (record, index) => (
<BillLineSearchSelect
disabled={disabled}
options={lineData}
onSelect={(value, opt) => {
setFieldsValue({
billlines: getFieldsValue(["billlines"]).billlines.map(
(item, idx) => {
if (idx === index) {
return {
...item,
line_desc: opt.line_desc,
quantity: opt.part_qty || 1,
actual_price: opt.cost,
cost_center: opt.part_type
? responsibilityCenters.defaults.costs[opt.part_type] ||
null
: null,
};
const columns = (remove) => {
return [
{
title: t("billlines.fields.jobline"),
dataIndex: "joblineid",
editable: true,
formItemProps: (field) => {
return {
key: `${field.index}joblinename`,
name: [field.name, "joblineid"],
rules: [
{
required: true,
//message: t("general.validation.required"),
},
],
};
},
formInput: (record, index) => (
<BillLineSearchSelect
disabled={disabled}
options={lineData}
style={{ width: "100%", minWidth: "10rem" }}
onSelect={(value, opt) => {
setFieldsValue({
billlines: getFieldsValue(["billlines"]).billlines.map(
(item, idx) => {
if (idx === index) {
return {
...item,
line_desc: opt.line_desc,
quantity: opt.part_qty || 1,
actual_price: opt.cost,
cost_center: opt.part_type
? responsibilityCenters.defaults.costs[
opt.part_type
] || null
: null,
};
}
return item;
}
return item;
}
),
});
}}
/>
),
},
{
title: t("billlines.fields.line_desc"),
dataIndex: "line_desc",
editable: true,
formItemProps: (field) => {
return {
key: `${field.index}line_desc`,
name: [field.name, "line_desc"],
rules: [
{
required: true,
message: t("general.validation.required"),
},
],
};
),
});
}}
/>
),
},
formInput: (record, index) => <Input disabled={disabled} />,
},
{
title: t("billlines.fields.quantity"),
dataIndex: "quantity",
editable: true,
formItemProps: (field) => {
return {
key: `${field.index}quantity`,
name: [field.name, "quantity"],
rules: [
{
required: true,
message: t("general.validation.required"),
},
],
};
{
title: t("billlines.fields.line_desc"),
dataIndex: "line_desc",
editable: true,
formItemProps: (field) => {
return {
key: `${field.index}line_desc`,
name: [field.name, "line_desc"],
rules: [
{
required: true,
//message: t("general.validation.required"),
},
],
};
},
formInput: (record, index) => <Input disabled={disabled} />,
},
formInput: (record, index) => (
<InputNumber precision={0} min={0} disabled={disabled} />
),
},
{
title: t("billlines.fields.actual_price"),
dataIndex: "actual_price",
editable: true,
formItemProps: (field) => {
return {
key: `${field.index}actual_price`,
name: [field.name, "actual_price"],
rules: [
{
required: true,
message: t("general.validation.required"),
},
],
};
{
title: t("billlines.fields.quantity"),
dataIndex: "quantity",
editable: true,
width: "4rem",
formItemProps: (field) => {
return {
key: `${field.index}quantity`,
name: [field.name, "quantity"],
rules: [
{
required: true,
//message: t("general.validation.required"),
},
],
};
},
formInput: (record, index) => (
<InputNumber precision={0} min={0} disabled={disabled} />
),
},
formInput: (record, index) => (
<CurrencyInput
min={0}
disabled={disabled}
onBlur={(e) => {
setFieldsValue({
billlines: getFieldsValue("billlines").billlines.map(
(item, idx) => {
console.log("Checking", index, idx);
if (idx === index) {
console.log(
"Found and setting.",
!!item.actual_cost
? item.actual_cost
: Math.round(
(parseFloat(e.target.value) * (1 - discount) +
Number.EPSILON) *
100
) / 100
);
return {
...item,
actual_cost: !!item.actual_cost
? item.actual_cost
: Math.round(
(parseFloat(e.target.value) * (1 - discount) +
Number.EPSILON) *
100
) / 100,
};
{
title: t("billlines.fields.actual_price"),
dataIndex: "actual_price",
width: "8rem",
editable: true,
formItemProps: (field) => {
return {
key: `${field.index}actual_price`,
name: [field.name, "actual_price"],
rules: [
{
required: true,
//message: t("general.validation.required"),
},
],
};
},
formInput: (record, index) => (
<CurrencyInput
min={0}
disabled={disabled}
onBlur={(e) => {
setFieldsValue({
billlines: getFieldsValue("billlines").billlines.map(
(item, idx) => {
console.log("Checking", index, idx);
if (idx === index) {
console.log(
"Found and setting.",
!!item.actual_cost
? item.actual_cost
: Math.round(
(parseFloat(e.target.value) * (1 - discount) +
Number.EPSILON) *
100
) / 100
);
return {
...item,
actual_cost: !!item.actual_cost
? item.actual_cost
: Math.round(
(parseFloat(e.target.value) * (1 - discount) +
Number.EPSILON) *
100
) / 100,
};
}
return item;
}
return item;
}
),
});
}}
/>
),
},
{
title: t("billlines.fields.actual_cost"),
dataIndex: "actual_cost",
editable: true,
formItemProps: (field) => {
return {
key: `${field.index}actual_cost`,
name: [field.name, "actual_cost"],
rules: [
{
required: true,
message: t("general.validation.required"),
},
],
};
),
});
}}
/>
),
},
formInput: (record, index) => (
<CurrencyInput min={0} disabled={disabled} />
),
additional: (record, index) => (
<Form.Item shouldUpdate>
{() => {
const line = getFieldsValue(["billlines"]).billlines[index];
if (!!!line) return null;
const lineDiscount = (
1 -
Math.round((line.actual_cost / line.actual_price) * 100) / 100
).toPrecision(2);
{
title: t("billlines.fields.actual_cost"),
dataIndex: "actual_cost",
editable: true,
width: "8rem",
formItemProps: (field) => {
return {
key: `${field.index}actual_cost`,
name: [field.name, "actual_cost"],
rules: [
{
required: true,
//message: t("general.validation.required"),
},
],
};
},
formInput: (record, index) => (
<CurrencyInput min={0} disabled={disabled} />
),
additional: (record, index) => (
<Form.Item shouldUpdate>
{() => {
const line = getFieldsValue(["billlines"]).billlines[index];
if (!!!line) return null;
const lineDiscount = (
1 -
Math.round((line.actual_cost / line.actual_price) * 100) / 100
).toPrecision(2);
if (lineDiscount - discount === 0) return <div />;
return <WarningOutlined style={{ color: "red" }} />;
}}
</Form.Item>
),
},
{
title: t("billlines.fields.cost_center"),
dataIndex: "cost_center",
editable: true,
formItemProps: (field) => {
return {
key: `${field.index}cost_center`,
name: [field.name, "cost_center"],
rules: [
{
required: true,
message: t("general.validation.required"),
},
],
};
if (lineDiscount - discount === 0) return <div />;
return <WarningOutlined style={{ color: "red" }} />;
}}
</Form.Item>
),
},
formInput: (record, index) => (
<Select style={{ width: "150px" }} disabled={disabled}>
{responsibilityCenters.costs.map((item) => (
<Select.Option key={item.name}>{item.name}</Select.Option>
))}
</Select>
),
},
{
title: t("billlines.fields.federal_tax_applicable"),
dataIndex: "applicable_taxes.federal",
editable: true,
formItemProps: (field) => {
return {
key: `${field.index}fedtax`,
valuePropName: "checked",
initialValue: true,
name: [field.name, "applicable_taxes", "federal"],
};
{
title: t("billlines.fields.cost_center"),
dataIndex: "cost_center",
editable: true,
formItemProps: (field) => {
return {
key: `${field.index}cost_center`,
name: [field.name, "cost_center"],
rules: [
{
required: true,
//message: t("general.validation.required"),
},
],
};
},
formInput: (record, index) => (
<Select style={{ minWidth: "3rem" }} disabled={disabled}>
{responsibilityCenters.costs.map((item) => (
<Select.Option key={item.name}>{item.name}</Select.Option>
))}
</Select>
),
},
formInput: (record, index) => <Switch disabled={disabled} />,
},
{
title: t("billlines.fields.state_tax_applicable"),
dataIndex: "applicable_taxes.state",
editable: true,
formItemProps: (field) => {
return {
key: `${field.index}statetax`,
valuePropName: "checked",
name: [field.name, "applicable_taxes", "state"],
};
},
formInput: (record, index) => <Switch disabled={disabled} />,
},
{
title: t("billlines.fields.local_tax_applicable"),
dataIndex: "applicable_taxes.local",
editable: true,
formItemProps: (field) => {
return {
key: `${field.index}localtax`,
valuePropName: "checked",
name: [field.name, "applicable_taxes", "local"],
};
},
formInput: (record, index) => <Switch disabled={disabled} />,
},
{
title: t("billlines.fields.location"),
dataIndex: "location",
editable: true,
formItemProps: (field) => {
return {
key: `${field.index}location`,
name: [field.name, "location"],
};
{
title: t("billlines.fields.location"),
dataIndex: "location",
editable: true,
formItemProps: (field) => {
return {
key: `${field.index}location`,
name: [field.name, "location"],
};
},
formInput: (record, index) => (
<Select disabled={disabled}>
{bodyshop.md_parts_locations.map((loc, idx) => (
<Select.Option key={idx} value={loc}>
{loc}
</Select.Option>
))}
</Select>
),
},
formInput: (record, index) => (
<Select style={{ width: "150px" }} disabled={disabled}>
{bodyshop.md_parts_locations.map((loc, idx) => (
<Select.Option key={idx} value={loc}>
{loc}
</Select.Option>
))}
</Select>
),
},
{
title: t("billlines.labels.deductedfromlbr"),
dataIndex: "deductedfromlbr",
editable: true,
formItemProps: (field) => {
return {
valuePropName: "checked",
key: `${field.index}deductedfromlbr`,
name: [field.name, "deductedfromlbr"],
};
{
title: t("billlines.labels.deductedfromlbr"),
dataIndex: "deductedfromlbr",
editable: true,
formItemProps: (field) => {
return {
valuePropName: "checked",
key: `${field.index}deductedfromlbr`,
name: [field.name, "deductedfromlbr"],
};
},
formInput: (record, index) => <Switch disabled={disabled} />,
additional: (record, index) => (
<Form.Item shouldUpdate style={{ display: "inline-block" }}>
{() => {
if (getFieldValue(["billlines", record.name, "deductedfromlbr"]))
return (
<div>
<Form.Item
label={t("joblines.fields.mod_lbr_ty")}
key={`${index}modlbrty`}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
name={[record.name, "lbr_adjustment", "mod_lbr_ty"]}
>
<Select allowClear>
<Select.Option value="LAA">
{t("joblines.fields.lbr_types.LAA")}
</Select.Option>
<Select.Option value="LAB">
{t("joblines.fields.lbr_types.LAB")}
</Select.Option>
<Select.Option value="LAD">
{t("joblines.fields.lbr_types.LAD")}
</Select.Option>
<Select.Option value="LAE">
{t("joblines.fields.lbr_types.LAE")}
</Select.Option>
<Select.Option value="LAF">
{t("joblines.fields.lbr_types.LAF")}
</Select.Option>
<Select.Option value="LAG">
{t("joblines.fields.lbr_types.LAG")}
</Select.Option>
<Select.Option value="LAM">
{t("joblines.fields.lbr_types.LAM")}
</Select.Option>
<Select.Option value="LAR">
{t("joblines.fields.lbr_types.LAR")}
</Select.Option>
<Select.Option value="LAS">
{t("joblines.fields.lbr_types.LAS")}
</Select.Option>
<Select.Option value="LAU">
{t("joblines.fields.lbr_types.LAU")}
</Select.Option>
<Select.Option value="LA1">
{t("joblines.fields.lbr_types.LA1")}
</Select.Option>
<Select.Option value="LA2">
{t("joblines.fields.lbr_types.LA2")}
</Select.Option>
<Select.Option value="LA3">
{t("joblines.fields.lbr_types.LA3")}
</Select.Option>
<Select.Option value="LA4">
{t("joblines.fields.lbr_types.LA4")}
</Select.Option>
</Select>
</Form.Item>
<Form.Item
label={t("jobs.labels.adjustmentrate")}
name={[record.name, "lbr_adjustment", "rate"]}
initialValue={bodyshop.default_adjustment_rate}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<InputNumber precision={2} min={0.01} />
</Form.Item>
</div>
);
return <></>;
}}
</Form.Item>
),
},
formInput: (record, index) => <Switch disabled={disabled} />,
additional: (record, index) => (
<Form.Item shouldUpdate style={{ display: "inline-block" }}>
{() => {
if (getFieldValue(["billlines", record.name, "deductedfromlbr"]))
return (
<div>
<Form.Item
label={t("joblines.fields.mod_lbr_ty")}
key={`${index}modlbrty`}
rules={[
{
required: true,
message: t("general.validation.required"),
},
]}
name={[record.name, "lbr_adjustment", "mod_lbr_ty"]}
>
<Select allowClear>
<Select.Option value="LAA">
{t("joblines.fields.lbr_types.LAA")}
</Select.Option>
<Select.Option value="LAB">
{t("joblines.fields.lbr_types.LAB")}
</Select.Option>
<Select.Option value="LAD">
{t("joblines.fields.lbr_types.LAD")}
</Select.Option>
<Select.Option value="LAE">
{t("joblines.fields.lbr_types.LAE")}
</Select.Option>
<Select.Option value="LAF">
{t("joblines.fields.lbr_types.LAF")}
</Select.Option>
<Select.Option value="LAG">
{t("joblines.fields.lbr_types.LAG")}
</Select.Option>
<Select.Option value="LAM">
{t("joblines.fields.lbr_types.LAM")}
</Select.Option>
<Select.Option value="LAR">
{t("joblines.fields.lbr_types.LAR")}
</Select.Option>
<Select.Option value="LAS">
{t("joblines.fields.lbr_types.LAS")}
</Select.Option>
<Select.Option value="LAU">
{t("joblines.fields.lbr_types.LAU")}
</Select.Option>
<Select.Option value="LA1">
{t("joblines.fields.lbr_types.LA1")}
</Select.Option>
<Select.Option value="LA2">
{t("joblines.fields.lbr_types.LA2")}
</Select.Option>
<Select.Option value="LA3">
{t("joblines.fields.lbr_types.LA3")}
</Select.Option>
<Select.Option value="LA4">
{t("joblines.fields.lbr_types.LA4")}
</Select.Option>
</Select>
</Form.Item>
<Form.Item
label={t("jobs.labels.adjustmentrate")}
name={[record.name, "lbr_adjustment", "rate"]}
initialValue={bodyshop.default_adjustment_rate}
rules={[
{
required: true,
message: t("general.validation.required"),
},
]}
>
<InputNumber precision={2} min={0.01} />
</Form.Item>
</div>
);
return <span />;
}}
</Form.Item>
),
},
];
{
title: t("billlines.fields.federal_tax_applicable"),
dataIndex: "applicable_taxes.federal",
editable: true,
const mergedColumns = columns.map((col) => {
if (!col.editable) return col;
return {
...col,
onCell: (record) => ({
record,
formItemProps: col.formItemProps,
formInput: col.formInput,
additional: col.additional,
dataIndex: col.dataIndex,
title: col.title,
}),
};
});
formItemProps: (field) => {
return {
key: `${field.index}fedtax`,
valuePropName: "checked",
initialValue: true,
name: [field.name, "applicable_taxes", "federal"],
};
},
formInput: (record, index) => <Switch disabled={disabled} />,
},
{
title: t("billlines.fields.state_tax_applicable"),
dataIndex: "applicable_taxes.state",
editable: true,
formItemProps: (field) => {
return {
key: `${field.index}statetax`,
valuePropName: "checked",
name: [field.name, "applicable_taxes", "state"],
};
},
formInput: (record, index) => <Switch disabled={disabled} />,
},
{
title: t("billlines.fields.local_tax_applicable"),
dataIndex: "applicable_taxes.local",
editable: true,
formItemProps: (field) => {
return {
key: `${field.index}localtax`,
valuePropName: "checked",
name: [field.name, "applicable_taxes", "local"],
};
},
formInput: (record, index) => <Switch disabled={disabled} />,
},
{
title: t("general.labels.actions"),
dataIndex: "actions",
render: (text, record) => (
<Button disabled={disabled} onClick={() => remove(record.name)}>
<DeleteFilled />
</Button>
),
},
];
};
const mergedColumns = (remove) =>
columns(remove).map((col) => {
if (!col.editable) return col;
return {
...col,
onCell: (record) => ({
record,
formItemProps: col.formItemProps,
formInput: col.formInput,
additional: col.additional,
dataIndex: col.dataIndex,
title: col.title,
}),
};
});
return (
<Form.List name="billlines">
@@ -420,7 +442,7 @@ export function BillEnterModalLinesComponent({
size="small"
bordered
dataSource={fields}
columns={mergedColumns}
columns={mergedColumns(remove)}
scroll={{ x: true }}
rowClassName="editable-row"
/>
@@ -462,12 +484,12 @@ const EditableCell = ({
if (additional)
return (
<td {...restProps}>
<Space>
<Space size="small">
<Form.Item
name={dataIndex}
{...(formItemProps && formItemProps(record))}
>
{formInput && formInput(record, record.key)}
{(formInput && formInput(record, record.key)) || children}
</Form.Item>
{additional && additional(record, record.key)}
</Space>
@@ -477,7 +499,7 @@ const EditableCell = ({
return (
<td {...restProps}>
<Form.Item name={dataIndex} {...(formItemProps && formItemProps(record))}>
{formInput && formInput(record, record.key)}
{(formInput && formInput(record, record.key)) || children}
</Form.Item>
</td>
);

View File

@@ -1,9 +1,6 @@
import Dinero from "dinero.js";
import { logImEXEvent } from "../../firebase/firebase.utils";
export const CalculateBillTotal = (invoice) => {
logImEXEvent("invoice_calculate_total");
const {
total,
billlines,

View File

@@ -1,37 +1,19 @@
import { Select, Tag } from "antd";
import React, { forwardRef, useEffect, useState } from "react";
import { Select } from "antd";
import React, { forwardRef } from "react";
import { useTranslation } from "react-i18next";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
//To be used as a form element only.
const { Option } = Select;
const BillLineSearchSelect = (
{ value, onChange, options, onBlur, onSelect, disabled },
ref
) => {
const [option, setOption] = useState(value);
const BillLineSearchSelect = ({ options, disabled, ...restProps }, ref) => {
const { t } = useTranslation();
useEffect(() => {
if (value !== option && onChange) {
onChange(option);
}
}, [value, option, onChange]);
return (
<Select
disabled={disabled}
ref={ref}
showSearch
autoFocus
value={option}
style={{
width: "100%",
}}
onChange={setOption}
optionFilterProp="line_desc"
onBlur={onBlur}
onSelect={onSelect}
{...restProps}
>
<Select.Option key={null} value={"noline"} cost={0} line_desc={""}>
{t("billlines.labels.other")}
@@ -46,17 +28,9 @@ const BillLineSearchSelect = (
line_desc={item.line_desc}
part_qty={item.part_qty}
>
<div className="imex-flex-row">
<div style={{ flex: 1 }}>{item.line_desc}</div>
{item.oem_partno ? (
<Tag color="blue">{item.oem_partno}</Tag>
) : null}
{item.act_price ? (
<Tag color="green">
<CurrencyFormatter>{item.act_price || 0}</CurrencyFormatter>
</Tag>
) : null}
</div>
{`${item.line_desc}${
item.oem_partno ? ` - ${item.oem_partno}` : ""
}`}
</Option>
))
: null}

View File

@@ -0,0 +1,80 @@
import { useMutation } from "@apollo/client";
import { Button, notification } from "antd";
import { gql } from "@apollo/client";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import {
selectAuthLevel,
selectBodyshop,
} from "../../redux/user/user.selectors";
import { HasRbacAccess } from "../rbac-wrapper/rbac-wrapper.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
authLevel: selectAuthLevel,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(BillMarkForReexportButton);
export function BillMarkForReexportButton({ bodyshop, authLevel, bill }) {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const [updateBill] = useMutation(gql`
mutation UPDATE_BILL($billId: uuid!) {
update_bills(where: { id: { _eq: $billId } }, _set: { exported: false }) {
returning {
id
exported
exported_at
}
}
}
`);
const handleUpdate = async () => {
setLoading(true);
const result = await updateBill({
variables: { billId: bill.id },
});
if (!result.errors) {
notification["success"]({ message: t("bills.successes.save") });
} else {
notification["error"]({
message: t("bills.errors.saving", {
error: JSON.stringify(result.errors),
}),
});
}
setLoading(false);
//Get the owner details, populate it all back into the job.
};
const hasAccess = HasRbacAccess({
bodyshop,
authLevel,
action: "bills:reexport",
});
if (hasAccess)
return (
<Button
loading={loading}
disabled={!bill.exported}
onClick={handleUpdate}
>
{t("bills.labels.markforreexport")}
</Button>
);
return <></>;
}

View File

@@ -1,26 +1,13 @@
import { EyeFilled, SyncOutlined } from "@ant-design/icons";
import {
Button,
Card,
Checkbox,
Descriptions,
Drawer,
Grid,
Input,
PageHeader,
Space,
Table,
} from "antd";
import queryString from "query-string";
import { Button, Card, Checkbox, Input, Space, Table } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { Link, useLocation } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { setModalContext } from "../../redux/modals/modals.actions";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { DateFormatter } from "../../utils/DateFormatter";
import { alphaSort } 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 PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
@@ -47,27 +34,12 @@ export function BillsListTableComponent({
setReconciliationContext,
}) {
const { t } = useTranslation();
const [selectedBillLinesByBill, setSelectedBillLinesByBill] = useState({});
const selectedBreakpoint = Object.entries(Grid.useBreakpoint())
.filter((screen) => !!screen[1])
.slice(-1)[0];
const bpoints = {
xs: "100%",
sm: "100%",
md: "100%",
lg: "75%",
xl: "75%",
xxl: "65%",
};
const drawerPercentage = selectedBreakpoint
? bpoints[selectedBreakpoint[0]]
: "100%";
const [state, setState] = useState({
sortedInfo: {},
});
const search = queryString.parse(useLocation().search);
const selectedBill = search.billid;
// const search = queryString.parse(useLocation().search);
// const selectedBill = search.billid;
const Templates = TemplateList("bill");
const bills = billsQuery.data ? billsQuery.data.bills : [];
const { refetch } = billsQuery;
@@ -78,16 +50,34 @@ export function BillsListTableComponent({
<EyeFilled />
</Button>
)}
{record.exported ? (
<Button disabled>{t("bills.actions.edit")}</Button>
) : (
<Link
to={`/manage/bills?billid=${record.id}&vendorid=${record.vendorid}`}
>
<Button>{t("bills.actions.edit")}</Button>
</Link>
)}
<BillDeleteButton bill={record} />
<Button
disabled={record.is_credit_memo}
onClick={() =>
setPartsOrderContext({
actions: {},
context: {
jobId: job.id,
vendorId: record.vendorid,
returnFromBill: record.id,
invoiceNumber: record.invoice_number,
linesToOrder: record.billlines.map((i) => {
return {
line_desc: i.line_desc,
// db_price: i.actual_price,
act_price: i.actual_price,
cost: i.actual_cost,
quantity: i.quantity,
joblineid: i.joblineid,
};
}),
isReturn: true,
},
})
}
>
{t("bills.actions.return")}
</Button>
{record.isinhouse && (
<PrintWrapperComponent
templateObject={{
@@ -122,7 +112,7 @@ export function BillsListTableComponent({
title: t("bills.fields.date"),
dataIndex: "date",
key: "date",
sorter: (a, b) => a.date - b.date,
sorter: (a, b) => dateSort(a.date, b.date),
sortOrder:
state.sortedInfo.columnKey === "date" && state.sortedInfo.order,
render: (text, record) => <DateFormatter>{record.date}</DateFormatter>,
@@ -164,189 +154,11 @@ export function BillsListTableComponent({
render: (text, record) => recordActions(record, true),
},
];
const selectedBillRecord = bills.find((r) => r.id === selectedBill);
const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
};
const rowExpander = (record) => {
const columns = [
{
title: t("billlines.fields.line_desc"),
dataIndex: "line_desc",
key: "line_desc",
sorter: (a, b) => alphaSort(a.line_desc, b.line_desc),
sortOrder:
state.sortedInfo.columnKey === "line_desc" && state.sortedInfo.order,
},
{
title: t("billlines.fields.actual_price"),
dataIndex: "actual_price",
key: "actual_price",
sorter: (a, b) => a.actual_price - b.actual_price,
sortOrder:
state.sortedInfo.columnKey === "actual_price" &&
state.sortedInfo.order,
render: (text, record) => (
<CurrencyFormatter>{record.actual_price}</CurrencyFormatter>
),
},
{
title: t("billlines.fields.actual_cost"),
dataIndex: "actual_cost",
key: "actual_cost",
sorter: (a, b) => a.actual_cost - b.actual_cost,
sortOrder:
state.sortedInfo.columnKey === "actual_cost" &&
state.sortedInfo.order,
render: (text, record) => (
<CurrencyFormatter>{record.actual_cost}</CurrencyFormatter>
),
},
{
title: t("billlines.fields.quantity"),
dataIndex: "quantity",
key: "quantity",
sorter: (a, b) => a.quantity - b.quantity,
sortOrder:
state.sortedInfo.columnKey === "quantity" && state.sortedInfo.order,
},
{
title: t("billlines.fields.cost_center"),
dataIndex: "cost_center",
key: "cost_center",
sorter: (a, b) => alphaSort(a.cost_center, b.cost_center),
sortOrder:
state.sortedInfo.columnKey === "cost_center" &&
state.sortedInfo.order,
},
{
title: t("billlines.fields.federal_tax_applicable"),
dataIndex: "applicable_taxes.federal",
key: "applicable_taxes.federal",
render: (text, record) => (
<Checkbox
disabled
checked={
(record.applicable_taxes && record.applicable_taxes.federal) ||
false
}
/>
),
},
{
title: t("billlines.fields.state_tax_applicable"),
dataIndex: "applicable_taxes.state",
key: "applicable_taxes.state",
render: (text, record) => (
<Checkbox
disabled
checked={
(record.applicable_taxes && record.applicable_taxes.state) ||
false
}
/>
),
},
{
title: t("billlines.fields.local_tax_applicable"),
dataIndex: "applicable_taxes.local",
key: "applicable_taxes.local",
render: (text, record) => (
<Checkbox
disabled
checked={
(record.applicable_taxes && record.applicable_taxes.local) ||
false
}
/>
),
},
];
const handleOnBillrowclick = (selectedRows) => {
setSelectedBillLinesByBill({
...selectedBillLinesByBill,
[record.id]: selectedRows.map((r) => r.id),
});
};
return (
<>
<PageHeader
title={
record &&
`${t("bills.fields.invoice_number")} ${record.invoice_number}`
}
extra={recordActions(record)}
/>
<Descriptions>
<Descriptions.Item label={t("bills.fields.federal_tax_rate")}>
{`${record.federal_tax_rate}%` || ""}
</Descriptions.Item>
<Descriptions.Item label={t("bills.fields.state_tax_rate")}>
{`${record.state_tax_rate}%` || ""}
</Descriptions.Item>
<Descriptions.Item label={t("bills.fields.local_tax_rate")}>
{`${record.local_tax_rate}%` || ""}
</Descriptions.Item>
</Descriptions>
<Button
disabled={
!selectedBillLinesByBill[record.id] ||
(selectedBillLinesByBill[record.id] &&
selectedBillLinesByBill[record.id].length === 0) ||
record.is_credit_memo
}
onClick={() =>
setPartsOrderContext({
actions: {},
context: {
jobId: job.id,
vendorId: record.vendorid,
returnFromBill: record.id,
invoiceNumber: record.invoice_number,
linesToOrder: record.billlines
.filter((il) =>
selectedBillLinesByBill[record.id].includes(il.id)
)
.map((i) => {
return {
line_desc: i.line_desc,
// db_price: i.actual_price,
act_price: i.actual_price,
cost: i.actual_cost,
quantity: i.quantity,
joblineid: i.joblineid,
};
}),
isReturn: true,
},
})
}
>
{t("bills.actions.return")}
</Button>
<Table
scroll={{ x: "50%", y: "40rem" }}
columns={columns}
rowKey="id"
dataSource={record.billlines}
rowSelection={{
onSelect: (record, selected, selectedRows) => {
handleOnBillrowclick(selectedRows);
},
onSelectAll: (selected, selectedRows, changeRows) => {
handleOnBillrowclick(selectedRows);
},
selectedRowKeys: selectedBillLinesByBill[record.id],
type: "checkbox",
}}
/>
</>
);
};
return (
<Card
title={t("bills.labels.bills")}
@@ -355,7 +167,7 @@ export function BillsListTableComponent({
<Button onClick={() => refetch()}>
<SyncOutlined />
</Button>
{job ? (
{job && job.converted ? (
<>
<Button
onClick={() => {
@@ -394,20 +206,11 @@ export function BillsListTableComponent({
</Space>
}
>
<Drawer
placement="right"
onClose={() => handleOnRowClick(null)}
visible={selectedBill}
//getContainer={false}
style={{ position: "absolute" }}
closable
width={drawerPercentage}
>
{selectedBillRecord && rowExpander(selectedBillRecord)}
</Drawer>
<Table
loading={billsQuery.loading}
scroll={{ x: true, y: "50rem" }}
scroll={{
x: true, // y: "50rem"
}}
columns={columns}
rowKey="id"
dataSource={bills}

View File

@@ -0,0 +1,93 @@
import { Button, Form, Modal } from "antd";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { logImEXEvent } from "../../firebase/firebase.utils";
import { toggleModalVisible } from "../../redux/modals/modals.actions";
import { selectCaBcEtfTableConvert } from "../../redux/modals/modals.selectors";
import { GenerateDocument } from "../../utils/RenderTemplate";
import { TemplateList } from "../../utils/TemplateConstants";
import CaBcEtfTableModalComponent from "./ca-bc-etf-table.modal.component";
const mapStateToProps = createStructuredSelector({
caBcEtfTableModal: selectCaBcEtfTableConvert,
});
const mapDispatchToProps = (dispatch) => ({
toggleModalVisible: () =>
dispatch(toggleModalVisible("ca_bc_eftTableConvert")),
});
export function ContractsFindModalContainer({
caBcEtfTableModal,
toggleModalVisible,
}) {
const { t } = useTranslation();
const { visible } = caBcEtfTableModal;
const [loading, setLoading] = useState(false);
const [form] = Form.useForm();
const EtfTemplate = TemplateList("special").ca_bc_etf_table;
const handleFinish = async (values) => {
logImEXEvent("ca_bc_etf_table_parse");
setLoading(true);
const claimNumbers = [];
values.table.split("\n").forEach((row, idx, arr) => {
const { 1: claim, 2: shortclaim, 4: amount } = row.split("\t");
if (!claim || !shortclaim) return;
const trimmedShortClaim = shortclaim.trim();
// const trimmedClaim = claim.trim();
claimNumbers.push({ claim: trimmedShortClaim, amount });
});
await GenerateDocument(
{
name: EtfTemplate.key,
variables: {
claimNumbers: `%(${claimNumbers.map((c) => c.claim).join("|")})%`,
claimdata: claimNumbers,
},
},
{},
values.sendby === "email" ? "e" : "p"
);
setLoading(false);
};
useEffect(() => {
if (visible) {
form.resetFields();
}
}, [visible, form]);
return (
<Modal
visible={visible}
width="70%"
title={t("payments.labels.findermodal")}
onCancel={() => toggleModalVisible()}
onOk={() => toggleModalVisible()}
destroyOnClose
forceRender
>
<Form
form={form}
layout="vertical"
autoComplete="no"
onFinish={handleFinish}
>
<CaBcEtfTableModalComponent form={form} />
<Button onClick={() => form.submit()} type="primary" loading={loading}>
{t("general.labels.search")}
</Button>
</Form>
</Modal>
);
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(ContractsFindModalContainer);

View File

@@ -0,0 +1,42 @@
import { Form, Input, Radio } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
export default connect(mapStateToProps, null)(PartsReceiveModalComponent);
export function PartsReceiveModalComponent({ bodyshop, form }) {
const { t } = useTranslation();
return (
<div>
<Form.Item
name="table"
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<Input.TextArea rows={8} />
</Form.Item>
<Form.Item
label={t("general.labels.sendby")}
name="sendby"
initialValue="print"
>
<Radio.Group>
<Radio value="email">{t("general.labels.email")}</Radio>
<Radio value="print">{t("general.labels.print")}</Radio>
</Radio.Group>
</Form.Item>
</div>
);
}

View File

@@ -24,6 +24,7 @@ export function ChatMediaSelector({
conversation,
}) {
const { t } = useTranslation();
const [visible, setVisible] = useState(false);
const { loading, error, data } = useQuery(GET_DOCUMENTS_BY_JOB, {
variables: {
@@ -33,12 +34,11 @@ export function ChatMediaSelector({
},
fetchPolicy: "network-only",
skip:
!visible ||
!conversation.job_conversations ||
conversation.job_conversations.length === 0,
});
const [visible, setVisible] = useState(false);
const handleVisibleChange = (visible) => {
setVisible(visible);
};

View File

@@ -6,13 +6,23 @@ import { connect } from "react-redux";
import { openChatByPhone } from "../../redux/messaging/messaging.actions";
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
openChatByPhone: (phone) => dispatch(openChatByPhone(phone)),
});
export function ChatOpenButton({ phone, jobid, openChatByPhone }) {
export function ChatOpenButton({ bodyshop, phone, jobid, openChatByPhone }) {
const { t } = useTranslation();
if (!phone) return <></>;
if (!bodyshop.messagingservicesid)
return <PhoneNumberFormatter>{phone}</PhoneNumberFormatter>;
return (
<a
href="# "
@@ -31,4 +41,4 @@ export function ChatOpenButton({ phone, jobid, openChatByPhone }) {
</a>
);
}
export default connect(null, mapDispatchToProps)(ChatOpenButton);
export default connect(mapStateToProps, mapDispatchToProps)(ChatOpenButton);

View File

@@ -1,10 +1,9 @@
import { Checkbox, Form } from "antd";
import React from "react";
import { Form, Checkbox } from "antd";
import { useTranslation } from "react-i18next";
export default function JobIntakeFormCheckboxComponent({ formItem, readOnly }) {
const { name, label, required } = formItem;
const { t } = useTranslation();
return (
<Form.Item
name={name}
@@ -13,7 +12,7 @@ export default function JobIntakeFormCheckboxComponent({ formItem, readOnly }) {
rules={[
{
required: required,
message: t("general.validation.required"),
//message: t("general.validation.required"),
},
]}
>

View File

@@ -1,10 +1,9 @@
import React from "react";
import { Form, Rate } from "antd";
import { useTranslation } from "react-i18next";
import React from "react";
export default function JobIntakeFormCheckboxComponent({ formItem, readOnly }) {
const { name, label, required } = formItem;
const { t } = useTranslation();
return (
<Form.Item
name={name}
@@ -12,7 +11,7 @@ export default function JobIntakeFormCheckboxComponent({ formItem, readOnly }) {
rules={[
{
required: required,
message: t("general.validation.required"),
//message: t("general.validation.required"),
},
]}
>

View File

@@ -1,10 +1,9 @@
import { Form, Slider } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
export default function JobIntakeFormCheckboxComponent({ formItem, readOnly }) {
const { name, label, required, min, max } = formItem;
const { t } = useTranslation();
return (
<Form.Item
name={name}
@@ -12,7 +11,7 @@ export default function JobIntakeFormCheckboxComponent({ formItem, readOnly }) {
rules={[
{
required: required,
message: t("general.validation.required"),
//message: t("general.validation.required"),
},
]}
>

View File

@@ -1,10 +1,8 @@
import React from "react";
import { Form, Input } from "antd";
import { useTranslation } from "react-i18next";
import React from "react";
export default function JobIntakeFormCheckboxComponent({ formItem, readOnly }) {
const { name, label, required } = formItem;
const { t } = useTranslation();
return (
<Form.Item
name={name}
@@ -12,7 +10,7 @@ export default function JobIntakeFormCheckboxComponent({ formItem, readOnly }) {
rules={[
{
required: required,
message: t("general.validation.required"),
//message: t("general.validation.required"),
},
]}
>

View File

@@ -1,10 +1,9 @@
import React from "react";
import { Form, Input } from "antd";
import { useTranslation } from "react-i18next";
import React from "react";
export default function JobIntakeFormCheckboxComponent({ formItem, readOnly }) {
const { name, label, required, rows } = formItem;
const { t } = useTranslation();
return (
<Form.Item
name={name}
@@ -12,7 +11,7 @@ export default function JobIntakeFormCheckboxComponent({ formItem, readOnly }) {
rules={[
{
required: required,
message: t("general.validation.required"),
//message: t("general.validation.required"),
},
]}
>

View File

@@ -6,7 +6,7 @@ import { alphaSort } from "../../utils/sorters";
export default function ContractsCarsComponent({
loading,
data,
selectedCar,
selectedCarId,
handleSelect,
}) {
const [state, setState] = useState({
@@ -117,7 +117,7 @@ export default function ContractsCarsComponent({
rowSelection={{
onSelect: handleSelect,
type: "radio",
selectedRowKeys: [selectedCar],
selectedRowKeys: [selectedCarId],
}}
onRow={(record, rowIndex) => {
return {

View File

@@ -13,12 +13,13 @@ export default function ContractCarsContainer({ selectedCarState, form }) {
const [selectedCar, setSelectedCar] = selectedCarState;
const handleSelect = (record) => {
setSelectedCar(record.id);
setSelectedCar(record);
form.setFieldsValue({
kmstart: record.mileage,
dailyrate: record.dailycost,
fuelout: record.fuel,
damage: record.damage,
});
};
@@ -26,7 +27,7 @@ export default function ContractCarsContainer({ selectedCarState, form }) {
return (
<ContractCarsComponent
handleSelect={handleSelect}
selectedCar={selectedCar}
selectedCarId={selectedCar && selectedCar.id}
loading={loading}
data={data ? data.courtesycars : []}
/>

View File

@@ -314,7 +314,7 @@ export function ContractConvertToRo({
rules={[
{
required: true,
message: t("general.validation.required"),
//message: t("general.validation.required"),
},
]}
>
@@ -332,7 +332,7 @@ export function ContractConvertToRo({
rules={[
{
required: bodyshop.enforce_class,
message: t("general.validation.required"),
//message: t("general.validation.required"),
},
]}
>
@@ -349,7 +349,7 @@ export function ContractConvertToRo({
rules={[
{
required: true,
message: t("general.validation.required"),
//message: t("general.validation.required"),
},
]}
name={"applyCleanupCharge"}
@@ -364,7 +364,7 @@ export function ContractConvertToRo({
rules={[
{
required: true,
message: t("general.validation.required"),
//message: t("general.validation.required"),
},
]}
name={"refuelqty"}

View File

@@ -1,12 +1,16 @@
import { WarningFilled } from "@ant-design/icons";
import { Form, Input, InputNumber, Space } from "antd";
import moment from "moment";
import React from "react";
import { useTranslation } from "react-i18next";
import { DateFormatter } from "../../utils/DateFormatter";
import ContractLicenseDecodeButton from "../contract-license-decode-button/contract-license-decode-button.component";
import ContractStatusSelector from "../contract-status-select/contract-status-select.component";
import ContractsRatesChangeButton from "../contracts-rates-change-button/contracts-rates-change-button.component";
import CourtesyCarFuelSlider from "../courtesy-car-fuel-select/courtesy-car-fuel-select.component";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
import FormDateTimePicker from "../form-date-time-picker/form-date-time-picker.component";
import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
import InputNumberCalculator from "../form-input-number-calculator/form-input-number-calculator.component";
import InputPhone, {
PhoneItemFormatterValidation,
} from "../form-items-formatted/phone-form-item.component";
@@ -17,6 +21,7 @@ export default function ContractFormComponent({
form,
create = false,
selectedJobState,
selectedCar,
}) {
const { t } = useTranslation();
return (
@@ -30,7 +35,7 @@ export default function ContractFormComponent({
rules={[
{
required: true,
message: t("general.validation.required"),
//message: t("general.validation.required"),
},
]}
>
@@ -44,47 +49,90 @@ export default function ContractFormComponent({
rules={[
{
required: true,
message: t("general.validation.required"),
//message: t("general.validation.required"),
},
]}
>
<FormDatePicker />
<FormDateTimePicker />
</Form.Item>
<Form.Item
label={t("contracts.fields.scheduledreturn")}
name="scheduledreturn"
>
<FormDatePicker />
<FormDateTimePicker />
</Form.Item>
{create ? null : (
<Form.Item
label={t("contracts.fields.actualreturn")}
name="actualreturn"
>
<FormDatePicker />
<FormDateTimePicker />
</Form.Item>
)}
</LayoutFormRow>
<LayoutFormRow>
<LayoutFormRow grow>
<Form.Item
label={t("contracts.fields.kmstart")}
name="kmstart"
rules={[
{
required: true,
message: t("general.validation.required"),
//message: t("general.validation.required"),
},
]}
>
<InputNumber />
</Form.Item>
{create && (
<Form.Item
shouldUpdate={(p, c) =>
p.kmstart !== c.kmstart || p.scheduledreturn !== c.scheduledreturn
}
>
{() => {
const mileageOver =
selectedCar &&
selectedCar.nextservicekm <= form.getFieldValue("kmstart");
const dueForService =
selectedCar &&
selectedCar.nextservicedate &&
moment(selectedCar.nextservicedate).isBefore(
moment(form.getFieldValue("scheduledreturn"))
);
if (mileageOver || dueForService)
return (
<Space direction="vertical" style={{ color: "tomato" }}>
<span>
<WarningFilled style={{ marginRight: ".3rem" }} />
{t("contracts.labels.cardueforservice")}
</span>
<span>{`${
selectedCar && selectedCar.nextservicekm
} km`}</span>
<span>
<DateFormatter>
{selectedCar && selectedCar.nextservicedate}
</DateFormatter>
</span>
</Space>
);
return <></>;
}}
</Form.Item>
)}
{create ? null : (
<Form.Item label={t("contracts.fields.kmend")} name="kmend">
<InputNumber />
</Form.Item>
)}
<Form.Item label={t("contracts.fields.damage")} name="damage">
<Input.TextArea />
</Form.Item>
</LayoutFormRow>
<LayoutFormRow>
<LayoutFormRow grow>
<Form.Item
label={t("contracts.fields.fuelout")}
name="fuelout"
@@ -92,7 +140,7 @@ export default function ContractFormComponent({
rules={[
{
required: true,
message: t("general.validation.required"),
//message: t("general.validation.required"),
},
]}
>
@@ -128,34 +176,49 @@ export default function ContractFormComponent({
rules={[
{
required: true,
message: t("general.validation.required"),
//message: t("general.validation.required"),
},
]}
>
<Input />
</Form.Item>
<Form.Item
label={t("contracts.fields.driver_dlexpiry")}
name="driver_dlexpiry"
rules={[
{
required: true,
message: t("general.validation.required"),
},
]}
shouldUpdate={(p, c) =>
p.driver_dlexpiry !== c.driver_dlexpiry ||
p.scheduledreturn !== c.scheduledreturn
}
>
<FormDatePicker />
{() => {
const dlExpiresBeforeReturn = moment(
form.getFieldValue("driver_dlexpiry")
).isBefore(moment(form.getFieldValue("scheduledreturn")));
return (
<div>
<Form.Item
label={t("contracts.fields.driver_dlexpiry")}
name="driver_dlexpiry"
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<FormDatePicker />
</Form.Item>
{dlExpiresBeforeReturn && (
<Space style={{ color: "tomato" }}>
<WarningFilled />
<span>{t("contracts.labels.dlexpirebeforereturn")}</span>
</Space>
)}
</div>
);
}}
</Form.Item>
<Form.Item
label={t("contracts.fields.driver_dlst")}
name="driver_dlst"
rules={[
{
required: true,
message: t("general.validation.required"),
},
]}
>
<Form.Item label={t("contracts.fields.driver_dlst")} name="driver_dlst">
<Input />
</Form.Item>
<Form.Item
@@ -164,7 +227,7 @@ export default function ContractFormComponent({
rules={[
{
required: true,
message: t("general.validation.required"),
//message: t("general.validation.required"),
},
]}
>
@@ -176,7 +239,7 @@ export default function ContractFormComponent({
rules={[
{
required: true,
message: t("general.validation.required"),
//message: t("general.validation.required"),
},
]}
>
@@ -188,7 +251,7 @@ export default function ContractFormComponent({
rules={[
{
required: true,
message: t("general.validation.required"),
//message: t("general.validation.required"),
},
]}
>
@@ -218,7 +281,7 @@ export default function ContractFormComponent({
rules={[
{
required: true,
message: t("general.validation.required"),
//message: t("general.validation.required"),
},
({ getFieldValue }) =>
PhoneItemFormatterValidation(getFieldValue, "driver_ph1"),
@@ -230,6 +293,7 @@ export default function ContractFormComponent({
<FormDatePicker />
</Form.Item>
</LayoutFormRow>
<ContractsRatesChangeButton form={form} />
<LayoutFormRow header={t("contracts.labels.rates")}>
<Form.Item label={t("contracts.fields.dailyrate")} name="dailyrate">
<InputNumber precision={2} />
@@ -267,16 +331,16 @@ export default function ContractFormComponent({
<InputNumber precision={2} />
</Form.Item>
<Form.Item label={t("contracts.fields.federaltax")} name="federaltax">
<InputNumberCalculator precision={2} />
<InputNumber precision={2} />
</Form.Item>
<Form.Item label={t("contracts.fields.statetax")} name="statetax">
<InputNumberCalculator precision={2} />
<InputNumber precision={2} />
</Form.Item>
<Form.Item label={t("contracts.fields.localtax")} name="localtax">
<InputNumberCalculator precision={2} />
<InputNumber precision={2} />
</Form.Item>
<Form.Item label={t("contracts.fields.coverage")} name="coverage">
<InputNumberCalculator precision={2} />
<InputNumber precision={2} />
</Form.Item>
</LayoutFormRow>
</div>

View File

@@ -0,0 +1,37 @@
import { Form, Input } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import FormDateTimePicker from "../form-date-time-picker/form-date-time-picker.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
export default connect(mapStateToProps, null)(PartsReceiveModalComponent);
export function PartsReceiveModalComponent({ bodyshop, form }) {
const { t } = useTranslation();
return (
<div>
<Form.Item name="fleet" label={t("courtesycars.fields.fleetnumber")}>
<Input />
</Form.Item>
<Form.Item
name="time"
label={t("contracts.labels.time")}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<FormDateTimePicker />
</Form.Item>
</div>
);
}

View File

@@ -0,0 +1,169 @@
import { useLazyQuery } from "@apollo/client";
import { Button, Form, Modal, Table } from "antd";
import React, { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { Link } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { logImEXEvent } from "../../firebase/firebase.utils";
import { FIND_CONTRACT } from "../../graphql/cccontracts.queries";
import { toggleModalVisible } from "../../redux/modals/modals.actions";
import { selectContractFinder } from "../../redux/modals/modals.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { DateTimeFormatter } from "../../utils/DateFormatter";
import ContractsFindModalComponent from "./contracts-find-modal.component";
import AlertComponent from "../alert/alert.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
contractFinderModal: selectContractFinder,
});
const mapDispatchToProps = (dispatch) => ({
toggleModalVisible: () => dispatch(toggleModalVisible("contractFinder")),
});
export function ContractsFindModalContainer({
contractFinderModal,
toggleModalVisible,
bodyshop,
}) {
const { t } = useTranslation();
const { visible } = contractFinderModal;
const [form] = Form.useForm();
// const [updateJobLines] = useMutation(UPDATE_JOB_LINE);
const [callSearch, { loading, error, data }] = useLazyQuery(FIND_CONTRACT);
const handleFinish = async (values) => {
logImEXEvent("contract_finder_search");
//Execute contract find
callSearch({
variables: {
fleet:
(values.fleet && values.fleet !== "" && values.fleet) || undefined,
time: values.time,
},
});
};
useEffect(() => {
if (visible) {
form.resetFields();
}
}, [visible, form]);
return (
<Modal
visible={visible}
width="70%"
title={t("contracts.labels.findermodal")}
onCancel={() => toggleModalVisible()}
onOk={() => toggleModalVisible()}
destroyOnClose
forceRender
>
<Form
form={form}
layout="vertical"
autoComplete="no"
onFinish={handleFinish}
>
<ContractsFindModalComponent form={form} />
<Button onClick={() => form.submit()} type="primary" loading={loading}>
{t("general.labels.search")}
</Button>
{error && (
<AlertComponent type="error" message={JSON.stringify(error)} />
)}
<Table
loading={loading}
columns={[
{
title: t("contracts.fields.agreementnumber"),
dataIndex: "agreementnumber",
key: "agreementnumber",
render: (text, record) => (
<Link to={`/manage/courtesycars/contracts/${record.id}`}>
{record.agreementnumber || ""}
</Link>
),
},
{
title: t("jobs.fields.ro_number"),
dataIndex: "job.ro_number",
key: "job.ro_number",
render: (text, record) => (
<Link to={`/manage/jobs/${record.job.id}`}>
{record.job.ro_number || ""}
</Link>
),
},
{
title: t("contracts.fields.driver"),
dataIndex: "driver_ln",
key: "driver_ln",
render: (text, record) =>
`${record.driver_fn || ""} ${record.driver_ln || ""}`,
},
{
title: t("contracts.labels.vehicle"),
dataIndex: "vehicle",
key: "vehicle",
render: (text, record) => (
<Link to={`/manage/courtesycars/${record.courtesycar.id}`}>{`${
record.courtesycar.year
} ${record.courtesycar.make} ${record.courtesycar.model} ${
record.courtesycar.plate
? `(${record.courtesycar.plate})`
: ""
}`}</Link>
),
},
{
title: t("contracts.fields.status"),
dataIndex: "status",
render: (text, record) => t(record.status),
},
{
title: t("contracts.fields.start"),
dataIndex: "start",
key: "start",
render: (text, record) => (
<DateTimeFormatter>{record.start}</DateTimeFormatter>
),
},
{
title: t("contracts.fields.scheduledreturn"),
dataIndex: "scheduledreturn",
key: "scheduledreturn",
render: (text, record) => (
<DateTimeFormatter>{record.scheduledreturn}</DateTimeFormatter>
),
},
{
title: t("contracts.fields.actualreturn"),
dataIndex: "actualreturn",
key: "actualreturn",
render: (text, record) => (
<DateTimeFormatter>{record.actualreturn}</DateTimeFormatter>
),
},
]}
rowKey="id"
dataSource={data && data.cccontracts}
/>
</Form>
</Modal>
);
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(ContractsFindModalContainer);

View File

@@ -1,14 +1,33 @@
import { SyncOutlined } from "@ant-design/icons";
import { Button, Card, Input, Space, Table } from "antd";
import { Button, Card, Input, Space, Table, Typography } from "antd";
import queryString from "query-string";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { Link, useHistory, useLocation } from "react-router-dom";
import { DateTimeFormatter } from "../../utils/DateFormatter";
import { alphaSort } from "../../utils/sorters";
import TimeTicketsDatesSelector from "../ticket-tickets-dates-selector/time-tickets-dates-selector.component";
import ContractsFindModalContainer from "../contracts-find-modal/contracts-find-modal.container";
import { setModalContext } from "../../redux/modals/modals.actions";
export default function ContractsList({ loading, contracts, refetch, total }) {
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
setContractFinderContext: (context) =>
dispatch(setModalContext({ context: context, modal: "contractFinder" })),
});
export default connect(mapStateToProps, mapDispatchToProps)(ContractsList);
export function ContractsList({
loading,
contracts,
refetch,
total,
setContractFinderContext,
}) {
const [state, setState] = useState({
sortedInfo: {},
filteredInfo: { text: "" },
@@ -126,14 +145,29 @@ export default function ContractsList({ loading, contracts, refetch, total }) {
<Card
extra={
<Space wrap>
{search.search && (
<>
<Typography.Title level={4}>
{t("general.labels.searchresults", { search: search.search })}
</Typography.Title>
<Button
onClick={() => {
delete search.search;
history.push({ search: queryString.stringify(search) });
}}
>
{t("general.actions.clear")}
</Button>
</>
)}
<Button onClick={() => setContractFinderContext()}>
{t("contracts.actions.find")}
</Button>
<Button onClick={() => refetch()}>
<SyncOutlined />
</Button>
<TimeTicketsDatesSelector />
<Input.Search
placeholder={t("general.labels.search")}
placeholder={search.searh || t("general.labels.search")}
onSearch={(value) => {
search.search = value;
history.push({ search: queryString.stringify(search) });
@@ -142,9 +176,12 @@ export default function ContractsList({ loading, contracts, refetch, total }) {
</Space>
}
>
<ContractsFindModalContainer />
<Table
loading={loading}
scroll={{ x: "50%", y: "40rem" }}
scroll={{
x: "50%", //y: "40rem"
}}
pagination={{
position: "top",
pageSize: 25,

View File

@@ -0,0 +1,46 @@
import { DownOutlined } from "@ant-design/icons";
import { Dropdown, Menu } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
export function ContractsRatesChangeButton({ disabled, form, bodyshop }) {
const { t } = useTranslation();
const handleClick = ({ item, key, keyPath }) => {
const { label, ...rate } = item.props.value;
form.setFieldsValue(rate);
};
const menu = (
<div>
<Menu onClick={handleClick}>
{bodyshop.md_ccc_rates.map((rate, idx) => (
<Menu.Item value={rate} key={idx}>
{rate.label}
</Menu.Item>
))}
</Menu>
</div>
);
return (
<Dropdown overlay={menu} disabled={disabled}>
<a
className="ant-dropdown-link"
href=" #"
onClick={(e) => e.preventDefault()}
>
{t("contracts.actions.changerate")} <DownOutlined />
</a>
</Dropdown>
);
}
export default connect(mapStateToProps, null)(ContractsRatesChangeButton);

View File

@@ -1,16 +1,22 @@
import { Button, Form, Input, InputNumber, PageHeader } from "antd";
import { WarningFilled } from "@ant-design/icons";
import { useApolloClient } from "@apollo/client";
import { Button, Form, Input, InputNumber, PageHeader, Space } from "antd";
import moment from "moment";
import React from "react";
import { useTranslation } from "react-i18next";
import { CHECK_CC_FLEET_NUMBER } from "../../graphql/courtesy-car.queries";
import { DateFormatter } from "../../utils/DateFormatter";
import CourtesyCarFuelSlider from "../courtesy-car-fuel-select/courtesy-car-fuel-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 FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import InputNumberCalculator from "../form-input-number-calculator/form-input-number-calculator.component";
export default function CourtesyCarCreateFormComponent({ form, saveLoading }) {
const { t } = useTranslation();
const client = useApolloClient();
return (
<div>
<PageHeader
@@ -34,7 +40,7 @@ export default function CourtesyCarCreateFormComponent({ form, saveLoading }) {
rules={[
{
required: true,
message: t("general.validation.required"),
//message: t("general.validation.required"),
},
]}
>
@@ -46,7 +52,7 @@ export default function CourtesyCarCreateFormComponent({ form, saveLoading }) {
rules={[
{
required: true,
message: t("general.validation.required"),
//message: t("general.validation.required"),
},
]}
>
@@ -58,7 +64,7 @@ export default function CourtesyCarCreateFormComponent({ form, saveLoading }) {
rules={[
{
required: true,
message: t("general.validation.required"),
//message: t("general.validation.required"),
},
]}
>
@@ -70,7 +76,7 @@ export default function CourtesyCarCreateFormComponent({ form, saveLoading }) {
rules={[
{
required: true,
message: t("general.validation.required"),
//message: t("general.validation.required"),
},
]}
>
@@ -82,7 +88,7 @@ export default function CourtesyCarCreateFormComponent({ form, saveLoading }) {
rules={[
{
required: true,
message: t("general.validation.required"),
//message: t("general.validation.required"),
},
]}
>
@@ -94,7 +100,7 @@ export default function CourtesyCarCreateFormComponent({ form, saveLoading }) {
rules={[
{
required: true,
message: t("general.validation.required"),
//message: t("general.validation.required"),
},
]}
>
@@ -108,7 +114,7 @@ export default function CourtesyCarCreateFormComponent({ form, saveLoading }) {
rules={[
{
required: true,
message: t("general.validation.required"),
//message: t("general.validation.required"),
},
]}
>
@@ -117,6 +123,41 @@ export default function CourtesyCarCreateFormComponent({ form, saveLoading }) {
<Form.Item
label={t("courtesycars.fields.fleetnumber")}
name="fleetnumber"
validateTrigger="onBlur"
hasFeedback
rules={[
{
required: true,
//message: t("general.validation.required"),
},
({ getFieldValue }) => ({
async validator(rule, value) {
if (value) {
const response = await client.query({
query: CHECK_CC_FLEET_NUMBER,
variables: {
name: value,
},
});
if (
response.data.courtesycars_aggregate.aggregate.count === 0
) {
return Promise.resolve();
} else if (
response.data.courtesycars_aggregate.nodes.length === 1 &&
response.data.courtesycars_aggregate.nodes[0].id ===
form.getFieldValue("id")
) {
return Promise.resolve();
}
return Promise.reject(t("courtesycars.labels.uniquefleet"));
} else {
return Promise.resolve();
}
},
}),
]}
>
<Input />
</Form.Item>
@@ -154,7 +195,7 @@ export default function CourtesyCarCreateFormComponent({ form, saveLoading }) {
rules={[
{
required: true,
message: t("general.validation.required"),
//message: t("general.validation.required"),
},
]}
>
@@ -166,7 +207,7 @@ export default function CourtesyCarCreateFormComponent({ form, saveLoading }) {
rules={[
{
required: true,
message: t("general.validation.required"),
//message: t("general.validation.required"),
},
]}
>
@@ -178,24 +219,60 @@ export default function CourtesyCarCreateFormComponent({ form, saveLoading }) {
rules={[
{
required: true,
message: t("general.validation.required"),
//message: t("general.validation.required"),
},
]}
>
<InputNumberCalculator />
</Form.Item>
<Form.Item
label={t("courtesycars.fields.nextservicedate")}
name="nextservicedate"
rules={[
{
required: true,
message: t("general.validation.required"),
},
]}
>
<FormDatePicker />
<InputNumber />
</Form.Item>
<div>
<Form.Item
label={t("courtesycars.fields.nextservicedate")}
name="nextservicedate"
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<FormDatePicker />
</Form.Item>
<Form.Item
shouldUpdate={(p, c) =>
p.mileage !== c.mileage ||
p.nextservicedate !== c.nextservicedate ||
p.nextservicekm !== c.nextservicekm
}
>
{() => {
const nextservicedate = form.getFieldValue("nextservicedate");
const nextservicekm = form.getFieldValue("nextservicekm");
const mileageOver =
nextservicekm <= form.getFieldValue("mileage");
const dueForService =
nextservicedate && moment(nextservicedate).isBefore(moment());
if (mileageOver || dueForService)
return (
<Space direction="vertical" style={{ color: "tomato" }}>
<span>
<WarningFilled style={{ marginRight: ".3rem" }} />
{t("contracts.labels.cardueforservice")}
</span>
<span>{`${nextservicekm} km`}</span>
<span>
<DateFormatter>{nextservicedate}</DateFormatter>
</span>
</Space>
);
return <></>;
}}
</Form.Item>
</div>
<Form.Item label={t("courtesycars.fields.damage")} name="damage">
<Input.TextArea />
</Form.Item>
@@ -209,7 +286,7 @@ export default function CourtesyCarCreateFormComponent({ form, saveLoading }) {
rules={[
{
required: true,
message: t("general.validation.required"),
//message: t("general.validation.required"),
},
]}
>
@@ -221,7 +298,7 @@ export default function CourtesyCarCreateFormComponent({ form, saveLoading }) {
rules={[
{
required: true,
message: t("general.validation.required"),
//message: t("general.validation.required"),
},
]}
>

View File

@@ -15,7 +15,7 @@ export default function CourtesyCarReturnModalComponent() {
rules={[
{
required: true,
message: t("general.validation.required"),
//message: t("general.validation.required"),
},
]}
>
@@ -27,7 +27,7 @@ export default function CourtesyCarReturnModalComponent() {
rules={[
{
required: true,
message: t("general.validation.required"),
//message: t("general.validation.required"),
},
]}
>
@@ -39,7 +39,7 @@ export default function CourtesyCarReturnModalComponent() {
rules={[
{
required: true,
message: t("general.validation.required"),
//message: t("general.validation.required"),
},
]}
>

View File

@@ -1,13 +1,13 @@
import { useQuery } from "@apollo/client";
import { Form } from "antd";
import { Card, Form, Result } from "antd";
import queryString from "query-string";
import React, { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { useLocation } from "react-router-dom";
import { QUERY_CSI_RESPONSE_BY_PK } from "../../graphql/csi.queries";
import ConfigFormComponents from "../config-form-components/config-form-components.component";
import { useTranslation } from "react-i18next";
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
import AlertComponent from "../alert/alert.component";
import ConfigFormComponents from "../config-form-components/config-form-components.component";
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
export default function CsiResponseFormContainer() {
const { t } = useTranslation();
@@ -25,19 +25,24 @@ export default function CsiResponseFormContainer() {
form.resetFields();
}, [data, form]);
if (!!!responseid) return <div>{t("csi.labels.noneselected")}</div>;
if (!!!responseid)
return (
<Card>
<Result title={t("csi.labels.noneselected")} />
</Card>
);
if (loading) return <LoadingSpinner />;
if (error) return <AlertComponent message={error.message} type="error" />;
return (
<div>
<Card>
<Form form={form} initialValues={data.csi_by_pk.response}>
<ConfigFormComponents
readOnly
componentList={data.csi_by_pk.csiquestion.config}
/>
</Form>
</div>
</Card>
);
}

View File

@@ -1,5 +1,5 @@
import { SyncOutlined } from "@ant-design/icons";
import { Button, Table } from "antd";
import { Button, Card, Table } from "antd";
import queryString from "query-string";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
@@ -46,15 +46,15 @@ export default function CsiResponseListPaginated({
width: "25%",
sortOrder: sortcolumn === "owner" && sortorder,
render: (text, record) => {
return record.owner ? (
<Link to={"/manage/owners/" + record.owner.id}>
return record.job.owner ? (
<Link to={"/manage/owners/" + record.job.owner.id}>
{`${record.job.ownr_fn || ""} ${record.job.ownr_ln || ""} ${
record.job.ownr_co_nm
record.job.ownr_co_nm || ""
}`}
</Link>
) : (
<span>{`${record.job.ownr_fn || ""} ${record.job.ownr_ln || ""} ${
record.job.ownr_co_nm
record.job.ownr_co_nm || ""
}`}</span>
);
},
@@ -96,28 +96,15 @@ export default function CsiResponseListPaginated({
};
return (
<div>
<Card
extra={
<Button onClick={() => refetch()}>
<SyncOutlined />
</Button>
}
>
<Table
loading={loading}
title={() => {
return (
<div style={{ display: "flex" }}>
<Button onClick={() => refetch()}>
<SyncOutlined />
</Button>
{
// <Input.Search
// placeholder={t("general.labels.search")}
// onSearch={(value) => {
// search.search = value;
// history.push({ search: queryString.stringify(search) });
// }}
// enterButton
// />
}
</div>
);
}}
pagination={{
position: "top",
pageSize: 25,
@@ -143,6 +130,6 @@ export default function CsiResponseListPaginated({
};
}}
/>
</div>
</Card>
);
}

View File

@@ -1,6 +1,6 @@
import { UploadOutlined } from "@ant-design/icons";
import { notification, Progress, Result, Space, Upload } from "antd";
import React, { useMemo } from "react";
import React, { useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
@@ -27,6 +27,7 @@ export function DocumentsUploadComponent({
ignoreSizeLimit = false,
}) {
const { t } = useTranslation();
const [fileList, setFileList] = useState([]);
const pct = useMemo(() => {
return parseInt(
@@ -40,12 +41,23 @@ export function DocumentsUploadComponent({
status="error"
title={t("documents.labels.storageexceeded_title")}
subTitle={t("documents.labels.storageexceeded")}
></Result>
/>
);
const handleDone = (uid) => {
setTimeout(() => {
setFileList((fileList) => fileList.filter((x) => x.uid !== uid));
}, 2000);
};
return (
<Upload.Dragger
multiple={true}
fileList={fileList}
onChange={(f) => {
if (f.event && f.event.percent === 100) handleDone(f.file.uid);
setFileList(f.fileList);
}}
beforeUpload={(file, fileList) => {
if (ignoreSizeLimit) return true;
const newFiles = fileList.reduce((acc, val) => acc + val.size, 0);

View File

@@ -134,7 +134,7 @@ export const uploadToCloudinary = async (
type: fileType,
extension: extension,
bodyshopid: bodyshop.id,
size: file.size || cloudinaryUploadResponse.data.bytes,
size: cloudinaryUploadResponse.data.bytes || file.size,
},
],
},
@@ -147,7 +147,9 @@ export const uploadToCloudinary = async (
status: "done",
key: documentInsert.data.insert_documents.returning[0].key,
});
notification["success"]({
notification.open({
type: "success",
key: "docuploadsuccess",
message: i18n.t("documents.successes.insert"),
});
if (callback) {

View File

@@ -1,68 +1,84 @@
import { UploadOutlined } from "@ant-design/icons";
import { Button, Card, Divider, Input, Select, Upload } from "antd";
import { Card, Divider, Form, Input, Select, Upload } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
export default function EmailOverlayComponent({
messageOptions,
handleConfigChange,
handleToChange,
handleHtmlChange,
handleUpload,
handleFileRemove,
}) {
export default function EmailOverlayComponent({ form }) {
const { t } = useTranslation();
return (
<div>
To:
<Select
<Form.Item
label={t("emails.fields.to")}
name="to"
mode="tags"
value={messageOptions.to}
//style={{ width: "100%" }}
onChange={handleToChange}
tokenSeparators={[",", ";"]}
/>
CC:
<Select
value={messageOptions.cc}
mode="tags"
onChange={(value) => handleConfigChange("cc", value)}
name="cc"
tokenSeparators={[",", ";"]}
/>
Subject:
<Input
value={messageOptions.subject}
onChange={(e) => handleConfigChange("subject", e.target.value)}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<Select mode="tags" tokenSeparators={[",", ";"]} />
</Form.Item>
<Form.Item label={t("emails.fields.cc")} name="cc">
<Select mode="tags" tokenSeparators={[",", ";"]} />
</Form.Item>
<Form.Item
label={t("emails.fields.subject")}
name="subject"
/>
<Divider>{t("emails.labels.preview")}</Divider>
<div
style={{
padding: "1rem",
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<Input />
</Form.Item>
backgroundColor: "lightgray",
borderLeft: "6px solid #2196F3",
<Divider>{t("emails.labels.preview")}</Divider>
<Form.Item shouldUpdate>
{() => {
return (
<div
style={{
padding: "1rem",
backgroundColor: "lightgray",
borderLeft: "6px solid #2196F3",
}}
dangerouslySetInnerHTML={{ __html: form.getFieldValue("html") }}
/>
);
}}
dangerouslySetInnerHTML={{ __html: messageOptions.html }}
/>
<Divider>
<Divider>{t("emails.labels.preview")}</Divider>
</Divider>
</Form.Item>
<Card title={t("emails.labels.attachments")}>
<Upload
fileList={messageOptions.fileList}
beforeUpload={handleUpload}
onRemove={handleFileRemove}
multiple
listType="picture-card"
style={{ width: "100%" }}
<Form.Item
name="fileList"
valuePropName="fileList"
getValueFromEvent={(e) => {
console.log("Upload event:", e);
if (Array.isArray(e)) {
return e;
}
return e && e.fileList;
}}
>
<Button>
<UploadOutlined /> Upload
</Button>
</Upload>
<Upload.Dragger
beforeUpload={Upload.LIST_IGNORE}
multiple
listType="picture-card"
>
<>
<p className="ant-upload-drag-icon">
<UploadOutlined />
</p>
<p className="ant-upload-text">
Click or drag files to this area to upload.
</p>
</>
</Upload.Dragger>
</Form.Item>
</Card>
</div>
);

View File

@@ -1,4 +1,4 @@
import { Modal, notification } from "antd";
import { Divider, Form, Modal, notification } from "antd";
import axios from "axios";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
@@ -10,9 +10,13 @@ import {
selectEmailConfig,
selectEmailVisible,
} from "../../redux/email/email.selectors.js";
import { selectBodyshop } from "../../redux/user/user.selectors";
import {
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
import RenderTemplate from "../../utils/RenderTemplate";
import { EmailSettings } from "../../utils/TemplateConstants";
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
import EmailOverlayComponent from "./email-overlay.component";
@@ -20,6 +24,7 @@ const mapStateToProps = createStructuredSelector({
modalVisible: selectEmailVisible,
emailConfig: selectEmailConfig,
bodyshop: selectBodyshop,
currentUser: selectCurrentUser,
});
const mapDispatchToProps = (dispatch) => ({
@@ -31,35 +36,34 @@ export function EmailOverlayContainer({
modalVisible,
toggleEmailOverlayVisible,
bodyshop,
currentUser,
}) {
const { t } = useTranslation();
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const [sending, setSending] = useState(false);
const [rawHtml, setRawHtml] = useState("");
const defaultEmailFrom = {
from: {
name: bodyshop.shopname || EmailSettings.fromNameDefault,
name: `${currentUser.displayName} @ ${bodyshop.shopname}`,
address: EmailSettings.fromAddress,
},
replyTo: bodyshop.email,
ReplyTo: {
Email: currentUser.validemail ? currentUser.email : bodyshop.email,
Name: currentUser.displayName,
},
};
const [messageOptions, setMessageOptions] = useState({
...defaultEmailFrom,
html: "",
fileList: [],
});
const handleOk = async () => {
const handleFinish = async (values) => {
logImEXEvent("email_send_from_modal");
console.log(`values`, values);
const attachments = [];
await asyncForEach(messageOptions.fileList, async (f) => {
await asyncForEach(values.fileList, async (f) => {
const t = {
ContentType: f.type,
Filename: f.name,
Base64Content: (await toBase64(f)).split(",")[1],
Base64Content: (await toBase64(f.originFileObj)).split(",")[1],
};
attachments.push(t);
});
@@ -67,9 +71,13 @@ export function EmailOverlayContainer({
setSending(true);
try {
await axios.post("/sendemail", {
...messageOptions,
...defaultEmailFrom,
...values,
html: rawHtml,
attachments,
attachments: await Promise.all(
values.fileList.map(async (f) => await toBase64(f.originFileObj))
),
//attachments,
});
notification["success"]({ message: t("emails.successes.sent") });
toggleEmailOverlayVisible();
@@ -82,34 +90,6 @@ export function EmailOverlayContainer({
setSending(false);
};
const handleConfigChange = (name, value) => {
setMessageOptions({ ...messageOptions, [name]: value });
};
const handleHtmlChange = (text) => {
setMessageOptions({ ...messageOptions, html: text });
};
const handleToChange = (recipients) => {
setMessageOptions({ ...messageOptions, to: recipients });
};
const handleUpload = (file) => {
setMessageOptions({
...messageOptions,
fileList: [...messageOptions.fileList, file],
});
return false;
};
const handleFileRemove = (file) => {
setMessageOptions((state) => {
const index = state.fileList.indexOf(file);
const newfileList = state.fileList.slice();
newfileList.splice(index, 1);
return {
fileList: newfileList,
};
});
};
const render = async () => {
logImEXEvent("email_render_template", { template: emailConfig.template });
setLoading(true);
@@ -120,11 +100,14 @@ export function EmailOverlayContainer({
url: `${window.location.protocol}://${window.location.host}/`,
});
setRawHtml(response.data);
console.log("response", response);
setMessageOptions({
form.setFieldsValue({
...emailConfig.messageOptions,
...defaultEmailFrom,
cc:
emailConfig.messageOptions.cc &&
emailConfig.messageOptions.cc.filter((x) => x),
to:
emailConfig.messageOptions.to &&
emailConfig.messageOptions.to.filter((x) => x),
html: response.data,
fileList: [],
});
@@ -140,29 +123,22 @@ export function EmailOverlayContainer({
destroyOnClose={true}
visible={modalVisible}
width={"80%"}
onOk={handleOk}
onOk={() => form.submit()}
onCancel={() => {
toggleEmailOverlayVisible();
}}
okButtonProps={{ loading: sending }}
>
<LoadingSpinner loading={loading}>
<EmailOverlayComponent
handleConfigChange={handleConfigChange}
messageOptions={messageOptions}
handleHtmlChange={handleHtmlChange}
handleUpload={handleUpload}
handleFileRemove={handleFileRemove}
handleToChange={handleToChange}
/>
<button
onClick={() => {
navigator.clipboard.writeText(messageOptions.html);
}}
>
Copy HTML
</button>
</LoadingSpinner>
<Form layout="vertical" form={form} onFinish={handleFinish}>
{loading && (
<div>
<LoadingSkeleton />
<Divider>{t("emails.labels.preview")}</Divider>
<LoadingSpinner message={t("emails.labels.generatingemail")} />
</div>
)}
{!loading && <EmailOverlayComponent form={form} />}
</Form>
</Modal>
);
}

View File

@@ -1,6 +1,5 @@
import { Button, Form, Input, Select, Switch } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { setEmailOptions } from "../../redux/email/email.actions";
@@ -18,7 +17,6 @@ const mapDispatchToProps = (dispatch) => ({
export function EmailTestComponent({ currentUser, setEmailOptions }) {
const [form] = Form.useForm();
const { t } = useTranslation();
const handleFinish = (values) => {
console.log("values", values);
@@ -71,7 +69,7 @@ export function EmailTestComponent({ currentUser, setEmailOptions }) {
rules={[
{
required: true,
message: t("general.validation.required"),
//message: t("general.validation.required"),
},
]}
>

View File

@@ -1,33 +1,21 @@
import { Select, Space, Tag } from "antd";
import React, { forwardRef, useEffect, useState } from "react";
import React, { forwardRef } from "react";
import { useTranslation } from "react-i18next";
const { Option } = Select;
//To be used as a form element only.
const EmployeeSearchSelect = (
{ value, onChange, options, onSelect, onBlur, ...restProps },
ref
) => {
const [option, setOption] = useState(value);
const EmployeeSearchSelect = ({ options, ...props }, ref) => {
const { t } = useTranslation();
useEffect(() => {
if (value !== option && onChange) {
onChange(option);
}
}, [value, option, onChange]);
return (
<Select
showSearch
value={option}
// value={option}
style={{
width: 400,
}}
onChange={setOption}
optionFilterProp="search"
onSelect={onSelect}
onBlur={onBlur}
{...restProps}
{...props}
>
{options
? options.map((o) => (

View File

@@ -5,7 +5,10 @@ import React, { forwardRef } from "react";
const dateFormat = "MM/DD/YYYY";
const FormDatePicker = ({ value, onChange, onBlur, ...restProps }, ref) => {
const FormDatePicker = (
{ value, onChange, onBlur, onlyFuture, ...restProps },
ref
) => {
const handleChange = (newDate) => {
if (value !== newDate && onChange) {
onChange(newDate);
@@ -27,6 +30,10 @@ const FormDatePicker = ({ value, onChange, onBlur, ...restProps }, ref) => {
onChange={handleChange}
format={dateFormat}
onBlur={onBlur}
disabledTime
{...(onlyFuture && {
disabledDate: (d) => moment().subtract(1, "day").isAfter(d),
})}
{...restProps}
/>
</div>

View File

@@ -6,7 +6,10 @@ import { TimePicker } from "antd";
import moment from "moment";
//To be used as a form element only.
const DateTimePicker = ({ value, onChange, onBlur, id, ...restProps }, ref) => {
const DateTimePicker = (
{ value, onChange, onBlur, id, onlyFuture, ...restProps },
ref
) => {
// const handleChange = (newDate) => {
// if (value !== newDate && onChange) {
// onChange(newDate);
@@ -17,6 +20,9 @@ const DateTimePicker = ({ value, onChange, onBlur, id, ...restProps }, ref) => {
<div id={id}>
<FormDatePicker
{...restProps}
{...(onlyFuture && {
disabledDate: (d) => moment().subtract(1, "day").isAfter(d),
})}
value={value}
onBlur={onBlur}
onChange={onChange}
@@ -25,6 +31,9 @@ const DateTimePicker = ({ value, onChange, onBlur, id, ...restProps }, ref) => {
<TimePicker
{...restProps}
value={value ? moment(value) : null}
{...(onlyFuture && {
disabledDate: (d) => moment().isAfter(d),
})}
onChange={onChange}
showSecond={false}
minuteStep={15}

View File

@@ -1,5 +1,5 @@
import React from "react";
import { Form } from "antd";
import { Form, Space } from "antd";
import { useTranslation } from "react-i18next";
import AlertComponent from "../alert/alert.component";
import { Prompt, useLocation } from "react-router-dom";
@@ -20,9 +20,10 @@ export default function FormsFieldChanged({ form }) {
style={{ margin: 0, padding: 0, minHeight: "unset" }}
>
{() => {
const errors = form.getFieldsError().filter((e) => e.errors.length > 0);
if (form.isFieldsTouched())
return (
<span>
<Space direction="vertical" style={{ width: "100%" }}>
<Prompt
when={true}
message={(location) => {
@@ -47,7 +48,23 @@ export default function FormsFieldChanged({ form }) {
</div>
}
/>
</span>
{errors.length > 0 && (
<AlertComponent
type="error"
message={
<div>
<ul>
{errors.map((e, idx) =>
e.errors.map((e2, idx2) => (
<li key={`${idx}${idx2}`}>{e2}</li>
))
)}
</ul>
</div>
}
/>
)}
</Space>
);
return <div style={{ display: "none" }}></div>;
}}

View File

@@ -1,14 +1,14 @@
import React, { forwardRef } from "react";
import { BlockPicker } from "react-color";
import React from "react";
import { SliderPicker } from "react-color";
//To be used as a form element only.
const ColorPickerFormItem = ({ value, onChange, style, ...restProps }, ref) => {
const ColorPickerFormItem = ({ value, onChange, style, ...restProps }) => {
const handleChangeComplete = (color) => {
if (onChange) onChange(color);
};
return (
<BlockPicker
<SliderPicker
{...restProps}
style={{ width: "100%", ...style }}
color={value}
@@ -17,4 +17,4 @@ const ColorPickerFormItem = ({ value, onChange, style, ...restProps }, ref) => {
/>
);
};
export default forwardRef(ColorPickerFormItem);
export default ColorPickerFormItem;

View File

@@ -137,6 +137,28 @@ export default function GlobalSearch() {
};
}),
},
{
label: renderTitle(t("menus.header.search.phonebook")),
options: data.search_phonebook.map((pb) => {
return {
key: pb.id,
value: `${pb.firstname || ""} ${pb.lastname || ""} ${
pb.company || ""
}`,
label: (
<Link to={`/manage/phonebook?phonebookentry=${pb.id}`}>
<Space wrap split={<Divider type="vertical" />}>
<span>{`${pb.firstname || ""} ${pb.lastname || ""} ${
pb.company || ""
}`}</span>
<PhoneNumberFormatter>{pb.phone1}</PhoneNumberFormatter>
<span>{pb.email}</span>
</Space>
</Link>
),
};
}),
},
]
: [];
@@ -144,12 +166,12 @@ export default function GlobalSearch() {
return (
<AutoComplete
dropdownMatchSelectWidth={false}
dropdownMatchSelectWidth={"false"}
options={options}
onSearch={handleSearch}
allowClear
>
<Input.Search loading={loading} style={{ width: "20vw" }} />
<Input.Search loading={loading} />
</AutoComplete>
);
}

View File

@@ -1,14 +1,21 @@
import Icon, {
BankFilled,
BarChartOutlined,
CarFilled,
ClockCircleFilled,
DollarCircleFilled,
ExportOutlined,
FieldTimeOutlined,
FileAddFilled,
FileFilled,
GlobalOutlined,
HomeFilled,
ImportOutlined,
LineChartOutlined,
PaperClipOutlined,
PhoneOutlined,
ScheduleOutlined,
SettingOutlined,
TeamOutlined,
ToolFilled,
UnorderedListOutlined,
@@ -24,6 +31,9 @@ import {
FaCreditCard,
FaFileInvoiceDollar,
} from "react-icons/fa";
import { GiPayMoney, GiPlayerTime, GiSettingsKnobs } from "react-icons/gi";
import { IoBusinessOutline } from "react-icons/io5";
import { RiSurveyLine } from "react-icons/ri";
import { connect } from "react-redux";
import { Link } from "react-router-dom";
import { createStructuredSelector } from "reselect";
@@ -35,6 +45,7 @@ import { setModalContext } from "../../redux/modals/modals.actions";
import { signOutStart } from "../../redux/user/user.actions";
import { selectCurrentUser } from "../../redux/user/user.selectors";
import GlobalSearch from "../global-search/global-search.component";
const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser,
recentItems: selectRecentItems,
@@ -67,137 +78,103 @@ function Header({
const { t } = useTranslation();
return (
<Layout.Header>
<Layout.Header style={{ display: "flex", alignItems: "center" }}>
<Menu
mode="horizontal"
//theme="light"
theme={"dark"}
style={{ flex: 5 }}
style={{ flex: 1 }}
selectedKeys={[selectedHeader]}
onClick={handleMenuClick}
subMenuCloseDelay={0.3}
>
<Menu.Item key="home">
<Link to="/manage">
<HomeFilled />
{t("menus.header.home")}
</Link>
<Menu.Item key="home" icon={<HomeFilled />}>
<Link to="/manage">{t("menus.header.home")}</Link>
</Menu.Item>
<Menu.Item key="schedule">
<Link to="/manage/schedule">
<Icon component={FaCalendarAlt} />
{t("menus.header.schedule")}
</Link>
<Menu.Item key="schedule" icon={<Icon component={FaCalendarAlt} />}>
<Link to="/manage/schedule">{t("menus.header.schedule")}</Link>
</Menu.Item>
<Menu.SubMenu
title={
<span>
<Icon component={FaCarCrash} />
<span>{t("menus.header.jobs")}</span>
</span>
}
icon={<Icon component={FaCarCrash} />}
title={t("menus.header.jobs")}
>
<Menu.Item key="activejobs">
<FileFilled />
<Menu.Item key="activejobs" icon={<FileFilled />}>
<Link to="/manage/jobs">{t("menus.header.activejobs")}</Link>
</Menu.Item>
<Menu.Item key="parts-queue">
<Link to="/manage/partsqueue">
<ToolFilled /> {t("menus.header.parts-queue")}
</Link>
<Menu.Item key="parts-queue" icon={<ToolFilled />}>
<Link to="/manage/partsqueue">{t("menus.header.parts-queue")}</Link>
</Menu.Item>
<Menu.Item key="availablejobs">
<Menu.Item key="availablejobs" icon={<ImportOutlined />}>
<Link to="/manage/available">
<ImportOutlined /> {t("menus.header.availablejobs")}
{t("menus.header.availablejobs")}
</Link>
</Menu.Item>
<Menu.Divider />
<Menu.Item key="alljobs">
<UnorderedListOutlined />
<Menu.Item key="alljobs" icon={<UnorderedListOutlined />}>
<Link to="/manage/jobs/all">{t("menus.header.alljobs")}</Link>
</Menu.Item>
<Menu.Divider />
<Menu.Item key="productionlist">
<Menu.Item key="productionlist" icon={<ScheduleOutlined />}>
<Link to="/manage/production/list">
<ScheduleOutlined />
{t("menus.header.productionlist")}
</Link>
</Menu.Item>
<Menu.Item key="productionboard">
<Menu.Item key="productionboard" icon={<Icon component={BsKanban} />}>
<Link to="/manage/production/board">
<Icon component={BsKanban} />
{t("menus.header.productionboard")}
</Link>
</Menu.Item>
<Menu.Divider />
<Menu.Item key="scoreboard">
<LineChartOutlined />
<Menu.Item key="scoreboard" icon={<LineChartOutlined />}>
<Link to="/manage/scoreboard">{t("menus.header.scoreboard")}</Link>
</Menu.Item>
</Menu.SubMenu>
<Menu.SubMenu
title={
<span>
<UserOutlined />
<span>{t("menus.header.customers")}</span>
</span>
}
icon={<UserOutlined />}
title={t("menus.header.customers")}
>
<Menu.Item key="owners">
<Link to="/manage/owners">
<TeamOutlined />
{t("menus.header.owners")}
</Link>
<Menu.Item key="owners" icon={<TeamOutlined />}>
<Link to="/manage/owners">{t("menus.header.owners")}</Link>
</Menu.Item>
<Menu.Item key="vehicles">
<Link to="/manage/vehicles">
<CarFilled />
{t("menus.header.vehicles")}
</Link>
<Menu.Item key="vehicles" icon={<CarFilled />}>
<Link to="/manage/vehicles">{t("menus.header.vehicles")}</Link>
</Menu.Item>
</Menu.SubMenu>
<Menu.SubMenu
title={
<span>
<CarFilled />
<span>{t("menus.header.courtesycars")}</span>
</span>
}
icon={<CarFilled />}
title={t("menus.header.courtesycars")}
>
<Menu.Item key="courtesycarsall">
<Menu.Item key="courtesycarsall" icon={<CarFilled />}>
<Link to="/manage/courtesycars">
<CarFilled />
{t("menus.header.courtesycars-all")}
</Link>
</Menu.Item>
<Menu.Item key="contracts">
<Menu.Item key="contracts" icon={<FileFilled />}>
<Link to="/manage/courtesycars/contracts">
<FileFilled />
{t("menus.header.courtesycars-contracts")}
</Link>
</Menu.Item>
<Menu.Item key="newcontract">
<Menu.Item key="newcontract" icon={<FileAddFilled />}>
<Link to="/manage/courtesycars/contracts/new">
<FileAddFilled />
{t("menus.header.courtesycars-newcontract")}
</Link>
</Menu.Item>
</Menu.SubMenu>
<Menu.SubMenu
title={
<span>
<DollarCircleFilled />
<span>{t("menus.header.accounting")}</span>
</span>
}
icon={<DollarCircleFilled />}
title={t("menus.header.accounting")}
>
<Menu.Item key="bills">
<Menu.Item
key="bills"
icon={<Icon component={FaFileInvoiceDollar} />}
>
<Link to="/manage/bills">{t("menus.header.bills")}</Link>
</Menu.Item>
<Menu.Item
key="enterbills"
icon={<Icon component={GiPayMoney} />}
onClick={() => {
setBillEnterContext({
actions: {},
@@ -205,11 +182,10 @@ function Header({
});
}}
>
<Icon component={FaFileInvoiceDollar} />
{t("menus.header.enterbills")}
</Menu.Item>
<Menu.Divider />
<Menu.Item key="allpayments">
<Menu.Item key="allpayments" icon={<BankFilled />}>
<Link to="/manage/payments">{t("menus.header.allpayments")}</Link>
</Menu.Item>
<Menu.Item
@@ -226,13 +202,14 @@ function Header({
</Menu.Item>
<Menu.Divider />
<Menu.Item key="timetickets">
<Menu.Item key="timetickets" icon={<FieldTimeOutlined />}>
<Link to="/manage/timetickets">
{t("menus.header.timetickets")}
</Link>
</Menu.Item>
<Menu.Item
key="entertimetickets"
icon={<Icon component={GiPlayerTime} />}
onClick={() => {
setTimeTicketContext({
actions: {},
@@ -244,7 +221,10 @@ function Header({
</Menu.Item>
<Menu.Divider />
<Menu.SubMenu title={t("menus.header.export")}>
<Menu.SubMenu
title={t("menus.header.export")}
icon={<ExportOutlined />}
>
<Menu.Item key="receivables">
<Link to="/manage/accounting/receivables">
{t("menus.header.accounting-receivables")}
@@ -260,26 +240,29 @@ function Header({
{t("menus.header.accounting-payments")}
</Link>
</Menu.Item>
<Menu.Item key="export-logs">
<Link to="/manage/accounting/exportlogs">
{t("menus.header.export-logs")}
</Link>
</Menu.Item>
</Menu.SubMenu>
</Menu.SubMenu>
<Menu.SubMenu title={t("menus.header.shop")}>
<Menu.Item key="shop">
<Menu.Item key="phonebook" icon={<PhoneOutlined />}>
<Link to="/manage/phonebook">{t("menus.header.phonebook")}</Link>
</Menu.Item>
<Menu.Item key="temporarydocs" icon={<PaperClipOutlined />}>
<Link to="/manage/temporarydocs">
{t("menus.header.temporarydocs")}
</Link>
</Menu.Item>
<Menu.SubMenu title={t("menus.header.shop")} icon={<SettingOutlined />}>
<Menu.Item key="shop" icon={<Icon component={GiSettingsKnobs} />}>
<Link to="/manage/shop">{t("menus.header.shop_config")}</Link>
</Menu.Item>
<Menu.Item key="temporarydocs">
<Link to="/manage/temporarydocs">
{t("menus.header.temporarydocs")}
</Link>
</Menu.Item>
{
// <Menu.Item key="shop-templates">
// <Link to="/manage/shop/templates">
// {t("menus.header.shop_templates")}
// </Link>
// </Menu.Item>
}
<Menu.Item
key="reportcenter"
icon={<BarChartOutlined />}
onClick={() => {
setReportCenterContext({
actions: {},
@@ -289,12 +272,15 @@ function Header({
>
{t("menus.header.reportcenter")}
</Menu.Item>
<Menu.Item key="shop-vendors">
<Menu.Item
key="shop-vendors"
icon={<Icon component={IoBusinessOutline} />}
>
<Link to="/manage/shop/vendors">
{t("menus.header.shop_vendors")}
</Link>
</Menu.Item>
<Menu.Item key="shop-csi">
<Menu.Item key="shop-csi" icon={<Icon component={RiSurveyLine} />}>
<Link to="/manage/shop/csi">{t("menus.header.shop_csi")}</Link>
</Menu.Item>
</Menu.SubMenu>
@@ -348,10 +334,10 @@ function Header({
</Menu.Item>
))}
</Menu.SubMenu>
<Menu.Item style={{ float: "right" }}>
<GlobalSearch />
</Menu.Item>
</Menu>
<div>
<GlobalSearch />
</div>
</Layout.Header>
);
}

View File

@@ -24,7 +24,10 @@ export function Jobd3RdPartyModal({ bodyshop, jobId }) {
const { t } = useTranslation();
const [form] = Form.useForm();
const { data: VendorAutoCompleteData } = useQuery(
SEARCH_VENDOR_AUTOCOMPLETE_WITH_ADDR
SEARCH_VENDOR_AUTOCOMPLETE_WITH_ADDR,
{
skip: !isModalVisible,
}
);
const showModal = () => {
@@ -198,7 +201,7 @@ export function Jobd3RdPartyModal({ bodyshop, jobId }) {
rules={[
{
required: true,
message: t("general.validation.required"),
//message: t("general.validation.required"),
},
]}
>

View File

@@ -15,34 +15,36 @@ const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export function ScheduleAtChange({ bodyshop, event }) {
export function JobAltTransportChange({ bodyshop, job }) {
const [updateJob] = useMutation(UPDATE_JOB);
const { t } = useTranslation();
const onClick = async ({ key }) => {
const result = await updateJob({
variables: { jobId: event.job.id, job: { alt_transport: key } },
variables: {
jobId: job.id,
job: { alt_transport: key === "null" ? null : key },
},
});
if (!!!result.errors) {
notification["success"]({ message: t("appointments.successes.saved") });
// notification["success"]({ message: t("appointments.successes.saved") });
} else {
notification["error"]({
message: t("appointments.errors.saving", {
message: t("jobs.errors.saving", {
error: JSON.stringify(result.errors),
}),
});
}
};
const menu = (
<Menu
selectedKeys={[event.job && event.job.alt_transport]}
onClick={onClick}
>
<Menu selectedKeys={[job && job.alt_transport]} onClick={onClick}>
{bodyshop.appt_alt_transport &&
bodyshop.appt_alt_transport.map((alt) => (
<Menu.Item key={alt}>{alt}</Menu.Item>
))}
<Menu.Divider />
<Menu.Item key={"null"}>{t("general.actions.clear")}</Menu.Item>
</Menu>
);
return (
@@ -53,4 +55,7 @@ export function ScheduleAtChange({ bodyshop, event }) {
</Dropdown>
);
}
export default connect(mapStateToProps, mapDispatchToProps)(ScheduleAtChange);
export default connect(
mapStateToProps,
mapDispatchToProps
)(JobAltTransportChange);

View File

@@ -21,7 +21,10 @@ export function ScheduleEventColor({ bodyshop, event }) {
const onClick = async ({ key }) => {
const result = await updateAppointment({
variables: { appid: event.id, app: { color: key } },
variables: {
appid: event.id,
app: { color: key === "null" ? null : key },
},
});
if (!!!result.errors) {
@@ -34,6 +37,13 @@ export function ScheduleEventColor({ bodyshop, event }) {
});
}
};
const selectedColor =
event.color &&
bodyshop.appt_colors &&
bodyshop.appt_colors.filter((color) => color.color.hex === event.color)[0]
?.label;
const menu = (
<Menu selectedKeys={[event.color]} onClick={onClick}>
{bodyshop.appt_colors &&
@@ -42,11 +52,15 @@ export function ScheduleEventColor({ bodyshop, event }) {
{color.label}
</Menu.Item>
))}
<Menu.Divider />
<Menu.Item key={"null"}>{t("general.actions.clear")}</Menu.Item>
</Menu>
);
console.log(`event`, event);
return (
<Dropdown overlay={menu}>
<a href=" #" onClick={(e) => e.preventDefault()}>
{selectedColor}
<DownOutlined />
</a>
</Dropdown>

View File

@@ -1,5 +1,5 @@
import { Button, Popover, Space } from "antd";
import React from "react";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { Link } from "react-router-dom";
@@ -7,10 +7,10 @@ import { setModalContext } from "../../redux/modals/modals.actions";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import PhoneFormatter from "../../utils/PhoneFormatter";
import { GenerateDocument } from "../../utils/RenderTemplate";
import DataLabel from "../data-label/data-label.component";
import ScheduleAtChange from "./schedule-event.at.component";
import ScheduleEventColor from "./schedule-event.color.component";
import { TemplateList } from "../../utils/TemplateConstants";
import DataLabel from "../data-label/data-label.component";
import ScheduleAtChange from "./job-at-change.component";
import ScheduleEventColor from "./schedule-event.color.component";
const mapDispatchToProps = (dispatch) => ({
setScheduleContext: (context) =>
dispatch(setModalContext({ context: context, modal: "schedule" })),
@@ -23,6 +23,16 @@ export function ScheduleEventComponent({
setScheduleContext,
}) {
const { t } = useTranslation();
const [visible, setVisible] = useState(false);
const blockContent = (
<div>
<Button onClick={() => handleCancel(event.id)} disabled={event.arrived}>
{t("appointments.actions.cancel")}
</Button>
</div>
);
const popoverContent = (
<div>
{!event.isintake ? (
@@ -67,12 +77,12 @@ export function ScheduleEventComponent({
</DataLabel>
<DataLabel label={t("jobs.fields.alt_transport")}>
{(event.job && event.job.alt_transport) || ""}
<ScheduleAtChange event={event} />
<ScheduleAtChange job={event && event.job} />
</DataLabel>
</div>
) : null}
<div className="imex-flex-row">
<Space wrap>
{event.job ? (
<Link to={`/manage/jobs/${event.job && event.job.id}`}>
<Button>{t("appointments.actions.viewjob")}</Button>
@@ -100,6 +110,7 @@ export function ScheduleEventComponent({
<Button
disabled={event.arrived}
onClick={() => {
setVisible(false);
setScheduleContext({
actions: { refetch: refetch },
context: {
@@ -124,7 +135,7 @@ export function ScheduleEventComponent({
</Button>
</Link>
) : null}
</div>
</Space>
</div>
);
@@ -162,8 +173,10 @@ export function ScheduleEventComponent({
return (
<Popover
visible={visible}
onVisibleChange={(vis) => setVisible(vis)}
trigger="click"
content={popoverContent}
content={event.block ? blockContent : popoverContent}
style={{ height: "100%", width: "100%" }}
>
{RegularEvent}

View File

@@ -30,26 +30,27 @@ export default function ScheduleEventContainer({ bodyshop, event, refetch }) {
return;
}
const jobUpdate = await updateJob({
variables: {
jobId: event.job.id,
if (event.job) {
const jobUpdate = await updateJob({
variables: {
jobId: event.job.id,
job: {
date_scheduled: null,
scheduled_in: null,
status: bodyshop.md_ro_statuses.default_imported,
job: {
date_scheduled: null,
scheduled_in: null,
status: bodyshop.md_ro_statuses.default_imported,
},
},
},
});
if (!!jobUpdate.errors) {
notification["error"]({
message: t("jobs.errors.updating", {
message: JSON.stringify(jobUpdate.errors),
}),
});
return;
if (!!jobUpdate.errors) {
notification["error"]({
message: t("jobs.errors.updating", {
message: JSON.stringify(jobUpdate.errors),
}),
});
return;
}
}
if (refetch) refetch();
};

View File

@@ -1,4 +1,4 @@
import { Card, Space, Statistic } from "antd";
import { Card, Col, Row, Space, Statistic, Tooltip, Typography } from "antd";
import Dinero from "dinero.js";
import React from "react";
import { useTranslation } from "react-i18next";
@@ -50,7 +50,7 @@ export default function JobBillsTotalComponent({
} else {
billCms = billCms.add(
Dinero({
amount: Math.round((il.actual_price || 0) * -100),
amount: Math.round((il.actual_price || 0) * 100),
}).multiply(il.quantity)
);
}
@@ -73,64 +73,171 @@ export default function JobBillsTotalComponent({
const discrepWithLbrAdj = discrepancy.add(lbrAdjustments);
const discrepWithCms = discrepWithLbrAdj.subtract(billCms);
const creditsNotReceived = totalReturns.add(billCms); //billCms is tracked as a negative number.
const discrepWithCms = discrepWithLbrAdj.add(billCms);
const creditsNotReceived = totalReturns.subtract(billCms); //billCms is tracked as a negative number.
return (
<Card title={t("jobs.labels.jobtotals")}>
<Space wrap size="large">
<Statistic
title={t("jobs.labels.rosaletotal")}
value={totalPartsSublet.toFormat()}
/>
<Statistic
title={t("bills.labels.retailtotal")}
value={billTotals.toFormat()}
/>
<Statistic
title={t("bills.labels.discrepancy")}
valueStyle={{
color: discrepancy.getAmount() === 0 ? "green" : "red",
}}
value={discrepancy.toFormat()}
/>
<Statistic
title={t("bills.labels.dedfromlbr")}
value={lbrAdjustments.toFormat()}
/>
<Statistic
title={t("bills.labels.discrepwithlbradj")}
valueStyle={{
color: discrepWithLbrAdj.getAmount() === 0 ? "green" : "red",
}}
value={discrepWithLbrAdj.toFormat()}
/>
<Statistic
title={t("bills.labels.billcmtotal")}
value={billCms.toFormat()}
/>
<Statistic
title={t("bills.labels.discrepwithcms")}
valueStyle={{
color: discrepWithCms.getAmount() === 0 ? "green" : "red",
}}
value={discrepWithCms.toFormat()}
/>
<Statistic
title={t("bills.labels.totalreturns")}
value={totalReturns.toFormat()}
/>
<Statistic
title={t("bills.labels.creditsreceived")}
value={billCms.toFormat()}
/>
<Statistic
title={t("bills.labels.creditsnotreceived")}
valueStyle={{
color: creditsNotReceived.getAmount() === 0 ? "green" : "red",
}}
value={creditsNotReceived.toFormat()}
/>
</Space>
</Card>
<Row gutter={16}>
<Col span={18}>
<Card title={t("jobs.labels.jobtotals")} style={{ height: "100%" }}>
<Space wrap size="large">
<Tooltip
title={
<div
dangerouslySetInnerHTML={{
__html: t("jobs.labels.plitooltips.partstotal"),
}}
/>
}
>
<Statistic
title={t("jobs.labels.rosaletotal")}
value={totalPartsSublet.toFormat()}
/>
</Tooltip>
<Typography.Title>-</Typography.Title>
<Tooltip
title={
<div
dangerouslySetInnerHTML={{
__html: t("jobs.labels.plitooltips.billtotal"),
}}
/>
}
>
<Statistic
title={t("bills.labels.retailtotal")}
value={billTotals.toFormat()}
/>
</Tooltip>
<Typography.Title>=</Typography.Title>
<Tooltip
title={
<div
dangerouslySetInnerHTML={{
__html: t("jobs.labels.plitooltips.discrep1"),
}}
/>
}
>
<Statistic
title={t("bills.labels.discrepancy")}
valueStyle={{
color: discrepancy.getAmount() === 0 ? "green" : "red",
}}
value={discrepancy.toFormat()}
/>
</Tooltip>
<Typography.Title>+</Typography.Title>
<Tooltip
title={
<div
dangerouslySetInnerHTML={{
__html: t("jobs.labels.plitooltips.laboradj"),
}}
/>
}
>
<Statistic
title={t("bills.labels.dedfromlbr")}
value={lbrAdjustments.toFormat()}
/>
</Tooltip>
<Typography.Title>=</Typography.Title>
<Tooltip
title={
<div
dangerouslySetInnerHTML={{
__html: t("jobs.labels.plitooltips.discrep2"),
}}
/>
}
>
<Statistic
title={t("bills.labels.discrepancy")}
valueStyle={{
color: discrepWithLbrAdj.getAmount() === 0 ? "green" : "red",
}}
value={discrepWithLbrAdj.toFormat()}
/>
</Tooltip>
<Typography.Title>+</Typography.Title>
<Tooltip
title={
<div
dangerouslySetInnerHTML={{
__html: t("jobs.labels.plitooltips.creditmemos"),
}}
/>
}
>
<Statistic
title={t("bills.labels.billcmtotal")}
value={billCms.toFormat()}
/>
</Tooltip>
<Typography.Title>=</Typography.Title>
<Tooltip
title={
<div
dangerouslySetInnerHTML={{
__html: t("jobs.labels.plitooltips.discrep3"),
}}
/>
}
>
<Statistic
title={t("bills.labels.discrepancy")}
valueStyle={{
color: discrepWithCms.getAmount() === 0 ? "green" : "red",
}}
value={discrepWithCms.toFormat()}
/>
</Tooltip>
</Space>
</Card>
</Col>
<Col span={6}>
<Card title={t("jobs.labels.returntotals")} style={{ height: "100%" }}>
<Space wrap>
<Tooltip
title={
<div
dangerouslySetInnerHTML={{
__html: t("jobs.labels.plitooltips.totalreturns"),
}}
/>
}
>
<Statistic
title={t("bills.labels.totalreturns")}
value={totalReturns.toFormat()}
/>
</Tooltip>
<Tooltip
title={
<div
dangerouslySetInnerHTML={{
__html: t("jobs.labels.plitooltips.creditsnotreceived"),
}}
/>
}
>
<Statistic
title={t("bills.labels.creditsnotreceived")}
valueStyle={{
color: creditsNotReceived.getAmount() <= 0 ? "green" : "red",
}}
value={
creditsNotReceived.getAmount() >= 0
? creditsNotReceived.toFormat()
: Dinero().toFormat()
}
/>
</Tooltip>
</Space>
</Card>
</Col>
</Row>
);
}

View File

@@ -1,5 +1,5 @@
import { useMutation } from "@apollo/client";
import { Button, Card, Form, notification, Switch } from "antd";
import { Button, Card, Form, Input, notification, Switch } from "antd";
import queryString from "query-string";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
@@ -54,7 +54,12 @@ export function JobChecklistForm({
(type === "intake" && bodyshop.md_ro_statuses.default_arrived) ||
(type === "deliver" && bodyshop.md_ro_statuses.default_delivered),
...(type === "intake" && { actual_in: new Date() }),
...(type === "intake" && {
production_vars: {
...job.production_vars,
...values.production_vars,
},
}),
...(type === "intake" && {
scheduled_completion: values.scheduled_completion,
}),
@@ -162,7 +167,7 @@ export function JobChecklistForm({
rules={[
{
required: true,
message: t("general.validation.required"),
//message: t("general.validation.required"),
},
]}
>
@@ -175,6 +180,13 @@ export function JobChecklistForm({
>
<DateTimePicker />
</Form.Item>
<Form.Item
name={["production_vars", "note"]}
label={t("jobs.fields.production_vars.note")}
disabled={readOnly}
>
<Input.TextArea rows={3} />
</Form.Item>
</div>
)}
{type === "deliver" && (
@@ -186,7 +198,7 @@ export function JobChecklistForm({
rules={[
{
required: true,
message: t("general.validation.required"),
//message: t("general.validation.required"),
},
]}
>

View File

@@ -28,7 +28,8 @@ export default function JobCostingPartsTable({ data, summaryData }) {
title: t("jobs.labels.sales"),
dataIndex: "sales",
key: "sales",
sorter: (a, b) => alphaSort(a.sales, b.sales),
sorter: (a, b) =>
parseFloat(a.sales.substring(1)) - parseFloat(b.sales.substring(1)),
sortOrder:
state.sortedInfo.columnKey === "sales" && state.sortedInfo.order,
},
@@ -37,7 +38,8 @@ export default function JobCostingPartsTable({ data, summaryData }) {
title: t("jobs.labels.costs"),
dataIndex: "costs",
key: "costs",
sorter: (a, b) => a.costs - b.costs,
sorter: (a, b) =>
parseFloat(a.costs.substring(1)) - parseFloat(b.costs.substring(1)),
sortOrder:
state.sortedInfo.columnKey === "costs" && state.sortedInfo.order,
},
@@ -46,7 +48,10 @@ export default function JobCostingPartsTable({ data, summaryData }) {
title: t("jobs.labels.gpdollars"),
dataIndex: "gpdollars",
key: "gpdollars",
sorter: (a, b) => a.gpdollars - b.gpdollars,
sorter: (a, b) =>
parseFloat(a.gpdollars.substring(1)) -
parseFloat(b.gpdollars.substring(1)),
sortOrder:
state.sortedInfo.columnKey === "gpdollars" && state.sortedInfo.order,
},
@@ -54,7 +59,9 @@ export default function JobCostingPartsTable({ data, summaryData }) {
title: t("jobs.labels.gppercent"),
dataIndex: "gppercent",
key: "gppercent",
sorter: (a, b) => a.gppercent - b.gppercent,
sorter: (a, b) =>
parseFloat(a.gppercent.slice(0, -1) || 0) -
parseFloat(b.gppercent.slice(0, -1) || 0),
sortOrder:
state.sortedInfo.columnKey === "gppercent" && state.sortedInfo.order,
},
@@ -70,6 +77,7 @@ export default function JobCostingPartsTable({ data, summaryData }) {
.includes(searchText.toLowerCase())
);
console.log("data :>> ", data);
return (
<div>
<Table
@@ -87,9 +95,11 @@ export default function JobCostingPartsTable({ data, summaryData }) {
</Space>
);
}}
scroll={{ x: "50%", y: "40rem" }}
scroll={{
x: "50%", //y: "40rem"
}}
onChange={handleTableChange}
pagination={{ position: "top", defaultPageSize: 25 }}
pagination={{ position: "top", defaultPageSize: 50 }}
columns={columns}
rowKey="id"
dataSource={filteredData}

View File

@@ -38,7 +38,6 @@ export default function JobCostingStatistics({ summaryData }) {
/>
<Statistic
value={summaryData.gppercentFormatted}
suffix="%"
title={t("jobs.labels.gppercent")}
/>
</div>

View File

@@ -1,6 +1,6 @@
import { PrinterFilled } from "@ant-design/icons";
import { useQuery } from "@apollo/client";
import { Button, Card, Drawer, Grid, PageHeader, Space, Tag } from "antd";
import { Button, Card, Col, Divider, Drawer, Grid, Row, Space } from "antd";
import queryString from "query-string";
import React from "react";
import { useTranslation } from "react-i18next";
@@ -9,9 +9,9 @@ import { Link, useHistory, useLocation } from "react-router-dom";
import { QUERY_JOB_CARD_DETAILS } from "../../graphql/jobs.queries";
import { setModalContext } from "../../redux/modals/modals.actions";
import AlertComponent from "../alert/alert.component";
import JobSyncButton from "../job-sync-button/job-sync-button.component";
import JobsDetailHeader from "../jobs-detail-header/jobs-detail-header.component";
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
import OwnerTagPopoverComponent from "../owner-tag-popover/owner-tag-popover.component";
import VehicleTagPopoverComponent from "../vehicle-tag-popover/vehicle-tag-popover.component";
import JobDetailCardsDamageComponent from "./job-detail-cards.damage.component";
import JobDetailCardsDatesComponent from "./job-detail-cards.dates.component";
import JobDetailCardsDocumentsComponent from "./job-detail-cards.documents.component";
@@ -25,6 +25,12 @@ const mapDispatchToProps = (dispatch) => ({
dispatch(setModalContext({ context: context, modal: "printCenter" })),
});
const span = {
sm: { span: 24 },
md: { span: 12 },
lg: { span: 8 },
};
export function JobDetailCards({ setPrintCenterContext }) {
const selectedBreakpoint = Object.entries(Grid.useBreakpoint())
.filter((screen) => !!screen[1])
@@ -34,9 +40,9 @@ export function JobDetailCards({ setPrintCenterContext }) {
xs: "100%",
sm: "100%",
md: "100%",
lg: "50%",
xl: "50%",
xxl: "45%",
lg: "75%",
xl: "75%",
xxl: "60%",
};
const drawerPercentage = selectedBreakpoint
? bpoints[selectedBreakpoint[0]]
@@ -46,7 +52,6 @@ export function JobDetailCards({ setPrintCenterContext }) {
const { selected } = searchParams;
const history = useHistory();
const { loading, error, data, refetch } = useQuery(QUERY_JOB_CARD_DETAILS, {
fetchPolicy: "network-only",
variables: { id: selected },
skip: !selected,
});
@@ -60,10 +65,7 @@ export function JobDetailCards({ setPrintCenterContext }) {
}),
});
};
const gridStyle = {
width: "25%",
textAlign: "center",
};
return (
<Drawer
visible={!!selected}
@@ -75,99 +77,98 @@ export function JobDetailCards({ setPrintCenterContext }) {
{loading ? <LoadingSpinner /> : null}
{error ? <AlertComponent message={error.message} type="error" /> : null}
{data ? (
<PageHeader
// ghost={true}
tags={[
<OwnerTagPopoverComponent key="owner" job={data.jobs_by_pk} />,
<VehicleTagPopoverComponent key="vehicle" job={data.jobs_by_pk} />,
<Tag
color="#f50"
key="production"
style={{
display:
data && data.jobs_by_pk && data.jobs_by_pk.inproduction
? ""
: "none",
}}
>
{t("jobs.labels.inproduction")}
</Tag>,
]}
subTitle={data.jobs_by_pk.status}
>
<Card
title={
<Link to={`/manage/jobs/${data.jobs_by_pk.id}`}>
{data.jobs_by_pk.ro_number || t("general.labels.na")}
<Card
title={
<Link to={`/manage/jobs/${data.jobs_by_pk.id}`}>
{data.jobs_by_pk.ro_number || t("general.labels.na")}
</Link>
}
extra={
<Space wrap>
<JobSyncButton job={data.jobs_by_pk} />
<Button
onClick={() => {
setPrintCenterContext({
actions: { refetch: refetch },
context: {
id: data.jobs_by_pk.id,
job: data.jobs_by_pk,
type: "job",
},
});
}}
>
<PrinterFilled />
{t("jobs.actions.printCenter")}
</Button>
<Link to={`/manage/jobs/${data.jobs_by_pk.id}?tab=repairdata`}>
<Button>{t("parts.actions.order")}</Button>
</Link>
}
extra={
<Space>
<Button
onClick={() => {
setPrintCenterContext({
actions: { refetch: refetch },
context: {
id: data.jobs_by_pk.id,
job: data.jobs_by_pk,
type: "job",
},
});
}}
>
<PrinterFilled />
{t("jobs.actions.printCenter")}
</Button>
<Link to={`/manage/jobs/${data.jobs_by_pk.id}?tab=repairdata`}>
<Button>{t("parts.actions.order")}</Button>
</Link>
</Space>
}
>
<Card.Grid style={gridStyle}>
<JobDetailCardsInsuranceComponent
loading={loading}
data={data ? data.jobs_by_pk : null}
/>
</Card.Grid>
<Card.Grid style={gridStyle}>
<JobDetailCardsTotalsComponent
loading={loading}
data={data ? data.jobs_by_pk : null}
/>
</Card.Grid>
<Card.Grid style={gridStyle}>
<JobDetailCardsDatesComponent
loading={loading}
data={data ? data.jobs_by_pk : null}
/>
</Card.Grid>
<Card.Grid style={gridStyle}>
<JobDetailCardsPartsComponent
loading={loading}
data={data ? data.jobs_by_pk : null}
/>
</Card.Grid>
<Card.Grid style={gridStyle}>
<JobDetailCardsNotesComponent
loading={loading}
data={data ? data.jobs_by_pk : null}
/>
</Card.Grid>
<Card.Grid style={gridStyle}>
<JobDetailCardsDocumentsComponent
loading={loading}
data={data ? data.jobs_by_pk : null}
/>
</Card.Grid>
<Card.Grid style={gridStyle}>
<JobDetailCardsDamageComponent
loading={loading}
data={data ? data.jobs_by_pk : null}
/>
</Card.Grid>
</Card>
</PageHeader>
</Space>
}
>
<JobsDetailHeader job={data ? data.jobs_by_pk : null} />
<Divider type="horizontal" />
<Row gutter={[16, 16]}>
<Col {...span}>
<Card.Grid style={{ width: "100%", height: "100%" }}>
<JobDetailCardsInsuranceComponent
loading={loading}
data={data ? data.jobs_by_pk : null}
/>
</Card.Grid>
</Col>
<Col {...span}>
<Card.Grid style={{ width: "100%", height: "100%" }}>
<JobDetailCardsTotalsComponent
loading={loading}
data={data ? data.jobs_by_pk : null}
/>
</Card.Grid>
</Col>
<Col {...span}>
<Card.Grid style={{ width: "100%", height: "100%" }}>
<JobDetailCardsDatesComponent
loading={loading}
data={data ? data.jobs_by_pk : null}
/>
</Card.Grid>
</Col>
<Col {...span}>
<Card.Grid style={{ width: "100%", height: "100%" }}>
<JobDetailCardsPartsComponent
loading={loading}
data={data ? data.jobs_by_pk : null}
/>
</Card.Grid>
</Col>
<Col {...span}>
<Card.Grid style={{ width: "100%", height: "100%" }}>
<JobDetailCardsNotesComponent
loading={loading}
data={data ? data.jobs_by_pk : null}
/>
</Card.Grid>
</Col>
<Col {...span}>
<Card.Grid style={{ width: "100%", height: "100%" }}>
<JobDetailCardsDocumentsComponent
loading={loading}
data={data ? data.jobs_by_pk : null}
/>
</Card.Grid>
</Col>
<Col {...span}>
<Card.Grid style={{ width: "100%", height: "100%" }}>
<JobDetailCardsDamageComponent
loading={loading}
data={data ? data.jobs_by_pk : null}
/>
</Card.Grid>
</Col>
</Row>
</Card>
) : null}
</Drawer>
);

View File

@@ -2,7 +2,7 @@ import { Carousel } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import CardTemplate from "./job-detail-cards.template.component";
import { DetermineFileType } from "../documents-upload/documents-upload.utility";
export default function JobDetailCardsDocumentsComponent({ loading, data }) {
const { t } = useTranslation();
@@ -17,13 +17,18 @@ export default function JobDetailCardsDocumentsComponent({ loading, data }) {
<CardTemplate
loading={loading}
title={t("jobs.labels.cards.documents")}
extraLink={`/manage/jobs/${data.id}?tab=documents`}>
extraLink={`/manage/jobs/${data.id}?tab=documents`}
>
{data.documents.length > 0 ? (
<Carousel autoplay>
{data.documents.map((item) => (
<img
key={item.id}
src={`${process.env.REACT_APP_CLOUDINARY_IMAGE_ENDPOINT}/${process.env.REACT_APP_CLOUDINARY_THUMB_TRANSFORMATIONS}/${item.key}.jpg`}
src={`${
process.env.REACT_APP_CLOUDINARY_ENDPOINT
}/${DetermineFileType(item.type)}/upload/${
process.env.REACT_APP_CLOUDINARY_THUMB_TRANSFORMATIONS
}/${item.key}`}
alt={item.name}
/>
))}

View File

@@ -1,6 +1,20 @@
import { DeleteFilled, FilterFilled, SyncOutlined } from "@ant-design/icons";
import {
DeleteFilled,
FilterFilled,
SyncOutlined,
WarningFilled,
} from "@ant-design/icons";
import { useMutation } from "@apollo/client";
import { Button, Dropdown, Input, Menu, PageHeader, Space, Table } from "antd";
import {
Button,
Dropdown,
Input,
Menu,
PageHeader,
Space,
Table,
Tag,
} from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
@@ -90,7 +104,10 @@ export function JobLinesComponent({
sortOrder:
state.sortedInfo.columnKey === "op_code_desc" && state.sortedInfo.order,
ellipsis: true,
render: (text, record) => record.op_code_desc,
render: (text, record) =>
`${record.op_code_desc||""}${
record.alt_partm ? ` ${record.alt_partm}` : ""
}`,
},
{
title: t("joblines.fields.part_type"),
@@ -293,10 +310,14 @@ export function JobLinesComponent({
};
const handleMark = (e) => {
setSelectedLines([
...selectedLines,
...jobLines.filter((item) => item.part_type === e.key),
]);
if (e.key === "clear") {
setSelectedLines([]);
} else {
setSelectedLines([
...selectedLines,
...jobLines.filter((item) => item.part_type === e.key),
]);
}
};
const markMenu = (
@@ -305,6 +326,8 @@ export function JobLinesComponent({
<Menu.Item key="PAN">{t("joblines.fields.part_types.PAN")}</Menu.Item>
<Menu.Item key="PAL">{t("joblines.fields.part_types.PAL")}</Menu.Item>
<Menu.Item key="PAS">{t("joblines.fields.part_types.PAS")}</Menu.Item>
<Menu.Divider />
<Menu.Item key="clear">{t("general.labels.clear")}</Menu.Item>
</Menu>
);
@@ -318,6 +341,15 @@ export function JobLinesComponent({
<Button onClick={() => refetch()}>
<SyncOutlined />
</Button>
{job.special_coverage_policy && (
<Tag color="tomato">
<Space>
<WarningFilled />
<span>{t("jobs.labels.specialcoveragepolicy")}</span>
</Space>
</Tag>
)}
<Button
disabled={
(job && !job.converted) ||
@@ -329,6 +361,7 @@ export function JobLinesComponent({
actions: { refetch: refetch },
context: {
jobId: job.id,
job: job,
linesToOrder: selectedLines,
},
});
@@ -338,6 +371,7 @@ export function JobLinesComponent({
}}
>
{t("parts.actions.order")}
{selectedLines.length > 0 && ` (${selectedLines.length})`}
</Button>
<Button
onClick={() => {
@@ -354,7 +388,6 @@ export function JobLinesComponent({
<Dropdown overlay={markMenu} trigger={["click"]}>
<Button>{t("jobs.actions.mark")}</Button>
</Dropdown>
<Button
disabled={jobRO}
onClick={() => {
@@ -366,7 +399,6 @@ export function JobLinesComponent({
>
{t("joblines.actions.new")}
</Button>
<Input.Search
placeholder={t("general.labels.search")}
onChange={(e) => {
@@ -387,6 +419,18 @@ export function JobLinesComponent({
scroll={{
x: true,
}}
onRow={(record, rowIndex) => {
return {
onDoubleClick: (event) => {
const notMatchingLines = selectedLines.filter(
(i) => i.id !== record.id
);
notMatchingLines.length !== selectedLines.length
? setSelectedLines(notMatchingLines)
: setSelectedLines([...selectedLines, record]);
}, // double click row
};
}}
rowSelection={{
selectedRowKeys: selectedLines.map((item) => item.id),
onSelectAll: (selected, selectedRows, changeRows) => {

View File

@@ -23,7 +23,9 @@ export function JobEmployeeAssignments({
jobRO,
body,
refinish,
prep,
csr,
handleAdd,
handleRemove,
loading,
@@ -155,6 +157,30 @@ export function JobEmployeeAssignments({
/>
)}
</DataLabel>
<DataLabel label={t("jobs.fields.employee_csr")}>
{csr ? (
<div>
<span>{`${csr.first_name || ""} ${csr.last_name || ""}`}</span>
<DeleteFilled
disabled={jobRO}
style={iconStyle}
operation="csr"
onClick={() => !jobRO && handleRemove("csr")}
/>
</div>
) : (
<PlusCircleFilled
disabled={jobRO}
style={iconStyle}
onClick={() => {
if (!jobRO) {
setAssignment({ operation: "csr" });
setVisibility(true);
}
}}
/>
)}
</DataLabel>
</Spin>
</Popover>
);

View File

@@ -61,6 +61,7 @@ export default function JobEmployeeAssignmentsContainer({ job, refetch }) {
body={job.employee_body_rel}
refinish={job.employee_refinish_rel}
prep={job.employee_prep_rel}
csr={job.employee_csr_rel}
handleAdd={handleAdd}
handleRemove={handleRemove}
loading={loading}
@@ -75,6 +76,8 @@ const determineFieldName = (operation) => {
return "employee_body";
case "prep":
return "employee_prep";
case "csr":
return "employee_csr";
case "refinish":
return "employee_refinish";

View File

@@ -1,13 +1,16 @@
import React from "react";
import { WarningFilled } from "@ant-design/icons";
export default function JobLinesBillRefernece({ jobline }) {
const billLine = jobline.billlines && jobline.billlines[0];
if (!billLine) return null;
const subletRequired = billLine.actual_price !== jobline.act_price;
return (
<div>{`${(billLine.actual_price * billLine.quantity).toFixed(2)} (${
billLine.bill.vendor.name
})`}</div>
<div style={{ color: subletRequired && "tomato" }}>
{subletRequired && <WarningFilled />}
{`${(billLine.actual_price * billLine.quantity).toFixed(2)} (${
billLine.bill.vendor.name
})`}
</div>
);
}

View File

@@ -46,7 +46,7 @@ export default function JobLinesUpsertModalComponent({
rules={[
{
required: true,
message: t("general.validation.required"),
//message: t("general.validation.required"),
},
]}
name="line_desc"

View File

@@ -1,4 +1,4 @@
import { Checkbox, Table } from "antd";
import { Checkbox, PageHeader, Table } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
@@ -86,7 +86,7 @@ export default function JobReconciliationBillsTable({
};
return (
<div>
<PageHeader title={t("bills.labels.bills")}>
<Table
pagination={false}
scroll={{ y: "40vh", x: true }}
@@ -99,6 +99,6 @@ export default function JobReconciliationBillsTable({
selectedRowKeys: selectedLines,
}}
/>
</div>
</PageHeader>
);
}

View File

@@ -1,4 +1,4 @@
import { Table } from "antd";
import { PageHeader, Table } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
@@ -102,7 +102,7 @@ export default function JobReconcilitionPartsTable({
};
return (
<div>
<PageHeader title={t("jobs.labels.lines")}>
<Table
pagination={false}
columns={columns}
@@ -122,6 +122,6 @@ export default function JobReconcilitionPartsTable({
<div style={{ fontStyle: "italic", margin: "4px" }}>
{t("jobs.labels.reconciliation.removedpartsstrikethrough")}
</div>
</div>
</PageHeader>
);
}

View File

@@ -8,7 +8,11 @@ import { INSERT_SCOREBOARD_ENTRY } from "../../graphql/scoreboard.queries";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
import InputNumberCalculator from "../form-input-number-calculator/form-input-number-calculator.component";
export default function ScoreboardAddButton({ job, ...otherBtnProps }) {
export default function ScoreboardAddButton({
job,
disabled,
...otherBtnProps
}) {
const { t } = useTranslation();
const [insertScoreboardEntry] = useMutation(INSERT_SCOREBOARD_ENTRY);
const [loading, setLoading] = useState(false);
@@ -53,7 +57,7 @@ export default function ScoreboardAddButton({ job, ...otherBtnProps }) {
rules={[
{
required: true,
message: t("general.validation.required"),
//message: t("general.validation.required"),
},
]}
>
@@ -65,7 +69,7 @@ export default function ScoreboardAddButton({ job, ...otherBtnProps }) {
rules={[
{
required: true,
message: t("general.validation.required"),
//message: t("general.validation.required"),
},
]}
>
@@ -77,7 +81,7 @@ export default function ScoreboardAddButton({ job, ...otherBtnProps }) {
rules={[
{
required: true,
message: t("general.validation.required"),
//message: t("general.validation.required"),
},
]}
>
@@ -118,7 +122,12 @@ export default function ScoreboardAddButton({ job, ...otherBtnProps }) {
return (
<Popover content={overlay} visible={visibility}>
<Button loading={loading} onClick={handleClick} {...otherBtnProps}>
<Button
loading={loading}
disabled={disabled}
onClick={handleClick}
{...otherBtnProps}
>
{t("jobs.actions.addtoscoreboard")}
</Button>
</Popover>

View File

@@ -2,7 +2,7 @@ import { LoadingOutlined } from "@ant-design/icons";
import { useLazyQuery } from "@apollo/client";
import { Empty, Select } from "antd";
import _ from "lodash";
import React, { forwardRef, useEffect, useState } from "react";
import React, { forwardRef, useEffect } from "react";
import { useTranslation } from "react-i18next";
import {
SEARCH_JOBS_BY_ID_FOR_AUTOCOMPLETE,
@@ -13,13 +13,11 @@ const { Option } = Select;
const JobSearchSelect = (
{
value,
onChange,
onBlur,
disabled,
convertedOnly = false,
notExported = true,
clm_no = false,
...restProps
},
ref
) => {
@@ -52,20 +50,11 @@ const JobSearchSelect = (
debouncedExecuteSearch({ variables: { search: value } });
};
const [option, setOption] = useState(value);
useEffect(() => {
if (value === option && value) {
callIdSearch({ variables: { id: value } }); // Sometimes results in a no-op. Not sure how to fix.
if (restProps.value) {
callIdSearch({ variables: { id: restProps.value } }); // Sometimes results in a no-op. Not sure how to fix.
}
}, [value, option, callIdSearch]);
const handleSelect = (value) => {
setOption(value);
if (value !== option && onChange) {
onChange(value);
}
};
}, [restProps.value, callIdSearch]);
const theOptions = _.uniqBy(
[
@@ -82,17 +71,13 @@ const JobSearchSelect = (
disabled={disabled}
showSearch
autoFocus
value={option}
style={{
width: "100%",
}}
filterOption={false}
onSearch={handleSearch}
// onChange={setOption}
onChange={handleSelect}
onSelect={handleSelect}
notFoundContent={loading ? <LoadingOutlined /> : <Empty />}
onBlur={onBlur}
{...restProps}
>
{theOptions
? theOptions.map((o) => (

View File

@@ -6,6 +6,7 @@ import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectJobReadOnly } from "../../redux/application/application.selectors";
import { selectCurrentUser } from "../../redux/user/user.selectors";
import JobCalculateTotals from "../job-calculate-totals/job-calculate-totals.component";
import "./job-totals-table.styles.scss";
import JobTotalsTableLabor from "./job-totals.table.labor.component";
@@ -14,7 +15,7 @@ import JobTotalsTableParts from "./job-totals.table.parts.component";
import JobTotalsTableTotals from "./job-totals.table.totals.component";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
currentUser: selectCurrentUser,
jobRO: selectJobReadOnly,
});
@@ -23,7 +24,7 @@ const colSpan = {
lg: { span: 12 },
};
export function JobsTotalsTableComponent({ jobRO, job }) {
export function JobsTotalsTableComponent({ jobRO, currentUser, job }) {
const { t } = useTranslation();
if (!!!job.job_totals) {
@@ -66,28 +67,30 @@ export function JobsTotalsTableComponent({ jobRO, job }) {
<JobTotalsTableTotals job={job} />
</Card>
</Col>
<Col span={24}>
<Card title="DEVELOPMENT USE ONLY">
<JobCalculateTotals job={job} disabled={jobRO} />
<Collapse>
<Collapse.Panel header="JSON Tree Totals">
<div>
<pre>
{JSON.stringify(
{
CIECA: job.cieca_ttl && job.cieca_ttl.data,
CIECASTL: job.cieca_stl && job.cieca_stl.data,
ImEXCalc: job.job_totals,
},
null,
2
)}
</pre>
</div>
</Collapse.Panel>
</Collapse>
</Card>
</Col>
{currentUser.email.includes("@imex.") && (
<Col span={24}>
<Card title="DEVELOPMENT USE ONLY">
<JobCalculateTotals job={job} disabled={jobRO} />
<Collapse>
<Collapse.Panel header="JSON Tree Totals">
<div>
<pre>
{JSON.stringify(
{
CIECA: job.cieca_ttl && job.cieca_ttl.data,
CIECASTL: job.cieca_stl && job.cieca_stl.data,
ImEXCalc: job.job_totals,
},
null,
2
)}
</pre>
</div>
</Collapse.Panel>
</Collapse>
</Card>
</Col>
)}
</Row>
</Col>
</Row>

View File

@@ -106,7 +106,12 @@ export default function JobTotalsTableLabor({ job }) {
<strong>{t("jobs.labels.labor_rates_subtotal")}</strong>
</Table.Summary.Cell>
<Table.Summary.Cell />
<Table.Summary.Cell />
<Table.Summary.Cell>
{(
job.job_totals.rates.mapa.hours +
job.job_totals.rates.mash.hours
).toFixed(1)}
</Table.Summary.Cell>
<Table.Summary.Cell align="right">
<strong>
{Dinero(job.job_totals.rates.rates_subtotal).toFormat()}
@@ -115,11 +120,13 @@ export default function JobTotalsTableLabor({ job }) {
</Table.Summary.Row>
<Table.Summary.Row>
<Table.Summary.Cell>{t("jobs.labels.mapa")}</Table.Summary.Cell>
<Table.Summary.Cell>
{job.job_totals.rates.mapa.rate}
<Table.Summary.Cell align="right">
<CurrencyFormatter>
{job.job_totals.rates.mapa.rate}
</CurrencyFormatter>
</Table.Summary.Cell>
<Table.Summary.Cell>
{job.job_totals.rates.mapa.hours.toFixed(2)}
{job.job_totals.rates.mapa.hours.toFixed(1)}
</Table.Summary.Cell>
<Table.Summary.Cell align="right">
{Dinero(job.job_totals.rates.mapa.total).toFormat()}
@@ -127,11 +134,13 @@ export default function JobTotalsTableLabor({ job }) {
</Table.Summary.Row>
<Table.Summary.Row>
<Table.Summary.Cell>{t("jobs.labels.mash")}</Table.Summary.Cell>
<Table.Summary.Cell>
{job.job_totals.rates.mash.rate}
<Table.Summary.Cell align="right">
<CurrencyFormatter>
{job.job_totals.rates.mash.rate}
</CurrencyFormatter>
</Table.Summary.Cell>
<Table.Summary.Cell>
{job.job_totals.rates.mash.hours.toFixed(2)}
{job.job_totals.rates.mash.hours.toFixed(1)}
</Table.Summary.Cell>
<Table.Summary.Cell align="right">
{Dinero(job.job_totals.rates.mash.total).toFormat()}

View File

@@ -42,7 +42,7 @@ export default function JobTotalsTableOther({ job }) {
const columns = [
{
//title: t("joblines.fields.part_type"),
title: t("general.labels.item"),
dataIndex: "key",
key: "key",
sorter: (a, b) => alphaSort(a.key, b.key),
@@ -82,7 +82,7 @@ export default function JobTotalsTableOther({ job }) {
<strong>{t("jobs.labels.additionaltotal")}</strong>
</Table.Summary.Cell>
<Table.Summary.Cell>
<Table.Summary.Cell align="right">
<strong>
{Dinero(job.job_totals.additional.total).toFormat()}
</strong>
@@ -93,7 +93,7 @@ export default function JobTotalsTableOther({ job }) {
<strong>{t("jobs.labels.subletstotal")}</strong>
</Table.Summary.Cell>
<Table.Summary.Cell>
<Table.Summary.Cell align="right">
<strong>
{Dinero(job.job_totals.parts.sublets.total).toFormat()}
</strong>

View File

@@ -74,7 +74,7 @@ export default function JobTotalsTableParts({ job }) {
<strong>{t("jobs.labels.partstotal")}</strong>
</Table.Summary.Cell>
<Table.Summary.Cell>
<Table.Summary.Cell align="right">
<strong>
{Dinero(job.job_totals.parts.parts.total).toFormat()}
</strong>

View File

@@ -25,6 +25,11 @@ export default function JobTotalsTableTotals({ job }) {
key: t("jobs.labels.federal_tax_amt"),
total: job.job_totals.totals.federal_tax,
},
{
key: t("jobs.labels.total_repairs"),
total: job.job_totals.totals.total_repairs,
bold: true,
},
{
key: t("jobs.fields.ded_amt"),
total: job.job_totals.totals.custPayable.deductible,
@@ -41,14 +46,11 @@ export default function JobTotalsTableTotals({ job }) {
key: t("jobs.fields.depreciation_taxes"),
total: job.job_totals.totals.custPayable.dep_taxes,
},
{
key: t("jobs.labels.total_repairs"),
total: job.job_totals.totals.total_repairs,
bold: true,
},
{
key: t("jobs.labels.total_cust_payable"),
total: job.job_totals.totals.custPayable.total,
bold: true,
},
{
key: t("jobs.labels.net_repairs"),

View File

@@ -6,7 +6,6 @@ import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { UPDATE_JOB } from "../../graphql/jobs.queries";
import { selectBodyshop } from "../../redux/user/user.selectors";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -55,26 +54,24 @@ export function JobsAdminClass({ bodyshop, job }) {
layout="vertical"
initialValues={job}
>
<LayoutFormRow>
<Form.Item
name={["class"]}
label={t("jobs.fields.class")}
rules={[
{
required: bodyshop.enforce_class,
message: t("general.validation.required"),
},
]}
>
<Select>
{bodyshop.md_classes.map((s) => (
<Select.Option key={s} value={s}>
{s}
</Select.Option>
))}
</Select>
</Form.Item>
</LayoutFormRow>
<Form.Item
name={["class"]}
label={t("jobs.fields.class")}
rules={[
{
required: bodyshop.enforce_class,
//message: t("general.validation.required"),
},
]}
>
<Select>
{bodyshop.md_classes.map((s) => (
<Select.Option key={s} value={s}>
{s}
</Select.Option>
))}
</Select>
</Form.Item>
</Form>
<Popconfirm

View File

@@ -38,7 +38,6 @@ export default function JobsAdminDatesChange({ job }) {
return (
<div>
<div>{t("jobs.labels.ownerassociation")}</div>
<Form
onFinish={handleFinish}
autoComplete={"off"}

View File

@@ -38,12 +38,8 @@ export default function JobAdminDeleteIntake({ job }) {
};
return (
<div>
<div>{t("jobs.labels.deleteintake")}</div>
<Button loading={loading} onClick={handleDelete}>
{t("general.actions.delete")}
</Button>
</div>
<Button loading={loading} onClick={handleDelete}>
{t("jobs.labels.deleteintake")}
</Button>
);
}

View File

@@ -0,0 +1,66 @@
import { useMutation } from "@apollo/client";
import { Button, notification } from "antd";
import { gql } from "@apollo/client";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(JobAdminMarkReexport);
export function JobAdminMarkReexport({ bodyshop, job }) {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const [updateJob] = useMutation(gql`
mutation UPDATE_JOB($jobId: uuid!) {
update_jobs_by_pk(
pk_columns: { id: $jobId }
_set: { date_exported: null
status: "${bodyshop.md_ro_statuses.default_invoiced}"
}
) {
id
intakechecklist
}
}
`);
const handleUpdate = async (values) => {
setLoading(true);
const result = await updateJob({
variables: { jobId: job.id },
});
if (!result.errors) {
notification["success"]({ message: t("jobs.successes.save") });
} else {
notification["error"]({
message: t("jobs.errors.saving", {
error: JSON.stringify(result.errors),
}),
});
}
setLoading(false);
//Get the owner details, populate it all back into the job.
};
return (
<Button
loading={loading}
disabled={(!job.voided && !job.date_exported) || !job.converted}
onClick={handleUpdate}
>
{t("jobs.labels.markforreexport")}
</Button>
);
}

View File

@@ -48,7 +48,7 @@ export default function JobAdminOwnerReassociate({ job }) {
rules={[
{
required: true,
message: t("general.validation.required"),
//message: t("general.validation.required"),
},
]}
>

View File

@@ -0,0 +1,103 @@
import { gql, useMutation } from "@apollo/client";
import { Button, notification } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import {
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
currentUser: selectCurrentUser,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(mapStateToProps, mapDispatchToProps)(JobsAdminUnvoid);
export function JobsAdminUnvoid({ bodyshop, job, currentUser }) {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const [updateJob] = useMutation(gql`
mutation UNVOID_JOB($jobId: uuid!) {
update_jobs_by_pk(pk_columns: {id: $jobId}, _set: {voided: false, status: "${
bodyshop.md_ro_statuses.default_imported
}"}) {
id
voided
status
}
insert_notes(objects: {jobid: $jobId, audit: true, created_by: "${
currentUser.email
}", text: "${t("jobs.labels.unvoidnote", { email: currentUser.email })}"}) {
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) => {
setLoading(true);
const result = await updateJob({
variables: { jobId: job.id },
});
if (!result.errors) {
notification["success"]({ message: t("jobs.successes.save") });
} else {
notification["error"]({
message: t("jobs.errors.saving", {
error: JSON.stringify(result.errors),
}),
});
}
setLoading(false);
//Get the owner details, populate it all back into the job.
};
return (
<Button loading={loading} disabled={!job.voided} onClick={handleUpdate}>
{t("jobs.actions.unvoid")}
</Button>
);
}

View File

@@ -48,7 +48,7 @@ export default function JobAdminOwnerReassociate({ job }) {
rules={[
{
required: true,
message: t("general.validation.required"),
//message: t("general.validation.required"),
},
]}
>

View File

@@ -5,18 +5,34 @@ import {
SyncOutlined,
} from "@ant-design/icons";
import { useMutation } from "@apollo/client";
import { Button, Card, Input, notification, Space, Table } from "antd";
import { Alert, Button, Card, Input, notification, Space, Table } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { Link } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import {
DELETE_ALL_AVAILABLE_JOBS,
DELETE_AVAILABLE_JOB,
} from "../../graphql/available-jobs.queries";
import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { TimeAgoFormatter } from "../../utils/DateFormatter";
import { alphaSort } from "../../utils/sorters";
export default function JobsAvailableComponent({
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(JobsAvailableComponent);
export function JobsAvailableComponent({
bodyshop,
loading,
data,
refetch,
@@ -58,11 +74,11 @@ export default function JobsAvailableComponent({
render: (text, record) =>
record.job ? (
<Link to={`/manage/jobs/${record.job.id}`}>
{(record.job && record.job_ro_number) || t("general.labels.na")}
{(record.job && record.job.ro_number) || t("general.labels.na")}
</Link>
) : (
<div>
{(record.job && record.job_ro_number) || t("general.labels.na")}
{(record.job && record.job.ro_number) || t("general.labels.na")}
</div>
),
},
@@ -132,31 +148,47 @@ export default function JobsAvailableComponent({
{
title: t("general.labels.actions"),
key: "actions",
render: (text, record) => (
<Space wrap>
<Button
onClick={() => {
deleteJob({ variables: { id: record.id } }).then((r) => {
notification["success"]({
message: t("jobs.successes.deleted"),
render: (text, record) => {
const isClosed =
record.job &&
(record.job.status === bodyshop.md_ro_statuses.default_exported ||
record.job.status === bodyshop.md_ro_statuses.default_invoiced);
return (
<Space wrap>
<Button
onClick={() => {
deleteJob({ variables: { id: record.id } }).then((r) => {
notification["success"]({
message: t("jobs.successes.deleted"),
});
refetch();
});
refetch();
});
}}
>
<DeleteFilled />
</Button>
<Button
onClick={() => addJobAsNew(record)}
disabled={record.issupplement}
>
<PlusCircleFilled />
</Button>
<Button onClick={() => addJobAsSupp(record)}>
<DownloadOutlined />
</Button>
</Space>
),
}}
>
<DeleteFilled />
</Button>
{!isClosed && (
<>
<Button
onClick={() => addJobAsNew(record)}
disabled={record.issupplement}
>
<PlusCircleFilled />
</Button>
<Button onClick={() => addJobAsSupp(record)}>
<DownloadOutlined />
</Button>
</>
)}
{isClosed && (
<Alert
type="error"
message={t("jobs.labels.alreadyclosed")}
></Alert>
)}
</Space>
);
},
},
];

View File

@@ -56,7 +56,13 @@ export function JobsChangeStatus({ job, bodyshop, jobRO }) {
} else if (
bodyshop.md_ro_statuses.post_production_statuses.includes(job.status)
) {
setAvailableStatuses(bodyshop.md_ro_statuses.post_production_statuses);
setAvailableStatuses(
bodyshop.md_ro_statuses.post_production_statuses.filter(
(s) =>
s !== bodyshop.md_ro_statuses.default_invoiced &&
s !== bodyshop.md_ro_statuses.default_exported
)
);
if (bodyshop.md_ro_statuses.production_statuses[0])
setOtherStages([bodyshop.md_ro_statuses.production_statuses[0]]);
} else {

View File

@@ -30,6 +30,7 @@ export function JobsCloseAutoAllocate({ bodyshop, joblines, form, disabled }) {
} else {
ret.profitcenter_labor = null;
}
//Verify that this is also manually updated in server/job-costing
if (!jl.part_type && !jl.mod_lbr_ty) {
const lineDesc = jl.line_desc.toLowerCase();
if (lineDesc.includes("shop materials")) {

View File

@@ -7,16 +7,27 @@ import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { auth } from "../../firebase/firebase.utils";
import { UPDATE_JOB } from "../../graphql/jobs.queries";
import { selectBodyshop } from "../../redux/user/user.selectors";
import {
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
import { logImEXEvent } from "../../firebase/firebase.utils";
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
currentUser: selectCurrentUser,
});
export function JobsCloseExportButton({ bodyshop, jobId, disabled }) {
export function JobsCloseExportButton({
bodyshop,
currentUser,
jobId,
disabled,
}) {
const { t } = useTranslation();
const [updateJob] = useMutation(UPDATE_JOB);
const [insertExportLog] = useMutation(INSERT_EXPORT_LOG);
const [loading, setLoading] = useState(false);
const handleQbxml = async () => {
logImEXEvent("jobs_close_export");
@@ -72,14 +83,47 @@ export function JobsCloseExportButton({ bodyshop, jobId, disabled }) {
const failedTransactions = PartnerResponse.data.filter((r) => !r.success);
if (failedTransactions.length > 0) {
//Uh oh. At least one was no good.
failedTransactions.map((ft) =>
notification["error"]({
failedTransactions.forEach((ft) => {
//insert failed export log
notification.open({
// key: "failedexports",
type: "error",
message: t("jobs.errors.exporting", {
error: ft.errorMessage || "",
}),
})
);
});
});
await insertExportLog({
variables: {
logs: [
{
bodyshopid: bodyshop.id,
jobid: jobId,
successful: false,
message: JSON.stringify(
failedTransactions.map((ft) => ft.errorMessage)
),
useremail: currentUser.email,
},
],
},
});
} else {
//Insert success export log.
await insertExportLog({
variables: {
logs: [
{
bodyshopid: bodyshop.id,
jobid: jobId,
successful: true,
useremail: currentUser.email,
},
],
},
});
const jobUpdateResponse = await updateJob({
variables: {
jobId: jobId,
@@ -90,8 +134,10 @@ export function JobsCloseExportButton({ bodyshop, jobId, disabled }) {
},
});
if (!!!jobUpdateResponse.errors) {
notification["success"]({
if (!jobUpdateResponse.errors) {
notification.open({
type: "success",
key: "jobsuccessexport",
message: t("jobs.successes.exported"),
});
} else {

View File

@@ -108,7 +108,7 @@ export function JobsCloseLines({ bodyshop, job, jobRO }) {
rules={[
{
required: !!job.joblines[index].act_price,
message: t("general.validation.required"),
//message: t("general.validation.required"),
},
]}
>
@@ -141,7 +141,7 @@ export function JobsCloseLines({ bodyshop, job, jobRO }) {
rules={[
{
required: !!job.joblines[index].mod_lbr_ty,
message: t("general.validation.required"),
//message: t("general.validation.required"),
},
]}
>

View File

@@ -27,10 +27,13 @@ const mapDispatchToProps = (dispatch) => ({
export function JobsConvertButton({ bodyshop, job, refetch, jobRO }) {
const [visible, setVisible] = useState(false);
const [loading, setLoading] = useState(false);
const [mutationConvertJob] = useMutation(CONVERT_JOB_TO_RO);
const { t } = useTranslation();
const [form] = Form.useForm();
const handleConvert = async (values) => {
setLoading(true);
const res = await mutationConvertJob({
variables: { jobId: job.id, ...values },
});
@@ -42,12 +45,14 @@ export function JobsConvertButton({ bodyshop, job, refetch, jobRO }) {
});
setVisible(false);
}
setLoading(false);
};
const popMenu = (
<div>
<Form
layout="vertical"
form={form}
onFinish={handleConvert}
initialValues={{ driveable: true, towin: false }}
>
@@ -57,7 +62,7 @@ export function JobsConvertButton({ bodyshop, job, refetch, jobRO }) {
rules={[
{
required: true,
message: t("general.validation.required"),
//message: t("general.validation.required"),
},
]}
>
@@ -69,24 +74,46 @@ export function JobsConvertButton({ bodyshop, job, refetch, jobRO }) {
))}
</Select>
</Form.Item>
<Form.Item
name={"class"}
label={t("jobs.fields.class")}
rules={[
{
required: bodyshop.enforce_class,
message: t("general.validation.required"),
},
]}
>
<Select>
{bodyshop.md_classes.map((s) => (
<Select.Option key={s} value={s}>
{s}
</Select.Option>
))}
</Select>
</Form.Item>
{bodyshop.enforce_class && (
<Form.Item
name={"class"}
label={t("jobs.fields.class")}
rules={[
{
required: bodyshop.enforce_class,
//message: t("general.validation.required"),
},
]}
>
<Select>
{bodyshop.md_classes.map((s) => (
<Select.Option key={s} value={s}>
{s}
</Select.Option>
))}
</Select>
</Form.Item>
)}
{bodyshop.enforce_referral && (
<Form.Item
name={"referral_source"}
label={t("jobs.fields.referralsource")}
rules={[
{
required: bodyshop.enforce_referral,
//message: t("general.validation.required"),
},
]}
>
<Select>
{bodyshop.md_referral_sources.map((s) => (
<Select.Option key={s} value={s}>
{s}
</Select.Option>
))}
</Select>
</Form.Item>
)}
<Form.Item
label={t("jobs.fields.ca_gst_registrant")}
name="ca_gst_registrant"
@@ -109,7 +136,7 @@ export function JobsConvertButton({ bodyshop, job, refetch, jobRO }) {
<Switch />
</Form.Item>
<Space wrap>
<Button type="danger" htmlType="submit">
<Button type="danger" onClick={() => form.submit()} loading={loading}>
{t("jobs.actions.convert")}
</Button>
<Button onClick={() => setVisible(false)}>
@@ -129,6 +156,7 @@ export function JobsConvertButton({ bodyshop, job, refetch, jobRO }) {
type="danger"
// style={{ display: job.converted ? "none" : "" }}
disabled={job.converted || jobRO}
loading={loading}
onClick={() => setVisible(true)}
>
{t("jobs.actions.convert")}

View File

@@ -146,9 +146,6 @@ export function JobsCreateJobsInfo({ bodyshop, form, selected }) {
</Collapse.Panel>
<Collapse.Panel key="claim" header={t("menus.jobsdetail.claimdetail")}>
<LayoutFormRow>
<Form.Item label={t("jobs.fields.csr")} name="csr">
<Input />
</Form.Item>
<Form.Item label={t("jobs.fields.loss_desc")} name="loss_desc">
<Input />
</Form.Item>
@@ -298,12 +295,14 @@ export function JobsCreateJobsInfo({ bodyshop, form, selected }) {
<Form.Item label={t("jobs.fields.rate_ma3s")} name="rate_ma3s">
<CurrencyInput />
</Form.Item>
<Form.Item label={t("jobs.fields.rate_mabl")} name="rate_mabl">
<CurrencyInput />
</Form.Item>
<Form.Item label={t("jobs.fields.rate_macs")} name="rate_macs">
<CurrencyInput />
</Form.Item>
{
// <Form.Item label={t("jobs.fields.rate_mabl")} name="rate_mabl">
// <CurrencyInput />
// </Form.Item>
// <Form.Item label={t("jobs.fields.rate_macs")} name="rate_macs">
// <CurrencyInput />
// </Form.Item>
}
<Form.Item label={t("jobs.fields.rate_matd")} name="rate_matd">
<CurrencyInput />
</Form.Item>

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