1
.platform/nginx/conf.d/proxy.conf
Normal file
1
.platform/nginx/conf.d/proxy.conf
Normal file
@@ -0,0 +1 @@
|
||||
client_max_body_size 15M;
|
||||
@@ -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!'
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
24
_reference/New Hasura Deployment.md
Normal file
24
_reference/New Hasura Deployment.md
Normal 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.
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
7963
admin/yarn.lock
7963
admin/yarn.lock
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
25134
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 <></>;
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
@@ -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);
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
|
||||
@@ -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"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
|
||||
@@ -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"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
|
||||
@@ -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"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
|
||||
@@ -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"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 : []}
|
||||
/>
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
@@ -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"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
|
||||
@@ -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"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
|
||||
@@ -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) => (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>;
|
||||
}}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
|
||||
@@ -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);
|
||||
@@ -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>
|
||||
@@ -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}
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -38,7 +38,6 @@ export default function JobCostingStatistics({ summaryData }) {
|
||||
/>
|
||||
<Statistic
|
||||
value={summaryData.gppercentFormatted}
|
||||
suffix="%"
|
||||
title={t("jobs.labels.gppercent")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ export default function JobLinesUpsertModalComponent({
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required"),
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
name="line_desc"
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) => (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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()}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -38,7 +38,6 @@ export default function JobsAdminDatesChange({ job }) {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>{t("jobs.labels.ownerassociation")}</div>
|
||||
<Form
|
||||
onFinish={handleFinish}
|
||||
autoComplete={"off"}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -48,7 +48,7 @@ export default function JobAdminOwnerReassociate({ job }) {
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required"),
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -48,7 +48,7 @@ export default function JobAdminOwnerReassociate({ job }) {
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required"),
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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")) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
|
||||
@@ -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")}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user