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 ..
|
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
|
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"
|
ssh-keygen -t rsa -C "your_email@example.com"
|
||||||
|
|
||||||
Copy the new key to clipboard:
|
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. Create a new user to replace root user
|
||||||
1. # adduser imex
|
1. # adduser imex
|
||||||
@@ -19,7 +20,7 @@ Add the SSH key to the drop creation screen.
|
|||||||
5. $ chmod 700 ~/.ssh
|
5. $ chmod 700 ~/.ssh
|
||||||
6. $ nano ~/.ssh/authorized_keys
|
6. $ nano ~/.ssh/authorized_keys
|
||||||
7. Add the copied SSH key and save.
|
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
|
2. Setup the Firewall
|
||||||
1. $ sudo ufw allow OpenSSH.
|
1. $ sudo ufw allow OpenSSH.
|
||||||
2. $ sudo ufw enable
|
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)
|
2. Nginx Http: Opens only port 80 (normal, unencrypted web traffic)
|
||||||
3. Nginx Https: Opens only port 443 (TLS/SSL encrypted 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.
|
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 -
|
1. $ curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash -
|
||||||
2. $ sudo apt install nodejs
|
2. $ sudo apt install nodejs
|
||||||
3. $ node --version
|
3. $ node --version
|
||||||
7. Clone Source Code
|
5. Clone Source Code
|
||||||
1. $ git clone git@bitbucket.org:snaptsoft/bodyshop.git //Requires SSH setup.
|
1. $ git clone git@bitbucket.org:snaptsoft/bodyshop.git //Requires SSH setup.
|
||||||
2. $ cd bodyshop && npm install //Install all server dependencies.
|
2. $ cd bodyshop && npm install //Install all server dependencies.
|
||||||
8. Setup PM2
|
6. Setup PM2
|
||||||
1. $ npm install pm2 -g //Had to be run as root.
|
1. $ npm install pm2 -g //Had to be run as root.
|
||||||
2. $ pm2 start ecosystem.config.js
|
2. $ pm2 start ecosystem.config.js
|
||||||
3. $ pm2 startup ubuntu //Ensure it starts when server does.
|
3. $ pm2 startup ubuntu //Ensure it starts when server does.
|
||||||
9. Alter Nginx config
|
7. Alter Nginx config
|
||||||
1. sudo nano /etc/nginx/sites-available/default
|
1. sudo nano /etc/nginx/sites-available/default
|
||||||
2. //Add Appropriate server names to the file. www. and non-www.
|
2. //Add Appropriate server names to the file. www. and non-www.
|
||||||
3. Add the following inside the location of the server block:
|
3. Add the following inside the location of the server block: (Remove the 404 bit.)
|
||||||
proxy_pass http://localhost:5000;
|
proxy_pass http://localhost:5000;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
proxy_set_header Connection 'upgrade';
|
proxy_set_header Connection 'upgrade';
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_cache_bypass $http_upgrade;
|
proxy_cache_bypass $http_upgrade;
|
||||||
10. Install Certbot
|
8. Install Certbot
|
||||||
4. $ sudo add-apt-repository ppa:certbot/certbot //Potential issue on ubuntu 20.04
|
9. $ sudo add-apt-repository ppa:certbot/certbot //Potential issue on ubuntu 20.04
|
||||||
5. $ sudo apt-get update
|
10. $ sudo apt-get update
|
||||||
6. $ sudo apt install python-certbot-nginx
|
11. $ sudo apt install python-certbot-nginx
|
||||||
7. $ sudo nano /etc/nginx/sites-available/default
|
12. $ sudo nano /etc/nginx/sites-available/default
|
||||||
8. Find the existing server_name line and replace the underscore with your domain name:
|
13. Find the existing server_name line and replace the underscore with your domain name:
|
||||||
...
|
...
|
||||||
server_name example.com www.example.com;
|
server_name example.com www.example.com;
|
||||||
...
|
...
|
||||||
9. $ sudo nginx -t //Verify syntax.
|
14. $ sudo nginx -t //Verify syntax.
|
||||||
10. $ sudo systemctl reload nginx
|
15. $ sudo systemctl reload nginx
|
||||||
11. Generate Certificate
|
##AWS INSTRUCTIONS
|
||||||
11. $ sudo certbot --nginx -d example.com -d www.example.com //Follow prompts.
|
$ sudo snap install core; sudo snap refresh core
|
||||||
12. $ sudo certbot renew --dry-run //Dry run to test auto renewal.
|
$ 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
|
ADding Yarn
|
||||||
curl -sL https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
|
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
|
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
|
sudo apt-get update && sudo apt-get install yarn
|
||||||
|
|||||||
@@ -3,24 +3,25 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apollo/client": "^3.2.1",
|
"@apollo/client": "^3.3.15",
|
||||||
"@testing-library/jest-dom": "^5.11.4",
|
"@testing-library/jest-dom": "^5.11.10",
|
||||||
"@testing-library/react": "^11.0.4",
|
"@testing-library/react": "^11.2.6",
|
||||||
"@testing-library/user-event": "^12.1.6",
|
"@testing-library/user-event": "^13.1.5",
|
||||||
"@types/prop-types": "^15.7.3",
|
"@types/prop-types": "^15.7.3",
|
||||||
"apollo-boost": "^0.4.9",
|
"apollo-boost": "^0.4.9",
|
||||||
"apollo-link-context": "^1.0.20",
|
"apollo-link-context": "^1.0.20",
|
||||||
"apollo-link-logger": "^1.2.3",
|
"apollo-link-logger": "^2.0.0",
|
||||||
"dotenv": "^8.2.0",
|
"dotenv": "^8.2.0",
|
||||||
"firebase": "^7.21.0",
|
"firebase": "^8.4.1",
|
||||||
"graphql": "^15.4.0",
|
"graphql": "^15.4.0",
|
||||||
"prop-types": "^15.7.2",
|
"prop-types": "^15.7.2",
|
||||||
"ra-data-hasura-graphql": "^0.1.12",
|
"ra-data-hasura-graphql": "^0.1.13",
|
||||||
"react": "^17.0.1",
|
"react": "^17.0.1",
|
||||||
"react-admin": "^3.8.5",
|
"react-admin": "^3.14.4",
|
||||||
"react-dom": "^17.0.1",
|
"react-dom": "^17.0.1",
|
||||||
"react-icons": "^3.11.0",
|
"react-icons": "^4.2.0",
|
||||||
"react-scripts": "4.0.0"
|
"react-scripts": "4.0.3",
|
||||||
|
"sass": "^1.32.10"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "set PORT=3001 && react-scripts start",
|
"start": "set PORT=3001 && react-scripts start",
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
ShowGuesser,
|
ShowGuesser,
|
||||||
} from "react-admin";
|
} from "react-admin";
|
||||||
import { FaFileInvoiceDollar } from "react-icons/fa";
|
import { FaFileInvoiceDollar } from "react-icons/fa";
|
||||||
|
import CircularProgress from "@material-ui/core/CircularProgress";
|
||||||
import { auth } from "../../firebase/admin-firebase-utils";
|
import { auth } from "../../firebase/admin-firebase-utils";
|
||||||
import authProvider from "../auth-provider/auth-provider";
|
import authProvider from "../auth-provider/auth-provider";
|
||||||
import JoblinesCreate from "../joblines/joblines.create";
|
import JoblinesCreate from "../joblines/joblines.create";
|
||||||
@@ -31,6 +32,7 @@ const httpLink = new HttpLink({
|
|||||||
// 'Authorization': `Bearer xxxx`,
|
// 'Authorization': `Bearer xxxx`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const authLink = setContext((_, { headers }) => {
|
const authLink = setContext((_, { headers }) => {
|
||||||
return (
|
return (
|
||||||
auth.currentUser &&
|
auth.currentUser &&
|
||||||
@@ -85,7 +87,11 @@ class AdminRoot extends Component {
|
|||||||
const { dataProvider } = this.state;
|
const { dataProvider } = this.state;
|
||||||
|
|
||||||
if (!dataProvider) {
|
if (!dataProvider) {
|
||||||
return <div>Loading</div>;
|
return (
|
||||||
|
<div>
|
||||||
|
<CircularProgress />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -2,45 +2,26 @@ import React from "react";
|
|||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
import {
|
import {
|
||||||
AutocompleteInput,
|
AutocompleteInput,
|
||||||
|
BooleanInput,
|
||||||
Edit,
|
Edit,
|
||||||
FormTab,
|
FormTab,
|
||||||
NumberInput,
|
NumberInput,
|
||||||
ReferenceInput,
|
ReferenceInput,
|
||||||
SelectInput,
|
SelectInput,
|
||||||
|
SimpleForm,
|
||||||
TabbedForm,
|
TabbedForm,
|
||||||
TextInput,
|
TextInput,
|
||||||
} from "react-admin";
|
} from "react-admin";
|
||||||
|
|
||||||
const JobsEdit = (props) => (
|
const JobsEdit = (props) => (
|
||||||
<Edit {...props}>
|
<Edit {...props}>
|
||||||
<TabbedForm margin="normal" variant="standard">
|
<TabbedForm>
|
||||||
<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>
|
|
||||||
<FormTab label="Job Info">
|
<FormTab label="Job Info">
|
||||||
<div
|
<SimpleForm>
|
||||||
style={{
|
|
||||||
columns: "3 auto",
|
|
||||||
// display: "flex",
|
|
||||||
width: "100%",
|
|
||||||
// justifyContent: "space-around",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ReferenceInput label="Shopid" source="shopid" reference="bodyshops">
|
<ReferenceInput label="Shopid" source="shopid" reference="bodyshops">
|
||||||
<SelectInput disabled optionText="shopname" />
|
<SelectInput disabled optionText="shopname" />
|
||||||
</ReferenceInput>
|
</ReferenceInput>
|
||||||
<TextInput fullWidth source="ro_number" />
|
<TextInput source="ro_number" />
|
||||||
<ReferenceInput label="Owner ID" source="ownerid" reference="owners">
|
<ReferenceInput label="Owner ID" source="ownerid" reference="owners">
|
||||||
<AutocompleteInput
|
<AutocompleteInput
|
||||||
matchSuggestion={(filter, choice) =>
|
matchSuggestion={(filter, choice) =>
|
||||||
@@ -64,7 +45,7 @@ const JobsEdit = (props) => (
|
|||||||
<ReferenceInput label="Shopid" source="shopid" reference="bodyshops">
|
<ReferenceInput label="Shopid" source="shopid" reference="bodyshops">
|
||||||
<SelectInput disabled optionText="shopname" />
|
<SelectInput disabled optionText="shopname" />
|
||||||
</ReferenceInput>
|
</ReferenceInput>
|
||||||
<TextInput fullWidth source="ro_number" />
|
<TextInput source="ro_number" />
|
||||||
<ReferenceInput label="Owner ID" source="ownerid" reference="owners">
|
<ReferenceInput label="Owner ID" source="ownerid" reference="owners">
|
||||||
<AutocompleteInput
|
<AutocompleteInput
|
||||||
matchSuggestion={(filter, choice) =>
|
matchSuggestion={(filter, choice) =>
|
||||||
@@ -85,254 +66,248 @@ const JobsEdit = (props) => (
|
|||||||
>
|
>
|
||||||
<SelectInput optionText="v_vin" />
|
<SelectInput optionText="v_vin" />
|
||||||
</ReferenceInput>
|
</ReferenceInput>
|
||||||
<TextInput fullWidth source="inproduction" />
|
<BooleanInput source="inproduction" />
|
||||||
<TextInput fullWidth source="converted" />
|
<BooleanInput source="converted" />
|
||||||
</div>
|
<TextInput disabled source="id" />
|
||||||
|
<TextInput disabled source="created_at" />
|
||||||
|
<TextInput disabled source="updated_at" />
|
||||||
|
</SimpleForm>
|
||||||
</FormTab>
|
</FormTab>
|
||||||
|
|
||||||
<FormTab label="Labor Rates">
|
<FormTab label="Labor Rates">
|
||||||
<div
|
<NumberInput source="labor_rate_id" />
|
||||||
style={{
|
<NumberInput source="labor_rate_desc" />
|
||||||
columns: "3 auto",
|
<NumberInput source="rate_lab" />
|
||||||
// display: "flex",
|
<NumberInput source="rate_lad" />
|
||||||
width: "100%",
|
<NumberInput source="rate_lae" />
|
||||||
// justifyContent: "space-around",
|
<NumberInput source="rate_lar" />
|
||||||
}}
|
<NumberInput source="rate_las" />
|
||||||
>
|
<NumberInput source="rate_laf" />
|
||||||
<NumberInput fullWidth source="labor_rate_id" />
|
<NumberInput source="rate_lam" />
|
||||||
<NumberInput fullWidth source="labor_rate_desc" />
|
<NumberInput source="rate_lag" />
|
||||||
<NumberInput fullWidth source="rate_lab" />
|
<NumberInput source="rate_atp" />
|
||||||
<NumberInput fullWidth source="rate_lad" />
|
<NumberInput source="rate_lau" />
|
||||||
<NumberInput fullWidth source="rate_lae" />
|
<NumberInput source="rate_la1" />
|
||||||
<NumberInput fullWidth source="rate_lar" />
|
<NumberInput source="rate_la2" />
|
||||||
<NumberInput fullWidth source="rate_las" />
|
<NumberInput source="rate_la3" />
|
||||||
<NumberInput fullWidth source="rate_laf" />
|
<NumberInput source="rate_la4" />
|
||||||
<NumberInput fullWidth source="rate_lam" />
|
<NumberInput source="rate_mapa" />
|
||||||
<NumberInput fullWidth source="rate_lag" />
|
<NumberInput source="rate_mash" />
|
||||||
<NumberInput fullWidth source="rate_atp" />
|
<NumberInput source="rate_mahw" />
|
||||||
<NumberInput fullWidth source="rate_lau" />
|
<NumberInput source="rate_ma2s" />
|
||||||
<NumberInput fullWidth source="rate_la1" />
|
<NumberInput source="rate_ma3s" />
|
||||||
<NumberInput fullWidth source="rate_la2" />
|
<NumberInput source="rate_ma2t" />
|
||||||
<NumberInput fullWidth source="rate_la3" />
|
<NumberInput source="rate_mabl" />
|
||||||
<NumberInput fullWidth source="rate_la4" />
|
<NumberInput source="rate_macs" />
|
||||||
<NumberInput fullWidth source="rate_mapa" />
|
<NumberInput source="rate_matd" />
|
||||||
<NumberInput fullWidth source="rate_mash" />
|
<NumberInput source="federal_tax_rate" />
|
||||||
<NumberInput fullWidth source="rate_mahw" />
|
<NumberInput source="state_tax_rate" />
|
||||||
<NumberInput fullWidth source="rate_ma2s" />
|
<NumberInput source="local_tax_rate" />
|
||||||
<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>
|
|
||||||
</FormTab>
|
</FormTab>
|
||||||
<FormTab label="Dates">
|
<FormTab label="Dates">
|
||||||
<TextInput fullWidth source="scheduled_in" />
|
<TextInput source="scheduled_in" />
|
||||||
<TextInput fullWidth source="actual_in" />
|
<TextInput source="actual_in" />
|
||||||
<TextInput fullWidth source="scheduled_completion" />
|
<TextInput source="scheduled_completion" />
|
||||||
<TextInput fullWidth source="actual_completion" />
|
<TextInput source="actual_completion" />
|
||||||
<TextInput fullWidth source="scheduled_delivery" />
|
<TextInput source="scheduled_delivery" />
|
||||||
<TextInput fullWidth source="actual_delivery" />
|
<TextInput source="actual_delivery" />
|
||||||
<TextInput fullWidth source="invoice_date" />
|
<TextInput source="invoice_date" />
|
||||||
<TextInput fullWidth source="date_estimated" />
|
<TextInput source="date_estimated" />
|
||||||
<TextInput fullWidth source="date_open" />
|
<TextInput source="date_open" />
|
||||||
<TextInput fullWidth source="date_scheduled" />
|
<TextInput source="date_scheduled" />
|
||||||
<TextInput fullWidth source="date_invoiced" />
|
<TextInput source="date_invoiced" />
|
||||||
<TextInput fullWidth source="date_exported" />
|
<TextInput source="date_exported" />
|
||||||
</FormTab>
|
</FormTab>
|
||||||
<FormTab label="Insurance info">
|
<FormTab label="Insurance info">
|
||||||
<TextInput fullWidth source="est_co_nm" />
|
<TextInput source="est_co_nm" />
|
||||||
<TextInput fullWidth source="est_addr1" />
|
<TextInput source="est_addr1" />
|
||||||
<TextInput fullWidth source="est_addr2" />
|
<TextInput source="est_addr2" />
|
||||||
<TextInput fullWidth source="est_city" />
|
<TextInput source="est_city" />
|
||||||
<TextInput fullWidth source="est_st" />
|
<TextInput source="est_st" />
|
||||||
<TextInput fullWidth source="est_zip" />
|
<TextInput source="est_zip" />
|
||||||
<TextInput fullWidth source="est_ctry" />
|
<TextInput source="est_ctry" />
|
||||||
<TextInput fullWidth source="est_ph1" />
|
<TextInput source="est_ph1" />
|
||||||
<TextInput fullWidth source="est_ea" />
|
<TextInput source="est_ea" />
|
||||||
<TextInput fullWidth source="est_ct_ln" />
|
<TextInput source="est_ct_ln" />
|
||||||
<TextInput fullWidth source="est_ct_fn" />
|
<TextInput source="est_ct_fn" />
|
||||||
<TextInput fullWidth source="regie_number" />
|
<TextInput source="regie_number" />
|
||||||
<TextInput fullWidth source="statusid" />
|
<TextInput source="statusid" />
|
||||||
<TextInput fullWidth source="ins_co_id" />
|
<TextInput source="ins_co_id" />
|
||||||
<TextInput fullWidth source="ins_co_nm" />
|
<TextInput source="ins_co_nm" />
|
||||||
<TextInput fullWidth source="ins_addr1" />
|
<TextInput source="ins_addr1" />
|
||||||
<TextInput fullWidth source="ins_addr2" />
|
<TextInput source="ins_addr2" />
|
||||||
<TextInput fullWidth source="ins_city" />
|
<TextInput source="ins_city" />
|
||||||
<TextInput fullWidth source="ins_st" />
|
<TextInput source="ins_st" />
|
||||||
<TextInput fullWidth source="ins_zip" />
|
<TextInput source="ins_zip" />
|
||||||
<TextInput fullWidth source="ins_ctry" />
|
<TextInput source="ins_ctry" />
|
||||||
<TextInput fullWidth source="ins_ph1" />
|
<TextInput source="ins_ph1" />
|
||||||
<TextInput fullWidth source="ins_ph1x" />
|
<TextInput source="ins_ph1x" />
|
||||||
<TextInput fullWidth source="ins_ph2" />
|
<TextInput source="ins_ph2" />
|
||||||
<TextInput fullWidth source="ins_ph2x" />
|
<TextInput source="ins_ph2x" />
|
||||||
<TextInput fullWidth source="ins_fax" />
|
<TextInput source="ins_fax" />
|
||||||
<TextInput fullWidth source="ins_faxx" />
|
<TextInput source="ins_faxx" />
|
||||||
<TextInput fullWidth source="ins_ct_ln" />
|
<TextInput source="ins_ct_ln" />
|
||||||
<TextInput fullWidth source="ins_ct_fn" />
|
<TextInput source="ins_ct_fn" />
|
||||||
<TextInput fullWidth source="ins_title" />
|
<TextInput source="ins_title" />
|
||||||
<TextInput fullWidth source="ins_ct_ph" />
|
<TextInput source="ins_ct_ph" />
|
||||||
<TextInput fullWidth source="ins_ct_phx" />
|
<TextInput source="ins_ct_phx" />
|
||||||
<TextInput fullWidth source="ins_ea" />
|
<TextInput source="ins_ea" />
|
||||||
<TextInput fullWidth source="ins_memo" />
|
<TextInput source="ins_memo" />
|
||||||
<TextInput fullWidth source="policy_no" />
|
<TextInput source="policy_no" />
|
||||||
<TextInput fullWidth source="ded_amt" />
|
<TextInput source="ded_amt" />
|
||||||
<TextInput fullWidth source="ded_status" />
|
<TextInput source="ded_status" />
|
||||||
<TextInput fullWidth source="asgn_no" />
|
<TextInput source="asgn_no" />
|
||||||
<TextInput fullWidth source="asgn_date" />
|
<TextInput source="asgn_date" />
|
||||||
<TextInput fullWidth source="asgn_type" />
|
<TextInput source="asgn_type" />
|
||||||
<TextInput fullWidth source="clm_no" />
|
<TextInput source="clm_no" />
|
||||||
<TextInput fullWidth source="clm_ofc_id" />
|
<TextInput source="clm_ofc_id" />
|
||||||
<TextInput fullWidth source="agt_co_id" />
|
<TextInput source="agt_co_id" />
|
||||||
<TextInput fullWidth source="agt_co_nm" />
|
<TextInput source="agt_co_nm" />
|
||||||
<TextInput fullWidth source="agt_addr1" />
|
<TextInput source="agt_addr1" />
|
||||||
<TextInput fullWidth source="agt_addr2" />
|
<TextInput source="agt_addr2" />
|
||||||
<TextInput fullWidth source="agt_city" />
|
<TextInput source="agt_city" />
|
||||||
<TextInput fullWidth source="agt_st" />
|
<TextInput source="agt_st" />
|
||||||
<TextInput fullWidth source="agt_zip" />
|
<TextInput source="agt_zip" />
|
||||||
<TextInput fullWidth source="agt_ctry" />
|
<TextInput source="agt_ctry" />
|
||||||
<TextInput fullWidth source="agt_ph1" />
|
<TextInput source="agt_ph1" />
|
||||||
<TextInput fullWidth source="agt_ph1x" />
|
<TextInput source="agt_ph1x" />
|
||||||
<TextInput fullWidth source="agt_ph2" />
|
<TextInput source="agt_ph2" />
|
||||||
<TextInput fullWidth source="agt_ph2x" />
|
<TextInput source="agt_ph2x" />
|
||||||
<TextInput fullWidth source="agt_fax" />
|
<TextInput source="agt_fax" />
|
||||||
<TextInput fullWidth source="agt_faxx" />
|
<TextInput source="agt_faxx" />
|
||||||
<TextInput fullWidth source="agt_ct_ln" />
|
<TextInput source="agt_ct_ln" />
|
||||||
<TextInput fullWidth source="agt_ct_fn" />
|
<TextInput source="agt_ct_fn" />
|
||||||
<TextInput fullWidth source="agt_ct_ph" />
|
<TextInput source="agt_ct_ph" />
|
||||||
<TextInput fullWidth source="agt_ct_phx" />
|
<TextInput source="agt_ct_phx" />
|
||||||
<TextInput fullWidth source="agt_ea" />
|
<TextInput source="agt_ea" />
|
||||||
<TextInput fullWidth source="agt_lic_no" />
|
<TextInput source="agt_lic_no" />
|
||||||
<TextInput fullWidth source="loss_type" />
|
<TextInput source="loss_type" />
|
||||||
<TextInput fullWidth source="loss_desc" />
|
<TextInput source="loss_desc" />
|
||||||
<TextInput fullWidth source="theft_ind" />
|
<TextInput source="theft_ind" />
|
||||||
<TextInput fullWidth source="cat_no" />
|
<TextInput source="cat_no" />
|
||||||
<TextInput fullWidth source="tlos_ind" />
|
<TextInput source="tlos_ind" />
|
||||||
<TextInput fullWidth source="ciecaid" />
|
<TextInput source="ciecaid" />
|
||||||
<TextInput fullWidth source="loss_date" />
|
<TextInput source="loss_date" />
|
||||||
<TextInput fullWidth source="clm_ofc_nm" />
|
<TextInput source="clm_ofc_nm" />
|
||||||
<TextInput fullWidth source="clm_addr1" />
|
<TextInput source="clm_addr1" />
|
||||||
<TextInput fullWidth source="clm_addr2" />
|
<TextInput source="clm_addr2" />
|
||||||
<TextInput fullWidth source="clm_city" />
|
<TextInput source="clm_city" />
|
||||||
<TextInput fullWidth source="clm_st" />
|
<TextInput source="clm_st" />
|
||||||
<TextInput fullWidth source="clm_zip" />
|
<TextInput source="clm_zip" />
|
||||||
<TextInput fullWidth source="clm_ctry" />
|
<TextInput source="clm_ctry" />
|
||||||
<TextInput fullWidth source="clm_ph1" />
|
<TextInput source="clm_ph1" />
|
||||||
<TextInput fullWidth source="clm_ph1x" />
|
<TextInput source="clm_ph1x" />
|
||||||
<TextInput fullWidth source="clm_ph2" />
|
<TextInput source="clm_ph2" />
|
||||||
<TextInput fullWidth source="clm_ph2x" />
|
<TextInput source="clm_ph2x" />
|
||||||
<TextInput fullWidth source="clm_fax" />
|
<TextInput source="clm_fax" />
|
||||||
<TextInput fullWidth source="clm_faxx" />
|
<TextInput source="clm_faxx" />
|
||||||
<TextInput fullWidth source="clm_ct_ln" />
|
<TextInput source="clm_ct_ln" />
|
||||||
<TextInput fullWidth source="clm_ct_fn" />
|
<TextInput source="clm_ct_fn" />
|
||||||
<TextInput fullWidth source="clm_title" />
|
<TextInput source="clm_title" />
|
||||||
<TextInput fullWidth source="clm_ct_ph" />
|
<TextInput source="clm_ct_ph" />
|
||||||
<TextInput fullWidth source="clm_ct_phx" />
|
<TextInput source="clm_ct_phx" />
|
||||||
<TextInput fullWidth source="clm_ea" />
|
<TextInput source="clm_ea" />
|
||||||
<TextInput fullWidth source="payee_nms" />
|
<TextInput source="payee_nms" />
|
||||||
<TextInput fullWidth source="pay_type" />
|
<TextInput source="pay_type" />
|
||||||
<TextInput fullWidth source="pay_date" />
|
<TextInput source="pay_date" />
|
||||||
<TextInput fullWidth source="pay_chknm" />
|
<TextInput source="pay_chknm" />
|
||||||
<TextInput fullWidth source="pay_amt" />
|
<TextInput source="pay_amt" />
|
||||||
</FormTab>
|
</FormTab>
|
||||||
<FormTab label="Owner Data on Job">
|
<FormTab label="Owner Data on Job">
|
||||||
<TextInput fullWidth source="cust_pr" />
|
<TextInput source="cust_pr" />
|
||||||
<TextInput fullWidth source="insd_ln" />
|
<TextInput source="insd_ln" />
|
||||||
<TextInput fullWidth source="insd_fn" />
|
<TextInput source="insd_fn" />
|
||||||
<TextInput fullWidth source="insd_title" />
|
<TextInput source="insd_title" />
|
||||||
<TextInput fullWidth source="insd_co_nm" />
|
<TextInput source="insd_co_nm" />
|
||||||
<TextInput fullWidth source="insd_addr1" />
|
<TextInput source="insd_addr1" />
|
||||||
<TextInput fullWidth source="insd_addr2" />
|
<TextInput source="insd_addr2" />
|
||||||
<TextInput fullWidth source="insd_city" />
|
<TextInput source="insd_city" />
|
||||||
<TextInput fullWidth source="insd_st" />
|
<TextInput source="insd_st" />
|
||||||
<TextInput fullWidth source="insd_zip" />
|
<TextInput source="insd_zip" />
|
||||||
<TextInput fullWidth source="insd_ctry" />
|
<TextInput source="insd_ctry" />
|
||||||
<TextInput fullWidth source="insd_ph1" />
|
<TextInput source="insd_ph1" />
|
||||||
<TextInput fullWidth source="insd_ph1x" />
|
<TextInput source="insd_ph1x" />
|
||||||
<TextInput fullWidth source="insd_ph2" />
|
<TextInput source="insd_ph2" />
|
||||||
<TextInput fullWidth source="insd_ph2x" />
|
<TextInput source="insd_ph2x" />
|
||||||
<TextInput fullWidth source="insd_fax" />
|
<TextInput source="insd_fax" />
|
||||||
<TextInput fullWidth source="insd_faxx" />
|
<TextInput source="insd_faxx" />
|
||||||
<TextInput fullWidth source="insd_ea" />
|
<TextInput source="insd_ea" />
|
||||||
<TextInput fullWidth source="ownr_ln" />
|
<TextInput source="ownr_ln" />
|
||||||
<TextInput fullWidth source="ownr_fn" />
|
<TextInput source="ownr_fn" />
|
||||||
<TextInput fullWidth source="ownr_title" />
|
<TextInput source="ownr_title" />
|
||||||
<TextInput fullWidth source="ownr_co_nm" />
|
<TextInput source="ownr_co_nm" />
|
||||||
<TextInput fullWidth source="ownr_addr1" />
|
<TextInput source="ownr_addr1" />
|
||||||
<TextInput fullWidth source="ownr_addr2" />
|
<TextInput source="ownr_addr2" />
|
||||||
<TextInput fullWidth source="ownr_city" />
|
<TextInput source="ownr_city" />
|
||||||
<TextInput fullWidth source="ownr_st" />
|
<TextInput source="ownr_st" />
|
||||||
<TextInput fullWidth source="ownr_zip" />
|
<TextInput source="ownr_zip" />
|
||||||
<TextInput fullWidth source="ownr_ctry" />
|
<TextInput source="ownr_ctry" />
|
||||||
<TextInput fullWidth source="ownr_ph1" />
|
<TextInput source="ownr_ph1" />
|
||||||
<TextInput fullWidth source="ownr_ph1x" />
|
<TextInput source="ownr_ph1x" />
|
||||||
<TextInput fullWidth source="ownr_ph2" />
|
<TextInput source="ownr_ph2" />
|
||||||
<TextInput fullWidth source="ownr_ph2x" />
|
<TextInput source="ownr_ph2x" />
|
||||||
<TextInput fullWidth source="ownr_fax" />
|
<TextInput source="ownr_fax" />
|
||||||
<TextInput fullWidth source="ownr_faxx" />
|
<TextInput source="ownr_faxx" />
|
||||||
<TextInput fullWidth source="ownr_ea" />
|
<TextInput source="ownr_ea" />
|
||||||
</FormTab>
|
</FormTab>
|
||||||
<FormTab label="Financial">
|
<FormTab label="Financial">
|
||||||
<TextInput fullWidth source="clm_total" />
|
<TextInput source="clm_total" />
|
||||||
<TextInput fullWidth source="owner_owing" />
|
<TextInput source="owner_owing" />
|
||||||
</FormTab>
|
</FormTab>
|
||||||
<FormTab label="Other">
|
<FormTab label="Other">
|
||||||
<TextInput fullWidth source="area_of_damage" />
|
<TextInput source="area_of_damage" />
|
||||||
<TextInput fullWidth source="loss_cat" />
|
<TextInput source="loss_cat" />
|
||||||
<TextInput fullWidth source="special_coverage_policy" />
|
<TextInput source="special_coverage_policy" />
|
||||||
<TextInput fullWidth source="csr" />
|
<TextInput source="csr" />
|
||||||
<TextInput fullWidth source="po_number" />
|
<TextInput source="po_number" />
|
||||||
<TextInput fullWidth source="unit_number" />
|
<TextInput source="unit_number" />
|
||||||
<TextInput fullWidth source="kmin" />
|
<TextInput source="kmin" />
|
||||||
<TextInput fullWidth source="kmout" />
|
<TextInput source="kmout" />
|
||||||
<TextInput fullWidth source="referral_source" />
|
<TextInput source="referral_source" />
|
||||||
<TextInput fullWidth source="selling_dealer" />
|
<TextInput source="selling_dealer" />
|
||||||
<TextInput fullWidth source="servicing_dealer" />
|
<TextInput source="servicing_dealer" />
|
||||||
<TextInput fullWidth source="servicing_dealer_contact" />
|
<TextInput source="servicing_dealer_contact" />
|
||||||
<TextInput fullWidth source="selling_dealer_contact" />
|
<TextInput source="selling_dealer_contact" />
|
||||||
<TextInput fullWidth source="depreciation_taxes" />
|
<TextInput source="depreciation_taxes" />
|
||||||
<TextInput fullWidth source="federal_tax_payable" />
|
<TextInput source="federal_tax_payable" />
|
||||||
<TextInput fullWidth source="other_amount_payable" />
|
<TextInput source="other_amount_payable" />
|
||||||
<TextInput fullWidth source="towing_payable" />
|
<TextInput source="towing_payable" />
|
||||||
<TextInput fullWidth source="storage_payable" />
|
<TextInput source="storage_payable" />
|
||||||
<TextInput fullWidth source="adjustment_bottom_line" />
|
<TextInput source="adjustment_bottom_line" />
|
||||||
<TextInput fullWidth source="tax_pstthr" />
|
<TextInput source="tax_pstthr" />
|
||||||
<TextInput fullWidth source="tax_tow_rt" />
|
<TextInput source="tax_tow_rt" />
|
||||||
<TextInput fullWidth source="tax_sub_rt" />
|
<TextInput source="tax_sub_rt" />
|
||||||
<TextInput fullWidth source="tax_paint_mat_rt" />
|
<TextInput source="tax_paint_mat_rt" />
|
||||||
<TextInput fullWidth source="tax_levies_rt" />
|
<TextInput source="tax_levies_rt" />
|
||||||
<TextInput fullWidth source="tax_prethr" />
|
<TextInput source="tax_prethr" />
|
||||||
<TextInput fullWidth source="tax_thramt" />
|
<TextInput source="tax_thramt" />
|
||||||
<TextInput fullWidth source="tax_str_rt" />
|
<TextInput source="tax_str_rt" />
|
||||||
<TextInput fullWidth source="tax_lbr_rt" />
|
<TextInput source="tax_lbr_rt" />
|
||||||
<TextInput fullWidth source="adj_g_disc" />
|
<TextInput source="adj_g_disc" />
|
||||||
<TextInput fullWidth source="adj_towdis" />
|
<TextInput source="adj_towdis" />
|
||||||
<TextInput fullWidth source="adj_strdis" />
|
<TextInput source="adj_strdis" />
|
||||||
<TextInput fullWidth source="tax_predis" />
|
<TextInput source="tax_predis" />
|
||||||
<TextInput fullWidth source="rate_laa" />
|
<TextInput source="rate_laa" />
|
||||||
<TextInput fullWidth source="status" />
|
<TextInput source="status" />
|
||||||
<TextInput fullWidth source="cieca_stl" />
|
<TextInput source="cieca_stl" />
|
||||||
<TextInput fullWidth source="g_bett_amt" />
|
<TextInput source="g_bett_amt" />
|
||||||
<TextInput fullWidth source="cieca_ttl" />
|
<TextInput source="cieca_ttl" />
|
||||||
<TextInput fullWidth source="plate_no" />
|
<TextInput source="plate_no" />
|
||||||
<TextInput fullWidth source="plate_st" />
|
<TextInput source="plate_st" />
|
||||||
<TextInput fullWidth source="v_vin" />
|
<TextInput source="v_vin" />
|
||||||
<TextInput fullWidth source="v_model_yr" />
|
<TextInput source="v_model_yr" />
|
||||||
<TextInput fullWidth source="v_model_desc" />
|
<TextInput source="v_model_desc" />
|
||||||
<TextInput fullWidth source="v_make_desc" />
|
<TextInput source="v_make_desc" />
|
||||||
<TextInput fullWidth source="v_color" />
|
<TextInput source="v_color" />
|
||||||
<TextInput fullWidth source="parts_tax_rates" />
|
<TextInput source="parts_tax_rates" />
|
||||||
<TextInput fullWidth source="job_totals" />
|
<TextInput source="job_totals" />
|
||||||
<TextInput fullWidth source="production_vars" />
|
<TextInput source="production_vars" />
|
||||||
<TextInput fullWidth source="intakechecklist" />
|
<TextInput source="intakechecklist" />
|
||||||
<TextInput fullWidth source="invoice_allocation" />
|
<TextInput source="invoice_allocation" />
|
||||||
<TextInput fullWidth source="kanbanparent" />
|
<TextInput source="kanbanparent" />
|
||||||
<TextInput fullWidth source="employee_body" />
|
<TextInput source="employee_body" />
|
||||||
<TextInput fullWidth source="employee_refinish" />
|
<TextInput source="employee_refinish" />
|
||||||
<TextInput fullWidth source="employee_prep" />
|
<TextInput source="employee_prep" />
|
||||||
</FormTab>
|
</FormTab>
|
||||||
</TabbedForm>
|
</TabbedForm>
|
||||||
</Edit>
|
</Edit>
|
||||||
|
|||||||
@@ -8,31 +8,35 @@ import {
|
|||||||
ReferenceField,
|
ReferenceField,
|
||||||
SelectInput,
|
SelectInput,
|
||||||
TextField,
|
TextField,
|
||||||
TextInput
|
TextInput,
|
||||||
} from "react-admin";
|
} from "react-admin";
|
||||||
import { QUERY_ALL_SHOPS } from "../../graphql/admin.shop.queries";
|
import { QUERY_ALL_SHOPS } from "../../graphql/admin.shop.queries";
|
||||||
|
|
||||||
const JobsList = (props) => (
|
const JobsList = (props) => (
|
||||||
<List filters={<JobsFilter />} {...props}>
|
<List filters={<JobsFilter />} {...props}>
|
||||||
<Datagrid rowClick="edit">
|
<Datagrid rowClick="edit">
|
||||||
<TextField source="id" />
|
<TextField source="id" label="Job ID" />
|
||||||
<ReferenceField source="shopid" reference="bodyshops">
|
<ReferenceField source="shopid" reference="bodyshops" label="Shop Name">
|
||||||
<TextField source="shopname" />
|
<TextField source="shopname" />
|
||||||
</ReferenceField>
|
</ReferenceField>
|
||||||
<TextField source="ro_number" />
|
<TextField source="ro_number" label="RO Number" />
|
||||||
|
|
||||||
<TextField source="ownr_fn" />
|
<TextField source="ownr_fn" label="Owner FN" />
|
||||||
<TextField source="ownr_ln" />
|
<TextField source="ownr_ln" label="Owner LN" />
|
||||||
<TextField source="ownr_co_nm" />
|
<TextField source="ownr_co_nm" label="Owner CO" />
|
||||||
|
|
||||||
<ReferenceField source="ownerid" reference="owners">
|
<ReferenceField source="ownerid" reference="owners" label="Owner Record">
|
||||||
<TextField source="id" />
|
<TextField source="id" />
|
||||||
</ReferenceField>
|
</ReferenceField>
|
||||||
<TextField source="v_model_yr" />
|
<TextField source="v_model_yr" label="Year" />
|
||||||
<TextField source="v_make_desc" />
|
<TextField source="v_make_desc" label="Make" />
|
||||||
<TextField source="v_model_desc" />
|
<TextField source="v_model_desc" label="Model" />
|
||||||
|
|
||||||
<ReferenceField source="vehicleid" reference="vehicles">
|
<ReferenceField
|
||||||
|
source="vehicleid"
|
||||||
|
reference="vehicles"
|
||||||
|
label="Vehicle ID"
|
||||||
|
>
|
||||||
<TextField source="id" />
|
<TextField source="id" />
|
||||||
</ReferenceField>
|
</ReferenceField>
|
||||||
</Datagrid>
|
</Datagrid>
|
||||||
@@ -47,13 +51,13 @@ const JobsFilter = (props) => {
|
|||||||
return (
|
return (
|
||||||
<Filter {...props}>
|
<Filter {...props}>
|
||||||
<TextInput label="RO Number" source="ro_number" />
|
<TextInput label="RO Number" source="ro_number" />
|
||||||
|
<TextInput label="Job ID" source="id" />
|
||||||
<SelectInput
|
<SelectInput
|
||||||
source="shopid"
|
source="shopid"
|
||||||
|
label="Bodyshop"
|
||||||
choices={data.bodyshops.map((b) => {
|
choices={data.bodyshops.map((b) => {
|
||||||
return { id: b.id, name: b.shopname };
|
return { id: b.id, name: b.shopname };
|
||||||
})}
|
})}
|
||||||
alwaysOn
|
|
||||||
allowEmpty={false}
|
|
||||||
/>
|
/>
|
||||||
</Filter>
|
</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,
|
"private": true,
|
||||||
"proxy": "http://localhost:5000",
|
"proxy": "http://localhost:5000",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apollo/client": "^3.3.11",
|
"@apollo/client": "^3.3.14",
|
||||||
"@craco/craco": "^6.1.1",
|
"@craco/craco": "^6.1.1",
|
||||||
"@fingerprintjs/fingerprintjs": "^3.0.6",
|
"@fingerprintjs/fingerprintjs": "^3.1.0",
|
||||||
"@lourenci/react-kanban": "^2.1.0",
|
"@lourenci/react-kanban": "^2.1.0",
|
||||||
"@sentry/react": "^6.2.0",
|
"@sentry/react": "^6.2.5",
|
||||||
"@sentry/tracing": "^6.2.0",
|
"@sentry/tracing": "^6.2.5",
|
||||||
"@stripe/react-stripe-js": "^1.4.0",
|
"@stripe/react-stripe-js": "^1.4.0",
|
||||||
"@stripe/stripe-js": "^1.12.1",
|
"@stripe/stripe-js": "^1.12.1",
|
||||||
"@tanem/react-nprogress": "^3.0.57",
|
"@tanem/react-nprogress": "^3.0.62",
|
||||||
"antd": "^4.13.1",
|
"antd": "^4.15.1",
|
||||||
"apollo-link-logger": "^2.0.0",
|
"apollo-link-logger": "^2.0.0",
|
||||||
"axios": "^0.21.1",
|
"axios": "^0.21.1",
|
||||||
"craco-less": "^1.17.1",
|
"craco-less": "^1.17.1",
|
||||||
"dinero.js": "^1.8.1",
|
"dinero.js": "^1.8.1",
|
||||||
"dotenv": "^8.2.0",
|
"dotenv": "^8.2.0",
|
||||||
"firebase": "^8.2.10",
|
"env-cmd": "^10.1.0",
|
||||||
|
"firebase": "^8.4.1",
|
||||||
"graphql": "^15.5.0",
|
"graphql": "^15.5.0",
|
||||||
"i18next": "^19.8.9",
|
"i18next": "^20.2.1",
|
||||||
"i18next-browser-languagedetector": "^6.0.1",
|
"i18next-browser-languagedetector": "^6.0.1",
|
||||||
"jsoneditor": "^9.1.10",
|
"jsoneditor": "^9.3.1",
|
||||||
"jsreport-browser-client-dist": "^1.3.0",
|
"jsreport-browser-client-dist": "^1.3.0",
|
||||||
"libphonenumber-js": "^1.9.12",
|
"libphonenumber-js": "^1.9.16",
|
||||||
"logrocket": "^1.0.13",
|
"logrocket": "^1.0.15",
|
||||||
"moment-business-days": "^1.2.0",
|
"moment-business-days": "^1.2.0",
|
||||||
"phone": "^2.4.21",
|
"phone": "^2.4.21",
|
||||||
"preval.macro": "^5.0.0",
|
"preval.macro": "^5.0.0",
|
||||||
"prop-types": "^15.7.2",
|
"prop-types": "^15.7.2",
|
||||||
"query-string": "^6.14.0",
|
"query-string": "^7.0.0",
|
||||||
"react": "^17.0.1",
|
"react": "^17.0.1",
|
||||||
"react-big-calendar": "^0.32.0",
|
"react-big-calendar": "^0.33.2",
|
||||||
"react-color": "^2.19.3",
|
"react-color": "^2.19.3",
|
||||||
"react-dom": "^17.0.1",
|
"react-dom": "^17.0.1",
|
||||||
"react-drag-listview": "^0.1.8",
|
"react-drag-listview": "^0.1.8",
|
||||||
"react-grid-gallery": "^0.5.5",
|
"react-grid-gallery": "^0.5.5",
|
||||||
"react-i18next": "^11.8.9",
|
"react-i18next": "^11.8.13",
|
||||||
"react-icons": "^4.2.0",
|
"react-icons": "^4.2.0",
|
||||||
"react-number-format": "^4.4.4",
|
"react-number-format": "^4.5.5",
|
||||||
"react-redux": "^7.2.2",
|
"react-redux": "^7.2.2",
|
||||||
"react-resizable": "^1.11.1",
|
"react-resizable": "^1.11.1",
|
||||||
"react-router-dom": "^5.2.0",
|
"react-router-dom": "^5.2.0",
|
||||||
@@ -53,26 +54,27 @@
|
|||||||
"redux-state-sync": "^3.1.2",
|
"redux-state-sync": "^3.1.2",
|
||||||
"reselect": "^4.0.0",
|
"reselect": "^4.0.0",
|
||||||
"sass": "^1.32.8",
|
"sass": "^1.32.8",
|
||||||
"styled-components": "^5.2.0",
|
"styled-components": "^5.2.3",
|
||||||
"subscriptions-transport-ws": "^0.9.18",
|
"subscriptions-transport-ws": "^0.9.18",
|
||||||
"web-vitals": "^0.2.4",
|
"web-vitals": "^1.1.1",
|
||||||
"workbox-background-sync": "^5.1.3",
|
"workbox-background-sync": "^6.1.5",
|
||||||
"workbox-broadcast-update": "^5.1.3",
|
"workbox-broadcast-update": "^6.1.5",
|
||||||
"workbox-cacheable-response": "^5.1.3",
|
"workbox-cacheable-response": "^6.1.5",
|
||||||
"workbox-core": "^5.1.3",
|
"workbox-core": "^6.1.5",
|
||||||
"workbox-expiration": "^5.1.3",
|
"workbox-expiration": "^6.1.5",
|
||||||
"workbox-google-analytics": "^5.1.3",
|
"workbox-google-analytics": "^6.1.5",
|
||||||
"workbox-navigation-preload": "^5.1.3",
|
"workbox-navigation-preload": "^6.1.5",
|
||||||
"workbox-precaching": "^5.1.3",
|
"workbox-precaching": "^6.1.5",
|
||||||
"workbox-range-requests": "^5.1.3",
|
"workbox-range-requests": "^6.1.5",
|
||||||
"workbox-routing": "^5.1.3",
|
"workbox-routing": "^6.1.5",
|
||||||
"workbox-strategies": "^5.1.3",
|
"workbox-strategies": "^6.1.5",
|
||||||
"workbox-streams": "^5.1.3"
|
"workbox-streams": "^6.1.5"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"analyze": "source-map-explorer 'build/static/js/*.js'",
|
"analyze": "source-map-explorer 'build/static/js/*.js'",
|
||||||
"start": "craco start",
|
"start": "craco start",
|
||||||
"build": "REACT_APP_GIT_SHA=`git rev-parse --short HEAD` craco build",
|
"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",
|
"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!'",
|
"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",
|
"test": "craco test",
|
||||||
|
|||||||
@@ -7,17 +7,25 @@ import React from "react";
|
|||||||
import GlobalLoadingBar from "../components/global-loading-bar/global-loading-bar.component";
|
import GlobalLoadingBar from "../components/global-loading-bar/global-loading-bar.component";
|
||||||
import client from "../utils/GraphQLClient";
|
import client from "../utils/GraphQLClient";
|
||||||
import App from "./App";
|
import App from "./App";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
moment.locale("en-US");
|
moment.locale("en-US");
|
||||||
|
|
||||||
if (process.env.NODE_ENV === "production") LogRocket.init("gvfvfw/bodyshopapp");
|
if (process.env.NODE_ENV === "production") LogRocket.init("gvfvfw/bodyshopapp");
|
||||||
|
|
||||||
export default function AppContainer() {
|
export default function AppContainer() {
|
||||||
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<ApolloProvider client={client}>
|
<ApolloProvider client={client}>
|
||||||
<ConfigProvider
|
<ConfigProvider
|
||||||
//componentSize="small"
|
//componentSize="small"
|
||||||
input={{ autoComplete: "new-password" }}
|
input={{ autoComplete: "new-password" }}
|
||||||
locale={enLocale}
|
locale={enLocale}
|
||||||
|
form={{
|
||||||
|
validateMessages: {
|
||||||
|
// eslint-disable-next-line no-template-curly-in-string
|
||||||
|
required: t("general.validation.required", { label: "${label}" }),
|
||||||
|
},
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<GlobalLoadingBar />
|
<GlobalLoadingBar />
|
||||||
<App />
|
<App />
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import React, { useState } from "react";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
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 PayableExportButton from "../payable-export-button/payable-export-button.component";
|
||||||
import PayableExportAll from "../payable-export-all-button/payable-export-all-button.component";
|
import PayableExportAll from "../payable-export-all-button/payable-export-all-button.component";
|
||||||
import { DateFormatter } from "../../utils/DateFormatter";
|
import { DateFormatter } from "../../utils/DateFormatter";
|
||||||
@@ -80,7 +80,7 @@ export default function AccountingPayablesTableComponent({ loading, bills }) {
|
|||||||
dataIndex: "date",
|
dataIndex: "date",
|
||||||
key: "date",
|
key: "date",
|
||||||
|
|
||||||
sorter: (a, b) => a.date - b.date,
|
sorter: (a, b) => dateSort(a.date, b.date),
|
||||||
sortOrder:
|
sortOrder:
|
||||||
state.sortedInfo.columnKey === "date" && state.sortedInfo.order,
|
state.sortedInfo.columnKey === "date" && state.sortedInfo.order,
|
||||||
render: (text, record) => <DateFormatter>{record.date}</DateFormatter>,
|
render: (text, record) => <DateFormatter>{record.date}</DateFormatter>,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { logImEXEvent } from "../../firebase/firebase.utils";
|
|||||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||||
import { alphaSort } from "../../utils/sorters";
|
import { alphaSort } from "../../utils/sorters";
|
||||||
import JobExportButton from "../jobs-close-export-button/jobs-close-export-button.component";
|
import JobExportButton from "../jobs-close-export-button/jobs-close-export-button.component";
|
||||||
import { JobsExportAllButton } from "../jobs-export-all-button/jobs-export-all-button.component";
|
import JobsExportAllButton from "../jobs-export-all-button/jobs-export-all-button.component";
|
||||||
|
|
||||||
export default function AccountingReceivablesTableComponent({ loading, jobs }) {
|
export default function AccountingReceivablesTableComponent({ loading, jobs }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { DeleteFilled } from "@ant-design/icons";
|
||||||
import { useMutation } from "@apollo/client";
|
import { useMutation } from "@apollo/client";
|
||||||
import { Button, notification, Popconfirm } from "antd";
|
import { Button, notification, Popconfirm } from "antd";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
@@ -57,7 +58,7 @@ export default function BillDeleteButton({ bill }) {
|
|||||||
// onClick={handleDelete}
|
// onClick={handleDelete}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
>
|
>
|
||||||
{t("general.actions.delete")}
|
<DeleteFilled />
|
||||||
</Button>
|
</Button>
|
||||||
</Popconfirm>
|
</Popconfirm>
|
||||||
</RbacWrapper>
|
</RbacWrapper>
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
import { useMutation, useQuery } from "@apollo/client";
|
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 moment from "moment";
|
||||||
import queryString from "query-string";
|
import queryString from "query-string";
|
||||||
import React, { useEffect, useState } from "react";
|
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 BillFormContainer from "../bill-form/bill-form.container";
|
||||||
import JobDocumentsGallery from "../jobs-documents-gallery/jobs-documents-gallery.container";
|
import JobDocumentsGallery from "../jobs-documents-gallery/jobs-documents-gallery.container";
|
||||||
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
|
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 search = queryString.parse(useLocation().search);
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -34,9 +59,9 @@ export default function BillDetailEditcontainer() {
|
|||||||
xs: "100%",
|
xs: "100%",
|
||||||
sm: "100%",
|
sm: "100%",
|
||||||
md: "100%",
|
md: "100%",
|
||||||
lg: "80%",
|
lg: "100%",
|
||||||
xl: "80%",
|
xl: "80%",
|
||||||
xxl: "70%",
|
xxl: "80%",
|
||||||
};
|
};
|
||||||
const drawerPercentage = selectedBreakpoint
|
const drawerPercentage = selectedBreakpoint
|
||||||
? bpoints[selectedBreakpoint[0]]
|
? bpoints[selectedBreakpoint[0]]
|
||||||
@@ -117,13 +142,13 @@ export default function BillDetailEditcontainer() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (search.billid) {
|
if (search.billid && data) {
|
||||||
form.resetFields();
|
form.resetFields();
|
||||||
}
|
}
|
||||||
}, [form, search.billid]);
|
}, [form, search.billid, data]);
|
||||||
|
|
||||||
if (error) return <AlertComponent message={error.message} type="error" />;
|
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;
|
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}`
|
`${data.bills_by_pk.invoice_number} - ${data.bills_by_pk.vendor.name}`
|
||||||
}
|
}
|
||||||
extra={
|
extra={
|
||||||
<Popconfirm
|
<Space>
|
||||||
visible={visible}
|
|
||||||
onConfirm={() => form.submit()}
|
|
||||||
onCancel={() => setVisible(false)}
|
|
||||||
okButtonProps={{ loading: updateLoading }}
|
|
||||||
title={t("bills.labels.editadjwarning")}
|
|
||||||
>
|
|
||||||
<Button
|
<Button
|
||||||
htmlType="submit"
|
disabled={data.bills_by_pk.is_credit_memo}
|
||||||
disabled={exported}
|
onClick={() => {
|
||||||
onClick={handleSave}
|
delete search.billid;
|
||||||
loading={updateLoading}
|
history.push({ search: queryString.stringify(search) });
|
||||||
type="primary"
|
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>
|
</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
|
<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) {
|
if (upload && upload.length > 0) {
|
||||||
//insert Each of the documents?
|
//insert Each of the documents?
|
||||||
@@ -191,7 +181,10 @@ function BillEnterModalContainer({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
toggleModalVisible();
|
const r = window.confirm(t("general.labels.cancel"));
|
||||||
|
if (r === true) {
|
||||||
|
toggleModalVisible();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -218,6 +211,8 @@ function BillEnterModalContainer({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (billEnterModal.visible) {
|
if (billEnterModal.visible) {
|
||||||
form.setFieldsValue(formValues);
|
form.setFieldsValue(formValues);
|
||||||
|
} else {
|
||||||
|
form.resetFields();
|
||||||
}
|
}
|
||||||
}, [billEnterModal.visible, form, formValues]);
|
}, [billEnterModal.visible, form, formValues]);
|
||||||
|
|
||||||
@@ -227,6 +222,7 @@ function BillEnterModalContainer({
|
|||||||
width={"90%"}
|
width={"90%"}
|
||||||
visible={billEnterModal.visible}
|
visible={billEnterModal.visible}
|
||||||
okText={t("general.actions.save")}
|
okText={t("general.actions.save")}
|
||||||
|
keyboard="false"
|
||||||
onOk={() => form.submit()}
|
onOk={() => form.submit()}
|
||||||
onCancel={handleCancel}
|
onCancel={handleCancel}
|
||||||
afterClose={() => form.resetFields()}
|
afterClose={() => form.resetFields()}
|
||||||
@@ -259,7 +255,7 @@ function BillEnterModalContainer({
|
|||||||
onFinishFailed={() => {
|
onFinishFailed={() => {
|
||||||
setEnterAgain(false);
|
setEnterAgain(false);
|
||||||
}}
|
}}
|
||||||
initialValues={formValues}
|
// initialValues={formValues}
|
||||||
>
|
>
|
||||||
<BillFormContainer
|
<BillFormContainer
|
||||||
form={form}
|
form={form}
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ export function BillFormComponent({
|
|||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
message: t("general.validation.required"),
|
//message: t("general.validation.required"),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
@@ -104,7 +104,7 @@ export function BillFormComponent({
|
|||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
message: t("general.validation.required"),
|
//message: t("general.validation.required"),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
@@ -124,7 +124,7 @@ export function BillFormComponent({
|
|||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
message: t("general.validation.required"),
|
//message: t("general.validation.required"),
|
||||||
},
|
},
|
||||||
({ getFieldValue }) => ({
|
({ getFieldValue }) => ({
|
||||||
async validator(rule, value) {
|
async validator(rule, value) {
|
||||||
@@ -165,7 +165,7 @@ export function BillFormComponent({
|
|||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
message: t("general.validation.required"),
|
//message: t("general.validation.required"),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
@@ -184,7 +184,7 @@ export function BillFormComponent({
|
|||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
message: t("general.validation.required"),
|
//message: t("general.validation.required"),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
@@ -202,23 +202,99 @@ export function BillFormComponent({
|
|||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
<LayoutFormRow>
|
<LayoutFormRow>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
|
span={3}
|
||||||
label={t("bills.fields.federal_tax_rate")}
|
label={t("bills.fields.federal_tax_rate")}
|
||||||
name="federal_tax_rate"
|
name="federal_tax_rate"
|
||||||
>
|
>
|
||||||
<CurrencyInput min={0} disabled={disabled} />
|
<CurrencyInput min={0} disabled={disabled} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
|
span={3}
|
||||||
label={t("bills.fields.state_tax_rate")}
|
label={t("bills.fields.state_tax_rate")}
|
||||||
name="state_tax_rate"
|
name="state_tax_rate"
|
||||||
>
|
>
|
||||||
<CurrencyInput min={0} disabled={disabled} />
|
<CurrencyInput min={0} disabled={disabled} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
|
span={3}
|
||||||
label={t("bills.fields.local_tax_rate")}
|
label={t("bills.fields.local_tax_rate")}
|
||||||
name="local_tax_rate"
|
name="local_tax_rate"
|
||||||
>
|
>
|
||||||
<CurrencyInput min={0} />
|
<CurrencyInput min={0} />
|
||||||
</Form.Item>
|
</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>
|
</LayoutFormRow>
|
||||||
<Divider orientation="left">{t("bills.labels.bill_lines")}</Divider>
|
<Divider orientation="left">{t("bills.labels.bill_lines")}</Divider>
|
||||||
<BillFormLines
|
<BillFormLines
|
||||||
@@ -244,77 +320,6 @@ export function BillFormComponent({
|
|||||||
<Button>Click to upload</Button>
|
<Button>Click to upload</Button>
|
||||||
</Upload>
|
</Upload>
|
||||||
</Form.Item>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { WarningOutlined } from "@ant-design/icons";
|
import { DeleteFilled, WarningOutlined } from "@ant-design/icons";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Form,
|
Form,
|
||||||
@@ -36,375 +36,397 @@ export function BillEnterModalLinesComponent({
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { setFieldsValue, getFieldsValue, getFieldValue } = form;
|
const { setFieldsValue, getFieldsValue, getFieldValue } = form;
|
||||||
|
|
||||||
const columns = [
|
const columns = (remove) => {
|
||||||
{
|
return [
|
||||||
title: t("billlines.fields.jobline"),
|
{
|
||||||
dataIndex: "joblineid",
|
title: t("billlines.fields.jobline"),
|
||||||
editable: true,
|
dataIndex: "joblineid",
|
||||||
width: "10%",
|
editable: true,
|
||||||
formItemProps: (field) => {
|
|
||||||
return {
|
formItemProps: (field) => {
|
||||||
key: `${field.index}joblinename`,
|
return {
|
||||||
name: [field.name, "joblineid"],
|
key: `${field.index}joblinename`,
|
||||||
rules: [
|
name: [field.name, "joblineid"],
|
||||||
{
|
rules: [
|
||||||
required: true,
|
{
|
||||||
message: t("general.validation.required"),
|
required: true,
|
||||||
},
|
//message: t("general.validation.required"),
|
||||||
],
|
},
|
||||||
};
|
],
|
||||||
},
|
};
|
||||||
formInput: (record, index) => (
|
},
|
||||||
<BillLineSearchSelect
|
formInput: (record, index) => (
|
||||||
disabled={disabled}
|
<BillLineSearchSelect
|
||||||
options={lineData}
|
disabled={disabled}
|
||||||
onSelect={(value, opt) => {
|
options={lineData}
|
||||||
setFieldsValue({
|
style={{ width: "100%", minWidth: "10rem" }}
|
||||||
billlines: getFieldsValue(["billlines"]).billlines.map(
|
onSelect={(value, opt) => {
|
||||||
(item, idx) => {
|
setFieldsValue({
|
||||||
if (idx === index) {
|
billlines: getFieldsValue(["billlines"]).billlines.map(
|
||||||
return {
|
(item, idx) => {
|
||||||
...item,
|
if (idx === index) {
|
||||||
line_desc: opt.line_desc,
|
return {
|
||||||
quantity: opt.part_qty || 1,
|
...item,
|
||||||
actual_price: opt.cost,
|
line_desc: opt.line_desc,
|
||||||
cost_center: opt.part_type
|
quantity: opt.part_qty || 1,
|
||||||
? responsibilityCenters.defaults.costs[opt.part_type] ||
|
actual_price: opt.cost,
|
||||||
null
|
cost_center: opt.part_type
|
||||||
: null,
|
? 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.line_desc"),
|
||||||
{
|
dataIndex: "line_desc",
|
||||||
title: t("billlines.fields.quantity"),
|
editable: true,
|
||||||
dataIndex: "quantity",
|
|
||||||
editable: true,
|
formItemProps: (field) => {
|
||||||
formItemProps: (field) => {
|
return {
|
||||||
return {
|
key: `${field.index}line_desc`,
|
||||||
key: `${field.index}quantity`,
|
name: [field.name, "line_desc"],
|
||||||
name: [field.name, "quantity"],
|
rules: [
|
||||||
rules: [
|
{
|
||||||
{
|
required: true,
|
||||||
required: true,
|
//message: t("general.validation.required"),
|
||||||
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.quantity"),
|
||||||
),
|
dataIndex: "quantity",
|
||||||
},
|
editable: true,
|
||||||
{
|
width: "4rem",
|
||||||
title: t("billlines.fields.actual_price"),
|
formItemProps: (field) => {
|
||||||
dataIndex: "actual_price",
|
return {
|
||||||
editable: true,
|
key: `${field.index}quantity`,
|
||||||
formItemProps: (field) => {
|
name: [field.name, "quantity"],
|
||||||
return {
|
rules: [
|
||||||
key: `${field.index}actual_price`,
|
{
|
||||||
name: [field.name, "actual_price"],
|
required: true,
|
||||||
rules: [
|
//message: t("general.validation.required"),
|
||||||
{
|
},
|
||||||
required: true,
|
],
|
||||||
message: t("general.validation.required"),
|
};
|
||||||
},
|
},
|
||||||
],
|
formInput: (record, index) => (
|
||||||
};
|
<InputNumber precision={0} min={0} disabled={disabled} />
|
||||||
|
),
|
||||||
},
|
},
|
||||||
formInput: (record, index) => (
|
{
|
||||||
<CurrencyInput
|
title: t("billlines.fields.actual_price"),
|
||||||
min={0}
|
dataIndex: "actual_price",
|
||||||
disabled={disabled}
|
width: "8rem",
|
||||||
onBlur={(e) => {
|
editable: true,
|
||||||
setFieldsValue({
|
formItemProps: (field) => {
|
||||||
billlines: getFieldsValue("billlines").billlines.map(
|
return {
|
||||||
(item, idx) => {
|
key: `${field.index}actual_price`,
|
||||||
console.log("Checking", index, idx);
|
name: [field.name, "actual_price"],
|
||||||
if (idx === index) {
|
rules: [
|
||||||
console.log(
|
{
|
||||||
"Found and setting.",
|
required: true,
|
||||||
!!item.actual_cost
|
//message: t("general.validation.required"),
|
||||||
? item.actual_cost
|
},
|
||||||
: Math.round(
|
],
|
||||||
(parseFloat(e.target.value) * (1 - discount) +
|
};
|
||||||
Number.EPSILON) *
|
},
|
||||||
100
|
formInput: (record, index) => (
|
||||||
) / 100
|
<CurrencyInput
|
||||||
);
|
min={0}
|
||||||
return {
|
disabled={disabled}
|
||||||
...item,
|
onBlur={(e) => {
|
||||||
actual_cost: !!item.actual_cost
|
setFieldsValue({
|
||||||
? item.actual_cost
|
billlines: getFieldsValue("billlines").billlines.map(
|
||||||
: Math.round(
|
(item, idx) => {
|
||||||
(parseFloat(e.target.value) * (1 - discount) +
|
console.log("Checking", index, idx);
|
||||||
Number.EPSILON) *
|
if (idx === index) {
|
||||||
100
|
console.log(
|
||||||
) / 100,
|
"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} />
|
title: t("billlines.fields.actual_cost"),
|
||||||
),
|
dataIndex: "actual_cost",
|
||||||
additional: (record, index) => (
|
editable: true,
|
||||||
<Form.Item shouldUpdate>
|
width: "8rem",
|
||||||
{() => {
|
formItemProps: (field) => {
|
||||||
const line = getFieldsValue(["billlines"]).billlines[index];
|
return {
|
||||||
if (!!!line) return null;
|
key: `${field.index}actual_cost`,
|
||||||
const lineDiscount = (
|
name: [field.name, "actual_cost"],
|
||||||
1 -
|
rules: [
|
||||||
Math.round((line.actual_cost / line.actual_price) * 100) / 100
|
{
|
||||||
).toPrecision(2);
|
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 />;
|
if (lineDiscount - discount === 0) return <div />;
|
||||||
return <WarningOutlined style={{ color: "red" }} />;
|
return <WarningOutlined style={{ color: "red" }} />;
|
||||||
}}
|
}}
|
||||||
</Form.Item>
|
</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"),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
formInput: (record, index) => (
|
{
|
||||||
<Select style={{ width: "150px" }} disabled={disabled}>
|
title: t("billlines.fields.cost_center"),
|
||||||
{responsibilityCenters.costs.map((item) => (
|
dataIndex: "cost_center",
|
||||||
<Select.Option key={item.name}>{item.name}</Select.Option>
|
editable: true,
|
||||||
))}
|
formItemProps: (field) => {
|
||||||
</Select>
|
return {
|
||||||
),
|
key: `${field.index}cost_center`,
|
||||||
},
|
name: [field.name, "cost_center"],
|
||||||
{
|
rules: [
|
||||||
title: t("billlines.fields.federal_tax_applicable"),
|
{
|
||||||
dataIndex: "applicable_taxes.federal",
|
required: true,
|
||||||
editable: true,
|
//message: t("general.validation.required"),
|
||||||
formItemProps: (field) => {
|
},
|
||||||
return {
|
],
|
||||||
key: `${field.index}fedtax`,
|
};
|
||||||
valuePropName: "checked",
|
},
|
||||||
initialValue: true,
|
formInput: (record, index) => (
|
||||||
name: [field.name, "applicable_taxes", "federal"],
|
<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"),
|
title: t("billlines.fields.location"),
|
||||||
dataIndex: "location",
|
dataIndex: "location",
|
||||||
editable: true,
|
editable: true,
|
||||||
formItemProps: (field) => {
|
formItemProps: (field) => {
|
||||||
return {
|
return {
|
||||||
key: `${field.index}location`,
|
key: `${field.index}location`,
|
||||||
name: [field.name, "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}>
|
title: t("billlines.labels.deductedfromlbr"),
|
||||||
{bodyshop.md_parts_locations.map((loc, idx) => (
|
dataIndex: "deductedfromlbr",
|
||||||
<Select.Option key={idx} value={loc}>
|
editable: true,
|
||||||
{loc}
|
formItemProps: (field) => {
|
||||||
</Select.Option>
|
return {
|
||||||
))}
|
valuePropName: "checked",
|
||||||
</Select>
|
key: `${field.index}deductedfromlbr`,
|
||||||
),
|
name: [field.name, "deductedfromlbr"],
|
||||||
},
|
};
|
||||||
{
|
},
|
||||||
title: t("billlines.labels.deductedfromlbr"),
|
formInput: (record, index) => <Switch disabled={disabled} />,
|
||||||
dataIndex: "deductedfromlbr",
|
additional: (record, index) => (
|
||||||
editable: true,
|
<Form.Item shouldUpdate style={{ display: "inline-block" }}>
|
||||||
formItemProps: (field) => {
|
{() => {
|
||||||
return {
|
if (getFieldValue(["billlines", record.name, "deductedfromlbr"]))
|
||||||
valuePropName: "checked",
|
return (
|
||||||
key: `${field.index}deductedfromlbr`,
|
<div>
|
||||||
name: [field.name, "deductedfromlbr"],
|
<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) => (
|
title: t("billlines.fields.federal_tax_applicable"),
|
||||||
<Form.Item shouldUpdate style={{ display: "inline-block" }}>
|
dataIndex: "applicable_taxes.federal",
|
||||||
{() => {
|
editable: true,
|
||||||
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>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const mergedColumns = columns.map((col) => {
|
formItemProps: (field) => {
|
||||||
if (!col.editable) return col;
|
return {
|
||||||
return {
|
key: `${field.index}fedtax`,
|
||||||
...col,
|
valuePropName: "checked",
|
||||||
onCell: (record) => ({
|
initialValue: true,
|
||||||
record,
|
name: [field.name, "applicable_taxes", "federal"],
|
||||||
formItemProps: col.formItemProps,
|
};
|
||||||
formInput: col.formInput,
|
},
|
||||||
additional: col.additional,
|
formInput: (record, index) => <Switch disabled={disabled} />,
|
||||||
dataIndex: col.dataIndex,
|
},
|
||||||
title: col.title,
|
{
|
||||||
}),
|
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 (
|
return (
|
||||||
<Form.List name="billlines">
|
<Form.List name="billlines">
|
||||||
@@ -420,7 +442,7 @@ export function BillEnterModalLinesComponent({
|
|||||||
size="small"
|
size="small"
|
||||||
bordered
|
bordered
|
||||||
dataSource={fields}
|
dataSource={fields}
|
||||||
columns={mergedColumns}
|
columns={mergedColumns(remove)}
|
||||||
scroll={{ x: true }}
|
scroll={{ x: true }}
|
||||||
rowClassName="editable-row"
|
rowClassName="editable-row"
|
||||||
/>
|
/>
|
||||||
@@ -462,12 +484,12 @@ const EditableCell = ({
|
|||||||
if (additional)
|
if (additional)
|
||||||
return (
|
return (
|
||||||
<td {...restProps}>
|
<td {...restProps}>
|
||||||
<Space>
|
<Space size="small">
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name={dataIndex}
|
name={dataIndex}
|
||||||
{...(formItemProps && formItemProps(record))}
|
{...(formItemProps && formItemProps(record))}
|
||||||
>
|
>
|
||||||
{formInput && formInput(record, record.key)}
|
{(formInput && formInput(record, record.key)) || children}
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
{additional && additional(record, record.key)}
|
{additional && additional(record, record.key)}
|
||||||
</Space>
|
</Space>
|
||||||
@@ -477,7 +499,7 @@ const EditableCell = ({
|
|||||||
return (
|
return (
|
||||||
<td {...restProps}>
|
<td {...restProps}>
|
||||||
<Form.Item name={dataIndex} {...(formItemProps && formItemProps(record))}>
|
<Form.Item name={dataIndex} {...(formItemProps && formItemProps(record))}>
|
||||||
{formInput && formInput(record, record.key)}
|
{(formInput && formInput(record, record.key)) || children}
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</td>
|
</td>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
import Dinero from "dinero.js";
|
import Dinero from "dinero.js";
|
||||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
|
||||||
|
|
||||||
export const CalculateBillTotal = (invoice) => {
|
export const CalculateBillTotal = (invoice) => {
|
||||||
logImEXEvent("invoice_calculate_total");
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
total,
|
total,
|
||||||
billlines,
|
billlines,
|
||||||
|
|||||||
@@ -1,37 +1,19 @@
|
|||||||
import { Select, Tag } from "antd";
|
import { Select } from "antd";
|
||||||
import React, { forwardRef, useEffect, useState } from "react";
|
import React, { forwardRef } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
|
||||||
|
|
||||||
//To be used as a form element only.
|
//To be used as a form element only.
|
||||||
const { Option } = Select;
|
const { Option } = Select;
|
||||||
const BillLineSearchSelect = (
|
const BillLineSearchSelect = ({ options, disabled, ...restProps }, ref) => {
|
||||||
{ value, onChange, options, onBlur, onSelect, disabled },
|
|
||||||
ref
|
|
||||||
) => {
|
|
||||||
const [option, setOption] = useState(value);
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (value !== option && onChange) {
|
|
||||||
onChange(option);
|
|
||||||
}
|
|
||||||
}, [value, option, onChange]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
showSearch
|
showSearch
|
||||||
autoFocus
|
|
||||||
value={option}
|
|
||||||
style={{
|
|
||||||
width: "100%",
|
|
||||||
}}
|
|
||||||
onChange={setOption}
|
|
||||||
optionFilterProp="line_desc"
|
optionFilterProp="line_desc"
|
||||||
onBlur={onBlur}
|
{...restProps}
|
||||||
onSelect={onSelect}
|
|
||||||
>
|
>
|
||||||
<Select.Option key={null} value={"noline"} cost={0} line_desc={""}>
|
<Select.Option key={null} value={"noline"} cost={0} line_desc={""}>
|
||||||
{t("billlines.labels.other")}
|
{t("billlines.labels.other")}
|
||||||
@@ -46,17 +28,9 @@ const BillLineSearchSelect = (
|
|||||||
line_desc={item.line_desc}
|
line_desc={item.line_desc}
|
||||||
part_qty={item.part_qty}
|
part_qty={item.part_qty}
|
||||||
>
|
>
|
||||||
<div className="imex-flex-row">
|
{`${item.line_desc}${
|
||||||
<div style={{ flex: 1 }}>{item.line_desc}</div>
|
item.oem_partno ? ` - ${item.oem_partno}` : ""
|
||||||
{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>
|
|
||||||
</Option>
|
</Option>
|
||||||
))
|
))
|
||||||
: null}
|
: 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 { EyeFilled, SyncOutlined } from "@ant-design/icons";
|
||||||
import {
|
import { Button, Card, Checkbox, Input, Space, Table } from "antd";
|
||||||
Button,
|
|
||||||
Card,
|
|
||||||
Checkbox,
|
|
||||||
Descriptions,
|
|
||||||
Drawer,
|
|
||||||
Grid,
|
|
||||||
Input,
|
|
||||||
PageHeader,
|
|
||||||
Space,
|
|
||||||
Table,
|
|
||||||
} from "antd";
|
|
||||||
import queryString from "query-string";
|
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { Link, useLocation } from "react-router-dom";
|
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||||
import { DateFormatter } from "../../utils/DateFormatter";
|
import { DateFormatter } from "../../utils/DateFormatter";
|
||||||
import { alphaSort } from "../../utils/sorters";
|
import { alphaSort, dateSort } from "../../utils/sorters";
|
||||||
import { TemplateList } from "../../utils/TemplateConstants";
|
import { TemplateList } from "../../utils/TemplateConstants";
|
||||||
import BillDeleteButton from "../bill-delete-button/bill-delete-button.component";
|
import BillDeleteButton from "../bill-delete-button/bill-delete-button.component";
|
||||||
import PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
|
import PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
|
||||||
@@ -47,27 +34,12 @@ export function BillsListTableComponent({
|
|||||||
setReconciliationContext,
|
setReconciliationContext,
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
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({
|
const [state, setState] = useState({
|
||||||
sortedInfo: {},
|
sortedInfo: {},
|
||||||
});
|
});
|
||||||
const search = queryString.parse(useLocation().search);
|
// const search = queryString.parse(useLocation().search);
|
||||||
const selectedBill = search.billid;
|
// const selectedBill = search.billid;
|
||||||
const Templates = TemplateList("bill");
|
const Templates = TemplateList("bill");
|
||||||
const bills = billsQuery.data ? billsQuery.data.bills : [];
|
const bills = billsQuery.data ? billsQuery.data.bills : [];
|
||||||
const { refetch } = billsQuery;
|
const { refetch } = billsQuery;
|
||||||
@@ -78,16 +50,34 @@ export function BillsListTableComponent({
|
|||||||
<EyeFilled />
|
<EyeFilled />
|
||||||
</Button>
|
</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} />
|
<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 && (
|
{record.isinhouse && (
|
||||||
<PrintWrapperComponent
|
<PrintWrapperComponent
|
||||||
templateObject={{
|
templateObject={{
|
||||||
@@ -122,7 +112,7 @@ export function BillsListTableComponent({
|
|||||||
title: t("bills.fields.date"),
|
title: t("bills.fields.date"),
|
||||||
dataIndex: "date",
|
dataIndex: "date",
|
||||||
key: "date",
|
key: "date",
|
||||||
sorter: (a, b) => a.date - b.date,
|
sorter: (a, b) => dateSort(a.date, b.date),
|
||||||
sortOrder:
|
sortOrder:
|
||||||
state.sortedInfo.columnKey === "date" && state.sortedInfo.order,
|
state.sortedInfo.columnKey === "date" && state.sortedInfo.order,
|
||||||
render: (text, record) => <DateFormatter>{record.date}</DateFormatter>,
|
render: (text, record) => <DateFormatter>{record.date}</DateFormatter>,
|
||||||
@@ -164,189 +154,11 @@ export function BillsListTableComponent({
|
|||||||
render: (text, record) => recordActions(record, true),
|
render: (text, record) => recordActions(record, true),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const selectedBillRecord = bills.find((r) => r.id === selectedBill);
|
|
||||||
|
|
||||||
const handleTableChange = (pagination, filters, sorter) => {
|
const handleTableChange = (pagination, filters, sorter) => {
|
||||||
setState({ ...state, filteredInfo: filters, sortedInfo: 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 (
|
return (
|
||||||
<Card
|
<Card
|
||||||
title={t("bills.labels.bills")}
|
title={t("bills.labels.bills")}
|
||||||
@@ -355,7 +167,7 @@ export function BillsListTableComponent({
|
|||||||
<Button onClick={() => refetch()}>
|
<Button onClick={() => refetch()}>
|
||||||
<SyncOutlined />
|
<SyncOutlined />
|
||||||
</Button>
|
</Button>
|
||||||
{job ? (
|
{job && job.converted ? (
|
||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -394,20 +206,11 @@ export function BillsListTableComponent({
|
|||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Drawer
|
|
||||||
placement="right"
|
|
||||||
onClose={() => handleOnRowClick(null)}
|
|
||||||
visible={selectedBill}
|
|
||||||
//getContainer={false}
|
|
||||||
style={{ position: "absolute" }}
|
|
||||||
closable
|
|
||||||
width={drawerPercentage}
|
|
||||||
>
|
|
||||||
{selectedBillRecord && rowExpander(selectedBillRecord)}
|
|
||||||
</Drawer>
|
|
||||||
<Table
|
<Table
|
||||||
loading={billsQuery.loading}
|
loading={billsQuery.loading}
|
||||||
scroll={{ x: true, y: "50rem" }}
|
scroll={{
|
||||||
|
x: true, // y: "50rem"
|
||||||
|
}}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
dataSource={bills}
|
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,
|
conversation,
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const [visible, setVisible] = useState(false);
|
||||||
|
|
||||||
const { loading, error, data } = useQuery(GET_DOCUMENTS_BY_JOB, {
|
const { loading, error, data } = useQuery(GET_DOCUMENTS_BY_JOB, {
|
||||||
variables: {
|
variables: {
|
||||||
@@ -33,12 +34,11 @@ export function ChatMediaSelector({
|
|||||||
},
|
},
|
||||||
fetchPolicy: "network-only",
|
fetchPolicy: "network-only",
|
||||||
skip:
|
skip:
|
||||||
|
!visible ||
|
||||||
!conversation.job_conversations ||
|
!conversation.job_conversations ||
|
||||||
conversation.job_conversations.length === 0,
|
conversation.job_conversations.length === 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
const [visible, setVisible] = useState(false);
|
|
||||||
|
|
||||||
const handleVisibleChange = (visible) => {
|
const handleVisibleChange = (visible) => {
|
||||||
setVisible(visible);
|
setVisible(visible);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,13 +6,23 @@ import { connect } from "react-redux";
|
|||||||
import { openChatByPhone } from "../../redux/messaging/messaging.actions";
|
import { openChatByPhone } from "../../redux/messaging/messaging.actions";
|
||||||
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
|
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
|
||||||
|
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
bodyshop: selectBodyshop,
|
||||||
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
openChatByPhone: (phone) => dispatch(openChatByPhone(phone)),
|
openChatByPhone: (phone) => dispatch(openChatByPhone(phone)),
|
||||||
});
|
});
|
||||||
|
|
||||||
export function ChatOpenButton({ phone, jobid, openChatByPhone }) {
|
export function ChatOpenButton({ bodyshop, phone, jobid, openChatByPhone }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
if (!phone) return <></>;
|
if (!phone) return <></>;
|
||||||
|
|
||||||
|
if (!bodyshop.messagingservicesid)
|
||||||
|
return <PhoneNumberFormatter>{phone}</PhoneNumberFormatter>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
href="# "
|
href="# "
|
||||||
@@ -31,4 +41,4 @@ export function ChatOpenButton({ phone, jobid, openChatByPhone }) {
|
|||||||
</a>
|
</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 React from "react";
|
||||||
import { Form, Checkbox } from "antd";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
|
|
||||||
export default function JobIntakeFormCheckboxComponent({ formItem, readOnly }) {
|
export default function JobIntakeFormCheckboxComponent({ formItem, readOnly }) {
|
||||||
const { name, label, required } = formItem;
|
const { name, label, required } = formItem;
|
||||||
const { t } = useTranslation();
|
|
||||||
return (
|
return (
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name={name}
|
name={name}
|
||||||
@@ -13,7 +12,7 @@ export default function JobIntakeFormCheckboxComponent({ formItem, readOnly }) {
|
|||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: required,
|
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 { Form, Rate } from "antd";
|
||||||
import { useTranslation } from "react-i18next";
|
import React from "react";
|
||||||
|
|
||||||
export default function JobIntakeFormCheckboxComponent({ formItem, readOnly }) {
|
export default function JobIntakeFormCheckboxComponent({ formItem, readOnly }) {
|
||||||
const { name, label, required } = formItem;
|
const { name, label, required } = formItem;
|
||||||
const { t } = useTranslation();
|
|
||||||
return (
|
return (
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name={name}
|
name={name}
|
||||||
@@ -12,7 +11,7 @@ export default function JobIntakeFormCheckboxComponent({ formItem, readOnly }) {
|
|||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: required,
|
required: required,
|
||||||
message: t("general.validation.required"),
|
//message: t("general.validation.required"),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { Form, Slider } from "antd";
|
import { Form, Slider } from "antd";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
|
|
||||||
export default function JobIntakeFormCheckboxComponent({ formItem, readOnly }) {
|
export default function JobIntakeFormCheckboxComponent({ formItem, readOnly }) {
|
||||||
const { name, label, required, min, max } = formItem;
|
const { name, label, required, min, max } = formItem;
|
||||||
const { t } = useTranslation();
|
|
||||||
return (
|
return (
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name={name}
|
name={name}
|
||||||
@@ -12,7 +11,7 @@ export default function JobIntakeFormCheckboxComponent({ formItem, readOnly }) {
|
|||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: required,
|
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 { Form, Input } from "antd";
|
||||||
import { useTranslation } from "react-i18next";
|
import React from "react";
|
||||||
|
|
||||||
export default function JobIntakeFormCheckboxComponent({ formItem, readOnly }) {
|
export default function JobIntakeFormCheckboxComponent({ formItem, readOnly }) {
|
||||||
const { name, label, required } = formItem;
|
const { name, label, required } = formItem;
|
||||||
const { t } = useTranslation();
|
|
||||||
return (
|
return (
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name={name}
|
name={name}
|
||||||
@@ -12,7 +10,7 @@ export default function JobIntakeFormCheckboxComponent({ formItem, readOnly }) {
|
|||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: required,
|
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 { Form, Input } from "antd";
|
||||||
import { useTranslation } from "react-i18next";
|
import React from "react";
|
||||||
|
|
||||||
export default function JobIntakeFormCheckboxComponent({ formItem, readOnly }) {
|
export default function JobIntakeFormCheckboxComponent({ formItem, readOnly }) {
|
||||||
const { name, label, required, rows } = formItem;
|
const { name, label, required, rows } = formItem;
|
||||||
const { t } = useTranslation();
|
|
||||||
return (
|
return (
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name={name}
|
name={name}
|
||||||
@@ -12,7 +11,7 @@ export default function JobIntakeFormCheckboxComponent({ formItem, readOnly }) {
|
|||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: required,
|
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({
|
export default function ContractsCarsComponent({
|
||||||
loading,
|
loading,
|
||||||
data,
|
data,
|
||||||
selectedCar,
|
selectedCarId,
|
||||||
handleSelect,
|
handleSelect,
|
||||||
}) {
|
}) {
|
||||||
const [state, setState] = useState({
|
const [state, setState] = useState({
|
||||||
@@ -117,7 +117,7 @@ export default function ContractsCarsComponent({
|
|||||||
rowSelection={{
|
rowSelection={{
|
||||||
onSelect: handleSelect,
|
onSelect: handleSelect,
|
||||||
type: "radio",
|
type: "radio",
|
||||||
selectedRowKeys: [selectedCar],
|
selectedRowKeys: [selectedCarId],
|
||||||
}}
|
}}
|
||||||
onRow={(record, rowIndex) => {
|
onRow={(record, rowIndex) => {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -13,12 +13,13 @@ export default function ContractCarsContainer({ selectedCarState, form }) {
|
|||||||
const [selectedCar, setSelectedCar] = selectedCarState;
|
const [selectedCar, setSelectedCar] = selectedCarState;
|
||||||
|
|
||||||
const handleSelect = (record) => {
|
const handleSelect = (record) => {
|
||||||
setSelectedCar(record.id);
|
setSelectedCar(record);
|
||||||
|
|
||||||
form.setFieldsValue({
|
form.setFieldsValue({
|
||||||
kmstart: record.mileage,
|
kmstart: record.mileage,
|
||||||
dailyrate: record.dailycost,
|
dailyrate: record.dailycost,
|
||||||
fuelout: record.fuel,
|
fuelout: record.fuel,
|
||||||
|
damage: record.damage,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -26,7 +27,7 @@ export default function ContractCarsContainer({ selectedCarState, form }) {
|
|||||||
return (
|
return (
|
||||||
<ContractCarsComponent
|
<ContractCarsComponent
|
||||||
handleSelect={handleSelect}
|
handleSelect={handleSelect}
|
||||||
selectedCar={selectedCar}
|
selectedCarId={selectedCar && selectedCar.id}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
data={data ? data.courtesycars : []}
|
data={data ? data.courtesycars : []}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -314,7 +314,7 @@ export function ContractConvertToRo({
|
|||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
message: t("general.validation.required"),
|
//message: t("general.validation.required"),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
@@ -332,7 +332,7 @@ export function ContractConvertToRo({
|
|||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: bodyshop.enforce_class,
|
required: bodyshop.enforce_class,
|
||||||
message: t("general.validation.required"),
|
//message: t("general.validation.required"),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
@@ -349,7 +349,7 @@ export function ContractConvertToRo({
|
|||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
message: t("general.validation.required"),
|
//message: t("general.validation.required"),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
name={"applyCleanupCharge"}
|
name={"applyCleanupCharge"}
|
||||||
@@ -364,7 +364,7 @@ export function ContractConvertToRo({
|
|||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
message: t("general.validation.required"),
|
//message: t("general.validation.required"),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
name={"refuelqty"}
|
name={"refuelqty"}
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
|
import { WarningFilled } from "@ant-design/icons";
|
||||||
import { Form, Input, InputNumber, Space } from "antd";
|
import { Form, Input, InputNumber, Space } from "antd";
|
||||||
|
import moment from "moment";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { DateFormatter } from "../../utils/DateFormatter";
|
||||||
import ContractLicenseDecodeButton from "../contract-license-decode-button/contract-license-decode-button.component";
|
import ContractLicenseDecodeButton from "../contract-license-decode-button/contract-license-decode-button.component";
|
||||||
import ContractStatusSelector from "../contract-status-select/contract-status-select.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 CourtesyCarFuelSlider from "../courtesy-car-fuel-select/courtesy-car-fuel-select.component";
|
||||||
import FormDatePicker from "../form-date-picker/form-date-picker.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 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, {
|
import InputPhone, {
|
||||||
PhoneItemFormatterValidation,
|
PhoneItemFormatterValidation,
|
||||||
} from "../form-items-formatted/phone-form-item.component";
|
} from "../form-items-formatted/phone-form-item.component";
|
||||||
@@ -17,6 +21,7 @@ export default function ContractFormComponent({
|
|||||||
form,
|
form,
|
||||||
create = false,
|
create = false,
|
||||||
selectedJobState,
|
selectedJobState,
|
||||||
|
selectedCar,
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
@@ -30,7 +35,7 @@ export default function ContractFormComponent({
|
|||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
message: t("general.validation.required"),
|
//message: t("general.validation.required"),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
@@ -44,47 +49,90 @@ export default function ContractFormComponent({
|
|||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
message: t("general.validation.required"),
|
//message: t("general.validation.required"),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<FormDatePicker />
|
<FormDateTimePicker />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("contracts.fields.scheduledreturn")}
|
label={t("contracts.fields.scheduledreturn")}
|
||||||
name="scheduledreturn"
|
name="scheduledreturn"
|
||||||
>
|
>
|
||||||
<FormDatePicker />
|
<FormDateTimePicker />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
{create ? null : (
|
{create ? null : (
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("contracts.fields.actualreturn")}
|
label={t("contracts.fields.actualreturn")}
|
||||||
name="actualreturn"
|
name="actualreturn"
|
||||||
>
|
>
|
||||||
<FormDatePicker />
|
<FormDateTimePicker />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
)}
|
)}
|
||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
<LayoutFormRow>
|
<LayoutFormRow grow>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("contracts.fields.kmstart")}
|
label={t("contracts.fields.kmstart")}
|
||||||
name="kmstart"
|
name="kmstart"
|
||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
message: t("general.validation.required"),
|
//message: t("general.validation.required"),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<InputNumber />
|
<InputNumber />
|
||||||
</Form.Item>
|
</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 : (
|
{create ? null : (
|
||||||
<Form.Item label={t("contracts.fields.kmend")} name="kmend">
|
<Form.Item label={t("contracts.fields.kmend")} name="kmend">
|
||||||
<InputNumber />
|
<InputNumber />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
)}
|
)}
|
||||||
|
<Form.Item label={t("contracts.fields.damage")} name="damage">
|
||||||
|
<Input.TextArea />
|
||||||
|
</Form.Item>
|
||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
<LayoutFormRow>
|
<LayoutFormRow grow>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("contracts.fields.fuelout")}
|
label={t("contracts.fields.fuelout")}
|
||||||
name="fuelout"
|
name="fuelout"
|
||||||
@@ -92,7 +140,7 @@ export default function ContractFormComponent({
|
|||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
message: t("general.validation.required"),
|
//message: t("general.validation.required"),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
@@ -128,34 +176,49 @@ export default function ContractFormComponent({
|
|||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
message: t("general.validation.required"),
|
//message: t("general.validation.required"),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("contracts.fields.driver_dlexpiry")}
|
shouldUpdate={(p, c) =>
|
||||||
name="driver_dlexpiry"
|
p.driver_dlexpiry !== c.driver_dlexpiry ||
|
||||||
rules={[
|
p.scheduledreturn !== c.scheduledreturn
|
||||||
{
|
}
|
||||||
required: true,
|
|
||||||
message: t("general.validation.required"),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
>
|
||||||
<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>
|
||||||
<Form.Item
|
|
||||||
label={t("contracts.fields.driver_dlst")}
|
<Form.Item label={t("contracts.fields.driver_dlst")} name="driver_dlst">
|
||||||
name="driver_dlst"
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
message: t("general.validation.required"),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
@@ -164,7 +227,7 @@ export default function ContractFormComponent({
|
|||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
message: t("general.validation.required"),
|
//message: t("general.validation.required"),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
@@ -176,7 +239,7 @@ export default function ContractFormComponent({
|
|||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
message: t("general.validation.required"),
|
//message: t("general.validation.required"),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
@@ -188,7 +251,7 @@ export default function ContractFormComponent({
|
|||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
message: t("general.validation.required"),
|
//message: t("general.validation.required"),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
@@ -218,7 +281,7 @@ export default function ContractFormComponent({
|
|||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
message: t("general.validation.required"),
|
//message: t("general.validation.required"),
|
||||||
},
|
},
|
||||||
({ getFieldValue }) =>
|
({ getFieldValue }) =>
|
||||||
PhoneItemFormatterValidation(getFieldValue, "driver_ph1"),
|
PhoneItemFormatterValidation(getFieldValue, "driver_ph1"),
|
||||||
@@ -230,6 +293,7 @@ export default function ContractFormComponent({
|
|||||||
<FormDatePicker />
|
<FormDatePicker />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
|
<ContractsRatesChangeButton form={form} />
|
||||||
<LayoutFormRow header={t("contracts.labels.rates")}>
|
<LayoutFormRow header={t("contracts.labels.rates")}>
|
||||||
<Form.Item label={t("contracts.fields.dailyrate")} name="dailyrate">
|
<Form.Item label={t("contracts.fields.dailyrate")} name="dailyrate">
|
||||||
<InputNumber precision={2} />
|
<InputNumber precision={2} />
|
||||||
@@ -267,16 +331,16 @@ export default function ContractFormComponent({
|
|||||||
<InputNumber precision={2} />
|
<InputNumber precision={2} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("contracts.fields.federaltax")} name="federaltax">
|
<Form.Item label={t("contracts.fields.federaltax")} name="federaltax">
|
||||||
<InputNumberCalculator precision={2} />
|
<InputNumber precision={2} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("contracts.fields.statetax")} name="statetax">
|
<Form.Item label={t("contracts.fields.statetax")} name="statetax">
|
||||||
<InputNumberCalculator precision={2} />
|
<InputNumber precision={2} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("contracts.fields.localtax")} name="localtax">
|
<Form.Item label={t("contracts.fields.localtax")} name="localtax">
|
||||||
<InputNumberCalculator precision={2} />
|
<InputNumber precision={2} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("contracts.fields.coverage")} name="coverage">
|
<Form.Item label={t("contracts.fields.coverage")} name="coverage">
|
||||||
<InputNumberCalculator precision={2} />
|
<InputNumber precision={2} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
</div>
|
</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 { 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 queryString from "query-string";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Link, useHistory, useLocation } from "react-router-dom";
|
import { Link, useHistory, useLocation } from "react-router-dom";
|
||||||
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
||||||
import { alphaSort } from "../../utils/sorters";
|
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({
|
const [state, setState] = useState({
|
||||||
sortedInfo: {},
|
sortedInfo: {},
|
||||||
filteredInfo: { text: "" },
|
filteredInfo: { text: "" },
|
||||||
@@ -126,14 +145,29 @@ export default function ContractsList({ loading, contracts, refetch, total }) {
|
|||||||
<Card
|
<Card
|
||||||
extra={
|
extra={
|
||||||
<Space wrap>
|
<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()}>
|
<Button onClick={() => refetch()}>
|
||||||
<SyncOutlined />
|
<SyncOutlined />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<TimeTicketsDatesSelector />
|
|
||||||
|
|
||||||
<Input.Search
|
<Input.Search
|
||||||
placeholder={t("general.labels.search")}
|
placeholder={search.searh || t("general.labels.search")}
|
||||||
onSearch={(value) => {
|
onSearch={(value) => {
|
||||||
search.search = value;
|
search.search = value;
|
||||||
history.push({ search: queryString.stringify(search) });
|
history.push({ search: queryString.stringify(search) });
|
||||||
@@ -142,9 +176,12 @@ export default function ContractsList({ loading, contracts, refetch, total }) {
|
|||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
<ContractsFindModalContainer />
|
||||||
<Table
|
<Table
|
||||||
loading={loading}
|
loading={loading}
|
||||||
scroll={{ x: "50%", y: "40rem" }}
|
scroll={{
|
||||||
|
x: "50%", //y: "40rem"
|
||||||
|
}}
|
||||||
pagination={{
|
pagination={{
|
||||||
position: "top",
|
position: "top",
|
||||||
pageSize: 25,
|
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 React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
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 CourtesyCarFuelSlider from "../courtesy-car-fuel-select/courtesy-car-fuel-select.component";
|
||||||
import CourtesyCarStatus from "../courtesy-car-status-select/courtesy-car-status-select.component";
|
import CourtesyCarStatus from "../courtesy-car-status-select/courtesy-car-status-select.component";
|
||||||
import FormDatePicker from "../form-date-picker/form-date-picker.component";
|
import FormDatePicker from "../form-date-picker/form-date-picker.component";
|
||||||
import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
|
import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
|
||||||
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
|
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
|
||||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||||
import InputNumberCalculator from "../form-input-number-calculator/form-input-number-calculator.component";
|
|
||||||
|
|
||||||
export default function CourtesyCarCreateFormComponent({ form, saveLoading }) {
|
export default function CourtesyCarCreateFormComponent({ form, saveLoading }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const client = useApolloClient();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<PageHeader
|
<PageHeader
|
||||||
@@ -34,7 +40,7 @@ export default function CourtesyCarCreateFormComponent({ form, saveLoading }) {
|
|||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
message: t("general.validation.required"),
|
//message: t("general.validation.required"),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
@@ -46,7 +52,7 @@ export default function CourtesyCarCreateFormComponent({ form, saveLoading }) {
|
|||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
message: t("general.validation.required"),
|
//message: t("general.validation.required"),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
@@ -58,7 +64,7 @@ export default function CourtesyCarCreateFormComponent({ form, saveLoading }) {
|
|||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
message: t("general.validation.required"),
|
//message: t("general.validation.required"),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
@@ -70,7 +76,7 @@ export default function CourtesyCarCreateFormComponent({ form, saveLoading }) {
|
|||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
message: t("general.validation.required"),
|
//message: t("general.validation.required"),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
@@ -82,7 +88,7 @@ export default function CourtesyCarCreateFormComponent({ form, saveLoading }) {
|
|||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
message: t("general.validation.required"),
|
//message: t("general.validation.required"),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
@@ -94,7 +100,7 @@ export default function CourtesyCarCreateFormComponent({ form, saveLoading }) {
|
|||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
message: t("general.validation.required"),
|
//message: t("general.validation.required"),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
@@ -108,7 +114,7 @@ export default function CourtesyCarCreateFormComponent({ form, saveLoading }) {
|
|||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
message: t("general.validation.required"),
|
//message: t("general.validation.required"),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
@@ -117,6 +123,41 @@ export default function CourtesyCarCreateFormComponent({ form, saveLoading }) {
|
|||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("courtesycars.fields.fleetnumber")}
|
label={t("courtesycars.fields.fleetnumber")}
|
||||||
name="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 />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
@@ -154,7 +195,7 @@ export default function CourtesyCarCreateFormComponent({ form, saveLoading }) {
|
|||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
message: t("general.validation.required"),
|
//message: t("general.validation.required"),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
@@ -166,7 +207,7 @@ export default function CourtesyCarCreateFormComponent({ form, saveLoading }) {
|
|||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
message: t("general.validation.required"),
|
//message: t("general.validation.required"),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
@@ -178,24 +219,60 @@ export default function CourtesyCarCreateFormComponent({ form, saveLoading }) {
|
|||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
message: t("general.validation.required"),
|
//message: t("general.validation.required"),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<InputNumberCalculator />
|
<InputNumber />
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label={t("courtesycars.fields.nextservicedate")}
|
|
||||||
name="nextservicedate"
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
message: t("general.validation.required"),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<FormDatePicker />
|
|
||||||
</Form.Item>
|
</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">
|
<Form.Item label={t("courtesycars.fields.damage")} name="damage">
|
||||||
<Input.TextArea />
|
<Input.TextArea />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
@@ -209,7 +286,7 @@ export default function CourtesyCarCreateFormComponent({ form, saveLoading }) {
|
|||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
message: t("general.validation.required"),
|
//message: t("general.validation.required"),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
@@ -221,7 +298,7 @@ export default function CourtesyCarCreateFormComponent({ form, saveLoading }) {
|
|||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
message: t("general.validation.required"),
|
//message: t("general.validation.required"),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export default function CourtesyCarReturnModalComponent() {
|
|||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
message: t("general.validation.required"),
|
//message: t("general.validation.required"),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
@@ -27,7 +27,7 @@ export default function CourtesyCarReturnModalComponent() {
|
|||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
message: t("general.validation.required"),
|
//message: t("general.validation.required"),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
@@ -39,7 +39,7 @@ export default function CourtesyCarReturnModalComponent() {
|
|||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
message: t("general.validation.required"),
|
//message: t("general.validation.required"),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { useQuery } from "@apollo/client";
|
import { useQuery } from "@apollo/client";
|
||||||
import { Form } from "antd";
|
import { Card, Form, Result } from "antd";
|
||||||
import queryString from "query-string";
|
import queryString from "query-string";
|
||||||
import React, { useEffect } from "react";
|
import React, { useEffect } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { useLocation } from "react-router-dom";
|
import { useLocation } from "react-router-dom";
|
||||||
import { QUERY_CSI_RESPONSE_BY_PK } from "../../graphql/csi.queries";
|
import { QUERY_CSI_RESPONSE_BY_PK } from "../../graphql/csi.queries";
|
||||||
import 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 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() {
|
export default function CsiResponseFormContainer() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -25,19 +25,24 @@ export default function CsiResponseFormContainer() {
|
|||||||
form.resetFields();
|
form.resetFields();
|
||||||
}, [data, form]);
|
}, [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 (loading) return <LoadingSpinner />;
|
||||||
if (error) return <AlertComponent message={error.message} type="error" />;
|
if (error) return <AlertComponent message={error.message} type="error" />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<Card>
|
||||||
<Form form={form} initialValues={data.csi_by_pk.response}>
|
<Form form={form} initialValues={data.csi_by_pk.response}>
|
||||||
<ConfigFormComponents
|
<ConfigFormComponents
|
||||||
readOnly
|
readOnly
|
||||||
componentList={data.csi_by_pk.csiquestion.config}
|
componentList={data.csi_by_pk.csiquestion.config}
|
||||||
/>
|
/>
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { SyncOutlined } from "@ant-design/icons";
|
import { SyncOutlined } from "@ant-design/icons";
|
||||||
import { Button, Table } from "antd";
|
import { Button, Card, Table } from "antd";
|
||||||
import queryString from "query-string";
|
import queryString from "query-string";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@@ -46,15 +46,15 @@ export default function CsiResponseListPaginated({
|
|||||||
width: "25%",
|
width: "25%",
|
||||||
sortOrder: sortcolumn === "owner" && sortorder,
|
sortOrder: sortcolumn === "owner" && sortorder,
|
||||||
render: (text, record) => {
|
render: (text, record) => {
|
||||||
return record.owner ? (
|
return record.job.owner ? (
|
||||||
<Link to={"/manage/owners/" + record.owner.id}>
|
<Link to={"/manage/owners/" + record.job.owner.id}>
|
||||||
{`${record.job.ownr_fn || ""} ${record.job.ownr_ln || ""} ${
|
{`${record.job.ownr_fn || ""} ${record.job.ownr_ln || ""} ${
|
||||||
record.job.ownr_co_nm
|
record.job.ownr_co_nm || ""
|
||||||
}`}
|
}`}
|
||||||
</Link>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
<span>{`${record.job.ownr_fn || ""} ${record.job.ownr_ln || ""} ${
|
<span>{`${record.job.ownr_fn || ""} ${record.job.ownr_ln || ""} ${
|
||||||
record.job.ownr_co_nm
|
record.job.ownr_co_nm || ""
|
||||||
}`}</span>
|
}`}</span>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -96,28 +96,15 @@ export default function CsiResponseListPaginated({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<Card
|
||||||
|
extra={
|
||||||
|
<Button onClick={() => refetch()}>
|
||||||
|
<SyncOutlined />
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
>
|
||||||
<Table
|
<Table
|
||||||
loading={loading}
|
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={{
|
pagination={{
|
||||||
position: "top",
|
position: "top",
|
||||||
pageSize: 25,
|
pageSize: 25,
|
||||||
@@ -143,6 +130,6 @@ export default function CsiResponseListPaginated({
|
|||||||
};
|
};
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { UploadOutlined } from "@ant-design/icons";
|
import { UploadOutlined } from "@ant-design/icons";
|
||||||
import { notification, Progress, Result, Space, Upload } from "antd";
|
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 { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
@@ -27,6 +27,7 @@ export function DocumentsUploadComponent({
|
|||||||
ignoreSizeLimit = false,
|
ignoreSizeLimit = false,
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const [fileList, setFileList] = useState([]);
|
||||||
|
|
||||||
const pct = useMemo(() => {
|
const pct = useMemo(() => {
|
||||||
return parseInt(
|
return parseInt(
|
||||||
@@ -40,12 +41,23 @@ export function DocumentsUploadComponent({
|
|||||||
status="error"
|
status="error"
|
||||||
title={t("documents.labels.storageexceeded_title")}
|
title={t("documents.labels.storageexceeded_title")}
|
||||||
subTitle={t("documents.labels.storageexceeded")}
|
subTitle={t("documents.labels.storageexceeded")}
|
||||||
></Result>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleDone = (uid) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
setFileList((fileList) => fileList.filter((x) => x.uid !== uid));
|
||||||
|
}, 2000);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Upload.Dragger
|
<Upload.Dragger
|
||||||
multiple={true}
|
multiple={true}
|
||||||
|
fileList={fileList}
|
||||||
|
onChange={(f) => {
|
||||||
|
if (f.event && f.event.percent === 100) handleDone(f.file.uid);
|
||||||
|
setFileList(f.fileList);
|
||||||
|
}}
|
||||||
beforeUpload={(file, fileList) => {
|
beforeUpload={(file, fileList) => {
|
||||||
if (ignoreSizeLimit) return true;
|
if (ignoreSizeLimit) return true;
|
||||||
const newFiles = fileList.reduce((acc, val) => acc + val.size, 0);
|
const newFiles = fileList.reduce((acc, val) => acc + val.size, 0);
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ export const uploadToCloudinary = async (
|
|||||||
type: fileType,
|
type: fileType,
|
||||||
extension: extension,
|
extension: extension,
|
||||||
bodyshopid: bodyshop.id,
|
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",
|
status: "done",
|
||||||
key: documentInsert.data.insert_documents.returning[0].key,
|
key: documentInsert.data.insert_documents.returning[0].key,
|
||||||
});
|
});
|
||||||
notification["success"]({
|
notification.open({
|
||||||
|
type: "success",
|
||||||
|
key: "docuploadsuccess",
|
||||||
message: i18n.t("documents.successes.insert"),
|
message: i18n.t("documents.successes.insert"),
|
||||||
});
|
});
|
||||||
if (callback) {
|
if (callback) {
|
||||||
|
|||||||
@@ -1,68 +1,84 @@
|
|||||||
import { UploadOutlined } from "@ant-design/icons";
|
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 React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export default function EmailOverlayComponent({
|
export default function EmailOverlayComponent({ form }) {
|
||||||
messageOptions,
|
|
||||||
handleConfigChange,
|
|
||||||
handleToChange,
|
|
||||||
handleHtmlChange,
|
|
||||||
handleUpload,
|
|
||||||
handleFileRemove,
|
|
||||||
}) {
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
To:
|
<Form.Item
|
||||||
<Select
|
label={t("emails.fields.to")}
|
||||||
name="to"
|
name="to"
|
||||||
mode="tags"
|
rules={[
|
||||||
value={messageOptions.to}
|
{
|
||||||
//style={{ width: "100%" }}
|
required: true,
|
||||||
onChange={handleToChange}
|
//message: t("general.validation.required"),
|
||||||
tokenSeparators={[",", ";"]}
|
},
|
||||||
/>
|
]}
|
||||||
CC:
|
>
|
||||||
<Select
|
<Select mode="tags" tokenSeparators={[",", ";"]} />
|
||||||
value={messageOptions.cc}
|
</Form.Item>
|
||||||
mode="tags"
|
<Form.Item label={t("emails.fields.cc")} name="cc">
|
||||||
onChange={(value) => handleConfigChange("cc", value)}
|
<Select mode="tags" tokenSeparators={[",", ";"]} />
|
||||||
name="cc"
|
</Form.Item>
|
||||||
tokenSeparators={[",", ";"]}
|
<Form.Item
|
||||||
/>
|
label={t("emails.fields.subject")}
|
||||||
Subject:
|
|
||||||
<Input
|
|
||||||
value={messageOptions.subject}
|
|
||||||
onChange={(e) => handleConfigChange("subject", e.target.value)}
|
|
||||||
name="subject"
|
name="subject"
|
||||||
/>
|
rules={[
|
||||||
<Divider>{t("emails.labels.preview")}</Divider>
|
{
|
||||||
<div
|
required: true,
|
||||||
style={{
|
//message: t("general.validation.required"),
|
||||||
padding: "1rem",
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
backgroundColor: "lightgray",
|
<Divider>{t("emails.labels.preview")}</Divider>
|
||||||
borderLeft: "6px solid #2196F3",
|
<Form.Item shouldUpdate>
|
||||||
|
{() => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
padding: "1rem",
|
||||||
|
|
||||||
|
backgroundColor: "lightgray",
|
||||||
|
borderLeft: "6px solid #2196F3",
|
||||||
|
}}
|
||||||
|
dangerouslySetInnerHTML={{ __html: form.getFieldValue("html") }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
dangerouslySetInnerHTML={{ __html: messageOptions.html }}
|
</Form.Item>
|
||||||
/>
|
|
||||||
<Divider>
|
|
||||||
<Divider>{t("emails.labels.preview")}</Divider>
|
|
||||||
</Divider>
|
|
||||||
<Card title={t("emails.labels.attachments")}>
|
<Card title={t("emails.labels.attachments")}>
|
||||||
<Upload
|
<Form.Item
|
||||||
fileList={messageOptions.fileList}
|
name="fileList"
|
||||||
beforeUpload={handleUpload}
|
valuePropName="fileList"
|
||||||
onRemove={handleFileRemove}
|
getValueFromEvent={(e) => {
|
||||||
multiple
|
console.log("Upload event:", e);
|
||||||
listType="picture-card"
|
if (Array.isArray(e)) {
|
||||||
style={{ width: "100%" }}
|
return e;
|
||||||
|
}
|
||||||
|
return e && e.fileList;
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Button>
|
<Upload.Dragger
|
||||||
<UploadOutlined /> Upload
|
beforeUpload={Upload.LIST_IGNORE}
|
||||||
</Button>
|
multiple
|
||||||
</Upload>
|
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>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Modal, notification } from "antd";
|
import { Divider, Form, Modal, notification } from "antd";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@@ -10,9 +10,13 @@ import {
|
|||||||
selectEmailConfig,
|
selectEmailConfig,
|
||||||
selectEmailVisible,
|
selectEmailVisible,
|
||||||
} from "../../redux/email/email.selectors.js";
|
} 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 RenderTemplate from "../../utils/RenderTemplate";
|
||||||
import { EmailSettings } from "../../utils/TemplateConstants";
|
import { EmailSettings } from "../../utils/TemplateConstants";
|
||||||
|
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
|
||||||
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
||||||
import EmailOverlayComponent from "./email-overlay.component";
|
import EmailOverlayComponent from "./email-overlay.component";
|
||||||
|
|
||||||
@@ -20,6 +24,7 @@ const mapStateToProps = createStructuredSelector({
|
|||||||
modalVisible: selectEmailVisible,
|
modalVisible: selectEmailVisible,
|
||||||
emailConfig: selectEmailConfig,
|
emailConfig: selectEmailConfig,
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
|
currentUser: selectCurrentUser,
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
@@ -31,35 +36,34 @@ export function EmailOverlayContainer({
|
|||||||
modalVisible,
|
modalVisible,
|
||||||
toggleEmailOverlayVisible,
|
toggleEmailOverlayVisible,
|
||||||
bodyshop,
|
bodyshop,
|
||||||
|
currentUser,
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const [form] = Form.useForm();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [sending, setSending] = useState(false);
|
const [sending, setSending] = useState(false);
|
||||||
const [rawHtml, setRawHtml] = useState("");
|
const [rawHtml, setRawHtml] = useState("");
|
||||||
const defaultEmailFrom = {
|
const defaultEmailFrom = {
|
||||||
from: {
|
from: {
|
||||||
name: bodyshop.shopname || EmailSettings.fromNameDefault,
|
name: `${currentUser.displayName} @ ${bodyshop.shopname}`,
|
||||||
address: EmailSettings.fromAddress,
|
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");
|
logImEXEvent("email_send_from_modal");
|
||||||
|
console.log(`values`, values);
|
||||||
const attachments = [];
|
const attachments = [];
|
||||||
|
|
||||||
await asyncForEach(messageOptions.fileList, async (f) => {
|
await asyncForEach(values.fileList, async (f) => {
|
||||||
const t = {
|
const t = {
|
||||||
ContentType: f.type,
|
ContentType: f.type,
|
||||||
Filename: f.name,
|
Filename: f.name,
|
||||||
Base64Content: (await toBase64(f)).split(",")[1],
|
Base64Content: (await toBase64(f.originFileObj)).split(",")[1],
|
||||||
};
|
};
|
||||||
attachments.push(t);
|
attachments.push(t);
|
||||||
});
|
});
|
||||||
@@ -67,9 +71,13 @@ export function EmailOverlayContainer({
|
|||||||
setSending(true);
|
setSending(true);
|
||||||
try {
|
try {
|
||||||
await axios.post("/sendemail", {
|
await axios.post("/sendemail", {
|
||||||
...messageOptions,
|
...defaultEmailFrom,
|
||||||
|
...values,
|
||||||
html: rawHtml,
|
html: rawHtml,
|
||||||
attachments,
|
attachments: await Promise.all(
|
||||||
|
values.fileList.map(async (f) => await toBase64(f.originFileObj))
|
||||||
|
),
|
||||||
|
//attachments,
|
||||||
});
|
});
|
||||||
notification["success"]({ message: t("emails.successes.sent") });
|
notification["success"]({ message: t("emails.successes.sent") });
|
||||||
toggleEmailOverlayVisible();
|
toggleEmailOverlayVisible();
|
||||||
@@ -82,34 +90,6 @@ export function EmailOverlayContainer({
|
|||||||
setSending(false);
|
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 () => {
|
const render = async () => {
|
||||||
logImEXEvent("email_render_template", { template: emailConfig.template });
|
logImEXEvent("email_render_template", { template: emailConfig.template });
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -120,11 +100,14 @@ export function EmailOverlayContainer({
|
|||||||
url: `${window.location.protocol}://${window.location.host}/`,
|
url: `${window.location.protocol}://${window.location.host}/`,
|
||||||
});
|
});
|
||||||
setRawHtml(response.data);
|
setRawHtml(response.data);
|
||||||
|
form.setFieldsValue({
|
||||||
console.log("response", response);
|
|
||||||
setMessageOptions({
|
|
||||||
...emailConfig.messageOptions,
|
...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,
|
html: response.data,
|
||||||
fileList: [],
|
fileList: [],
|
||||||
});
|
});
|
||||||
@@ -140,29 +123,22 @@ export function EmailOverlayContainer({
|
|||||||
destroyOnClose={true}
|
destroyOnClose={true}
|
||||||
visible={modalVisible}
|
visible={modalVisible}
|
||||||
width={"80%"}
|
width={"80%"}
|
||||||
onOk={handleOk}
|
onOk={() => form.submit()}
|
||||||
onCancel={() => {
|
onCancel={() => {
|
||||||
toggleEmailOverlayVisible();
|
toggleEmailOverlayVisible();
|
||||||
}}
|
}}
|
||||||
okButtonProps={{ loading: sending }}
|
okButtonProps={{ loading: sending }}
|
||||||
>
|
>
|
||||||
<LoadingSpinner loading={loading}>
|
<Form layout="vertical" form={form} onFinish={handleFinish}>
|
||||||
<EmailOverlayComponent
|
{loading && (
|
||||||
handleConfigChange={handleConfigChange}
|
<div>
|
||||||
messageOptions={messageOptions}
|
<LoadingSkeleton />
|
||||||
handleHtmlChange={handleHtmlChange}
|
<Divider>{t("emails.labels.preview")}</Divider>
|
||||||
handleUpload={handleUpload}
|
<LoadingSpinner message={t("emails.labels.generatingemail")} />
|
||||||
handleFileRemove={handleFileRemove}
|
</div>
|
||||||
handleToChange={handleToChange}
|
)}
|
||||||
/>
|
{!loading && <EmailOverlayComponent form={form} />}
|
||||||
<button
|
</Form>
|
||||||
onClick={() => {
|
|
||||||
navigator.clipboard.writeText(messageOptions.html);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Copy HTML
|
|
||||||
</button>
|
|
||||||
</LoadingSpinner>
|
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { Button, Form, Input, Select, Switch } from "antd";
|
import { Button, Form, Input, Select, Switch } from "antd";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { setEmailOptions } from "../../redux/email/email.actions";
|
import { setEmailOptions } from "../../redux/email/email.actions";
|
||||||
@@ -18,7 +17,6 @@ const mapDispatchToProps = (dispatch) => ({
|
|||||||
|
|
||||||
export function EmailTestComponent({ currentUser, setEmailOptions }) {
|
export function EmailTestComponent({ currentUser, setEmailOptions }) {
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
const handleFinish = (values) => {
|
const handleFinish = (values) => {
|
||||||
console.log("values", values);
|
console.log("values", values);
|
||||||
@@ -71,7 +69,7 @@ export function EmailTestComponent({ currentUser, setEmailOptions }) {
|
|||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
message: t("general.validation.required"),
|
//message: t("general.validation.required"),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,33 +1,21 @@
|
|||||||
import { Select, Space, Tag } from "antd";
|
import { Select, Space, Tag } from "antd";
|
||||||
import React, { forwardRef, useEffect, useState } from "react";
|
import React, { forwardRef } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
const { Option } = Select;
|
const { Option } = Select;
|
||||||
//To be used as a form element only.
|
//To be used as a form element only.
|
||||||
|
|
||||||
const EmployeeSearchSelect = (
|
const EmployeeSearchSelect = ({ options, ...props }, ref) => {
|
||||||
{ value, onChange, options, onSelect, onBlur, ...restProps },
|
|
||||||
ref
|
|
||||||
) => {
|
|
||||||
const [option, setOption] = useState(value);
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
useEffect(() => {
|
|
||||||
if (value !== option && onChange) {
|
|
||||||
onChange(option);
|
|
||||||
}
|
|
||||||
}, [value, option, onChange]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
showSearch
|
showSearch
|
||||||
value={option}
|
// value={option}
|
||||||
style={{
|
style={{
|
||||||
width: 400,
|
width: 400,
|
||||||
}}
|
}}
|
||||||
onChange={setOption}
|
|
||||||
optionFilterProp="search"
|
optionFilterProp="search"
|
||||||
onSelect={onSelect}
|
{...props}
|
||||||
onBlur={onBlur}
|
|
||||||
{...restProps}
|
|
||||||
>
|
>
|
||||||
{options
|
{options
|
||||||
? options.map((o) => (
|
? options.map((o) => (
|
||||||
|
|||||||
@@ -5,7 +5,10 @@ import React, { forwardRef } from "react";
|
|||||||
|
|
||||||
const dateFormat = "MM/DD/YYYY";
|
const dateFormat = "MM/DD/YYYY";
|
||||||
|
|
||||||
const FormDatePicker = ({ value, onChange, onBlur, ...restProps }, ref) => {
|
const FormDatePicker = (
|
||||||
|
{ value, onChange, onBlur, onlyFuture, ...restProps },
|
||||||
|
ref
|
||||||
|
) => {
|
||||||
const handleChange = (newDate) => {
|
const handleChange = (newDate) => {
|
||||||
if (value !== newDate && onChange) {
|
if (value !== newDate && onChange) {
|
||||||
onChange(newDate);
|
onChange(newDate);
|
||||||
@@ -27,6 +30,10 @@ const FormDatePicker = ({ value, onChange, onBlur, ...restProps }, ref) => {
|
|||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
format={dateFormat}
|
format={dateFormat}
|
||||||
onBlur={onBlur}
|
onBlur={onBlur}
|
||||||
|
disabledTime
|
||||||
|
{...(onlyFuture && {
|
||||||
|
disabledDate: (d) => moment().subtract(1, "day").isAfter(d),
|
||||||
|
})}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,7 +6,10 @@ import { TimePicker } from "antd";
|
|||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
//To be used as a form element only.
|
//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) => {
|
// const handleChange = (newDate) => {
|
||||||
// if (value !== newDate && onChange) {
|
// if (value !== newDate && onChange) {
|
||||||
// onChange(newDate);
|
// onChange(newDate);
|
||||||
@@ -17,6 +20,9 @@ const DateTimePicker = ({ value, onChange, onBlur, id, ...restProps }, ref) => {
|
|||||||
<div id={id}>
|
<div id={id}>
|
||||||
<FormDatePicker
|
<FormDatePicker
|
||||||
{...restProps}
|
{...restProps}
|
||||||
|
{...(onlyFuture && {
|
||||||
|
disabledDate: (d) => moment().subtract(1, "day").isAfter(d),
|
||||||
|
})}
|
||||||
value={value}
|
value={value}
|
||||||
onBlur={onBlur}
|
onBlur={onBlur}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
@@ -25,6 +31,9 @@ const DateTimePicker = ({ value, onChange, onBlur, id, ...restProps }, ref) => {
|
|||||||
<TimePicker
|
<TimePicker
|
||||||
{...restProps}
|
{...restProps}
|
||||||
value={value ? moment(value) : null}
|
value={value ? moment(value) : null}
|
||||||
|
{...(onlyFuture && {
|
||||||
|
disabledDate: (d) => moment().isAfter(d),
|
||||||
|
})}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
showSecond={false}
|
showSecond={false}
|
||||||
minuteStep={15}
|
minuteStep={15}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Form } from "antd";
|
import { Form, Space } from "antd";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import AlertComponent from "../alert/alert.component";
|
import AlertComponent from "../alert/alert.component";
|
||||||
import { Prompt, useLocation } from "react-router-dom";
|
import { Prompt, useLocation } from "react-router-dom";
|
||||||
@@ -20,9 +20,10 @@ export default function FormsFieldChanged({ form }) {
|
|||||||
style={{ margin: 0, padding: 0, minHeight: "unset" }}
|
style={{ margin: 0, padding: 0, minHeight: "unset" }}
|
||||||
>
|
>
|
||||||
{() => {
|
{() => {
|
||||||
|
const errors = form.getFieldsError().filter((e) => e.errors.length > 0);
|
||||||
if (form.isFieldsTouched())
|
if (form.isFieldsTouched())
|
||||||
return (
|
return (
|
||||||
<span>
|
<Space direction="vertical" style={{ width: "100%" }}>
|
||||||
<Prompt
|
<Prompt
|
||||||
when={true}
|
when={true}
|
||||||
message={(location) => {
|
message={(location) => {
|
||||||
@@ -47,7 +48,23 @@ export default function FormsFieldChanged({ form }) {
|
|||||||
</div>
|
</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>;
|
return <div style={{ display: "none" }}></div>;
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import React, { forwardRef } from "react";
|
import React from "react";
|
||||||
import { BlockPicker } from "react-color";
|
import { SliderPicker } from "react-color";
|
||||||
//To be used as a form element only.
|
//To be used as a form element only.
|
||||||
|
|
||||||
const ColorPickerFormItem = ({ value, onChange, style, ...restProps }, ref) => {
|
const ColorPickerFormItem = ({ value, onChange, style, ...restProps }) => {
|
||||||
const handleChangeComplete = (color) => {
|
const handleChangeComplete = (color) => {
|
||||||
if (onChange) onChange(color);
|
if (onChange) onChange(color);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BlockPicker
|
<SliderPicker
|
||||||
{...restProps}
|
{...restProps}
|
||||||
style={{ width: "100%", ...style }}
|
style={{ width: "100%", ...style }}
|
||||||
color={value}
|
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 (
|
return (
|
||||||
<AutoComplete
|
<AutoComplete
|
||||||
dropdownMatchSelectWidth={false}
|
dropdownMatchSelectWidth={"false"}
|
||||||
options={options}
|
options={options}
|
||||||
onSearch={handleSearch}
|
onSearch={handleSearch}
|
||||||
allowClear
|
allowClear
|
||||||
>
|
>
|
||||||
<Input.Search loading={loading} style={{ width: "20vw" }} />
|
<Input.Search loading={loading} />
|
||||||
</AutoComplete>
|
</AutoComplete>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,21 @@
|
|||||||
import Icon, {
|
import Icon, {
|
||||||
|
BankFilled,
|
||||||
|
BarChartOutlined,
|
||||||
CarFilled,
|
CarFilled,
|
||||||
ClockCircleFilled,
|
ClockCircleFilled,
|
||||||
DollarCircleFilled,
|
DollarCircleFilled,
|
||||||
|
ExportOutlined,
|
||||||
|
FieldTimeOutlined,
|
||||||
FileAddFilled,
|
FileAddFilled,
|
||||||
FileFilled,
|
FileFilled,
|
||||||
GlobalOutlined,
|
GlobalOutlined,
|
||||||
HomeFilled,
|
HomeFilled,
|
||||||
ImportOutlined,
|
ImportOutlined,
|
||||||
LineChartOutlined,
|
LineChartOutlined,
|
||||||
|
PaperClipOutlined,
|
||||||
|
PhoneOutlined,
|
||||||
ScheduleOutlined,
|
ScheduleOutlined,
|
||||||
|
SettingOutlined,
|
||||||
TeamOutlined,
|
TeamOutlined,
|
||||||
ToolFilled,
|
ToolFilled,
|
||||||
UnorderedListOutlined,
|
UnorderedListOutlined,
|
||||||
@@ -24,6 +31,9 @@ import {
|
|||||||
FaCreditCard,
|
FaCreditCard,
|
||||||
FaFileInvoiceDollar,
|
FaFileInvoiceDollar,
|
||||||
} from "react-icons/fa";
|
} 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 { connect } from "react-redux";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
@@ -35,6 +45,7 @@ import { setModalContext } from "../../redux/modals/modals.actions";
|
|||||||
import { signOutStart } from "../../redux/user/user.actions";
|
import { signOutStart } from "../../redux/user/user.actions";
|
||||||
import { selectCurrentUser } from "../../redux/user/user.selectors";
|
import { selectCurrentUser } from "../../redux/user/user.selectors";
|
||||||
import GlobalSearch from "../global-search/global-search.component";
|
import GlobalSearch from "../global-search/global-search.component";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
currentUser: selectCurrentUser,
|
currentUser: selectCurrentUser,
|
||||||
recentItems: selectRecentItems,
|
recentItems: selectRecentItems,
|
||||||
@@ -67,137 +78,103 @@ function Header({
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout.Header>
|
<Layout.Header style={{ display: "flex", alignItems: "center" }}>
|
||||||
<Menu
|
<Menu
|
||||||
mode="horizontal"
|
mode="horizontal"
|
||||||
//theme="light"
|
//theme="light"
|
||||||
theme={"dark"}
|
theme={"dark"}
|
||||||
style={{ flex: 5 }}
|
style={{ flex: 1 }}
|
||||||
selectedKeys={[selectedHeader]}
|
selectedKeys={[selectedHeader]}
|
||||||
onClick={handleMenuClick}
|
onClick={handleMenuClick}
|
||||||
subMenuCloseDelay={0.3}
|
subMenuCloseDelay={0.3}
|
||||||
>
|
>
|
||||||
<Menu.Item key="home">
|
<Menu.Item key="home" icon={<HomeFilled />}>
|
||||||
<Link to="/manage">
|
<Link to="/manage">{t("menus.header.home")}</Link>
|
||||||
<HomeFilled />
|
|
||||||
{t("menus.header.home")}
|
|
||||||
</Link>
|
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.Item key="schedule">
|
<Menu.Item key="schedule" icon={<Icon component={FaCalendarAlt} />}>
|
||||||
<Link to="/manage/schedule">
|
<Link to="/manage/schedule">{t("menus.header.schedule")}</Link>
|
||||||
<Icon component={FaCalendarAlt} />
|
|
||||||
{t("menus.header.schedule")}
|
|
||||||
</Link>
|
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.SubMenu
|
<Menu.SubMenu
|
||||||
title={
|
icon={<Icon component={FaCarCrash} />}
|
||||||
<span>
|
title={t("menus.header.jobs")}
|
||||||
<Icon component={FaCarCrash} />
|
|
||||||
<span>{t("menus.header.jobs")}</span>
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<Menu.Item key="activejobs">
|
<Menu.Item key="activejobs" icon={<FileFilled />}>
|
||||||
<FileFilled />
|
|
||||||
<Link to="/manage/jobs">{t("menus.header.activejobs")}</Link>
|
<Link to="/manage/jobs">{t("menus.header.activejobs")}</Link>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.Item key="parts-queue">
|
<Menu.Item key="parts-queue" icon={<ToolFilled />}>
|
||||||
<Link to="/manage/partsqueue">
|
<Link to="/manage/partsqueue">{t("menus.header.parts-queue")}</Link>
|
||||||
<ToolFilled /> {t("menus.header.parts-queue")}
|
|
||||||
</Link>
|
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.Item key="availablejobs">
|
<Menu.Item key="availablejobs" icon={<ImportOutlined />}>
|
||||||
<Link to="/manage/available">
|
<Link to="/manage/available">
|
||||||
<ImportOutlined /> {t("menus.header.availablejobs")}
|
{t("menus.header.availablejobs")}
|
||||||
</Link>
|
</Link>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.Divider />
|
<Menu.Divider />
|
||||||
<Menu.Item key="alljobs">
|
<Menu.Item key="alljobs" icon={<UnorderedListOutlined />}>
|
||||||
<UnorderedListOutlined />
|
|
||||||
<Link to="/manage/jobs/all">{t("menus.header.alljobs")}</Link>
|
<Link to="/manage/jobs/all">{t("menus.header.alljobs")}</Link>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.Divider />
|
<Menu.Divider />
|
||||||
|
|
||||||
<Menu.Item key="productionlist">
|
<Menu.Item key="productionlist" icon={<ScheduleOutlined />}>
|
||||||
<Link to="/manage/production/list">
|
<Link to="/manage/production/list">
|
||||||
<ScheduleOutlined />
|
|
||||||
{t("menus.header.productionlist")}
|
{t("menus.header.productionlist")}
|
||||||
</Link>
|
</Link>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.Item key="productionboard">
|
<Menu.Item key="productionboard" icon={<Icon component={BsKanban} />}>
|
||||||
<Link to="/manage/production/board">
|
<Link to="/manage/production/board">
|
||||||
<Icon component={BsKanban} />
|
|
||||||
{t("menus.header.productionboard")}
|
{t("menus.header.productionboard")}
|
||||||
</Link>
|
</Link>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.Divider />
|
<Menu.Divider />
|
||||||
|
|
||||||
<Menu.Item key="scoreboard">
|
<Menu.Item key="scoreboard" icon={<LineChartOutlined />}>
|
||||||
<LineChartOutlined />
|
|
||||||
<Link to="/manage/scoreboard">{t("menus.header.scoreboard")}</Link>
|
<Link to="/manage/scoreboard">{t("menus.header.scoreboard")}</Link>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
</Menu.SubMenu>
|
</Menu.SubMenu>
|
||||||
<Menu.SubMenu
|
<Menu.SubMenu
|
||||||
title={
|
icon={<UserOutlined />}
|
||||||
<span>
|
title={t("menus.header.customers")}
|
||||||
<UserOutlined />
|
|
||||||
<span>{t("menus.header.customers")}</span>
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<Menu.Item key="owners">
|
<Menu.Item key="owners" icon={<TeamOutlined />}>
|
||||||
<Link to="/manage/owners">
|
<Link to="/manage/owners">{t("menus.header.owners")}</Link>
|
||||||
<TeamOutlined />
|
|
||||||
{t("menus.header.owners")}
|
|
||||||
</Link>
|
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.Item key="vehicles">
|
<Menu.Item key="vehicles" icon={<CarFilled />}>
|
||||||
<Link to="/manage/vehicles">
|
<Link to="/manage/vehicles">{t("menus.header.vehicles")}</Link>
|
||||||
<CarFilled />
|
|
||||||
{t("menus.header.vehicles")}
|
|
||||||
</Link>
|
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
</Menu.SubMenu>
|
</Menu.SubMenu>
|
||||||
<Menu.SubMenu
|
<Menu.SubMenu
|
||||||
title={
|
icon={<CarFilled />}
|
||||||
<span>
|
title={t("menus.header.courtesycars")}
|
||||||
<CarFilled />
|
|
||||||
<span>{t("menus.header.courtesycars")}</span>
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<Menu.Item key="courtesycarsall">
|
<Menu.Item key="courtesycarsall" icon={<CarFilled />}>
|
||||||
<Link to="/manage/courtesycars">
|
<Link to="/manage/courtesycars">
|
||||||
<CarFilled />
|
|
||||||
{t("menus.header.courtesycars-all")}
|
{t("menus.header.courtesycars-all")}
|
||||||
</Link>
|
</Link>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.Item key="contracts">
|
<Menu.Item key="contracts" icon={<FileFilled />}>
|
||||||
<Link to="/manage/courtesycars/contracts">
|
<Link to="/manage/courtesycars/contracts">
|
||||||
<FileFilled />
|
|
||||||
{t("menus.header.courtesycars-contracts")}
|
{t("menus.header.courtesycars-contracts")}
|
||||||
</Link>
|
</Link>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.Item key="newcontract">
|
<Menu.Item key="newcontract" icon={<FileAddFilled />}>
|
||||||
<Link to="/manage/courtesycars/contracts/new">
|
<Link to="/manage/courtesycars/contracts/new">
|
||||||
<FileAddFilled />
|
|
||||||
{t("menus.header.courtesycars-newcontract")}
|
{t("menus.header.courtesycars-newcontract")}
|
||||||
</Link>
|
</Link>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
</Menu.SubMenu>
|
</Menu.SubMenu>
|
||||||
<Menu.SubMenu
|
<Menu.SubMenu
|
||||||
title={
|
icon={<DollarCircleFilled />}
|
||||||
<span>
|
title={t("menus.header.accounting")}
|
||||||
<DollarCircleFilled />
|
|
||||||
<span>{t("menus.header.accounting")}</span>
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<Menu.Item key="bills">
|
<Menu.Item
|
||||||
|
key="bills"
|
||||||
|
icon={<Icon component={FaFileInvoiceDollar} />}
|
||||||
|
>
|
||||||
<Link to="/manage/bills">{t("menus.header.bills")}</Link>
|
<Link to="/manage/bills">{t("menus.header.bills")}</Link>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
key="enterbills"
|
key="enterbills"
|
||||||
|
icon={<Icon component={GiPayMoney} />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setBillEnterContext({
|
setBillEnterContext({
|
||||||
actions: {},
|
actions: {},
|
||||||
@@ -205,11 +182,10 @@ function Header({
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon component={FaFileInvoiceDollar} />
|
|
||||||
{t("menus.header.enterbills")}
|
{t("menus.header.enterbills")}
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.Divider />
|
<Menu.Divider />
|
||||||
<Menu.Item key="allpayments">
|
<Menu.Item key="allpayments" icon={<BankFilled />}>
|
||||||
<Link to="/manage/payments">{t("menus.header.allpayments")}</Link>
|
<Link to="/manage/payments">{t("menus.header.allpayments")}</Link>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
@@ -226,13 +202,14 @@ function Header({
|
|||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.Divider />
|
<Menu.Divider />
|
||||||
|
|
||||||
<Menu.Item key="timetickets">
|
<Menu.Item key="timetickets" icon={<FieldTimeOutlined />}>
|
||||||
<Link to="/manage/timetickets">
|
<Link to="/manage/timetickets">
|
||||||
{t("menus.header.timetickets")}
|
{t("menus.header.timetickets")}
|
||||||
</Link>
|
</Link>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
key="entertimetickets"
|
key="entertimetickets"
|
||||||
|
icon={<Icon component={GiPlayerTime} />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setTimeTicketContext({
|
setTimeTicketContext({
|
||||||
actions: {},
|
actions: {},
|
||||||
@@ -244,7 +221,10 @@ function Header({
|
|||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.Divider />
|
<Menu.Divider />
|
||||||
|
|
||||||
<Menu.SubMenu title={t("menus.header.export")}>
|
<Menu.SubMenu
|
||||||
|
title={t("menus.header.export")}
|
||||||
|
icon={<ExportOutlined />}
|
||||||
|
>
|
||||||
<Menu.Item key="receivables">
|
<Menu.Item key="receivables">
|
||||||
<Link to="/manage/accounting/receivables">
|
<Link to="/manage/accounting/receivables">
|
||||||
{t("menus.header.accounting-receivables")}
|
{t("menus.header.accounting-receivables")}
|
||||||
@@ -260,26 +240,29 @@ function Header({
|
|||||||
{t("menus.header.accounting-payments")}
|
{t("menus.header.accounting-payments")}
|
||||||
</Link>
|
</Link>
|
||||||
</Menu.Item>
|
</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>
|
</Menu.SubMenu>
|
||||||
<Menu.SubMenu title={t("menus.header.shop")}>
|
<Menu.Item key="phonebook" icon={<PhoneOutlined />}>
|
||||||
<Menu.Item key="shop">
|
<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>
|
<Link to="/manage/shop">{t("menus.header.shop_config")}</Link>
|
||||||
</Menu.Item>
|
</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
|
<Menu.Item
|
||||||
key="reportcenter"
|
key="reportcenter"
|
||||||
|
icon={<BarChartOutlined />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setReportCenterContext({
|
setReportCenterContext({
|
||||||
actions: {},
|
actions: {},
|
||||||
@@ -289,12 +272,15 @@ function Header({
|
|||||||
>
|
>
|
||||||
{t("menus.header.reportcenter")}
|
{t("menus.header.reportcenter")}
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.Item key="shop-vendors">
|
<Menu.Item
|
||||||
|
key="shop-vendors"
|
||||||
|
icon={<Icon component={IoBusinessOutline} />}
|
||||||
|
>
|
||||||
<Link to="/manage/shop/vendors">
|
<Link to="/manage/shop/vendors">
|
||||||
{t("menus.header.shop_vendors")}
|
{t("menus.header.shop_vendors")}
|
||||||
</Link>
|
</Link>
|
||||||
</Menu.Item>
|
</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>
|
<Link to="/manage/shop/csi">{t("menus.header.shop_csi")}</Link>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
</Menu.SubMenu>
|
</Menu.SubMenu>
|
||||||
@@ -348,10 +334,10 @@ function Header({
|
|||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
))}
|
))}
|
||||||
</Menu.SubMenu>
|
</Menu.SubMenu>
|
||||||
<Menu.Item style={{ float: "right" }}>
|
|
||||||
<GlobalSearch />
|
|
||||||
</Menu.Item>
|
|
||||||
</Menu>
|
</Menu>
|
||||||
|
<div>
|
||||||
|
<GlobalSearch />
|
||||||
|
</div>
|
||||||
</Layout.Header>
|
</Layout.Header>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,10 @@ export function Jobd3RdPartyModal({ bodyshop, jobId }) {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const { data: VendorAutoCompleteData } = useQuery(
|
const { data: VendorAutoCompleteData } = useQuery(
|
||||||
SEARCH_VENDOR_AUTOCOMPLETE_WITH_ADDR
|
SEARCH_VENDOR_AUTOCOMPLETE_WITH_ADDR,
|
||||||
|
{
|
||||||
|
skip: !isModalVisible,
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const showModal = () => {
|
const showModal = () => {
|
||||||
@@ -198,7 +201,7 @@ export function Jobd3RdPartyModal({ bodyshop, jobId }) {
|
|||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
message: t("general.validation.required"),
|
//message: t("general.validation.required"),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -15,34 +15,36 @@ const mapDispatchToProps = (dispatch) => ({
|
|||||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||||
});
|
});
|
||||||
|
|
||||||
export function ScheduleAtChange({ bodyshop, event }) {
|
export function JobAltTransportChange({ bodyshop, job }) {
|
||||||
const [updateJob] = useMutation(UPDATE_JOB);
|
const [updateJob] = useMutation(UPDATE_JOB);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const onClick = async ({ key }) => {
|
const onClick = async ({ key }) => {
|
||||||
const result = await updateJob({
|
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) {
|
if (!!!result.errors) {
|
||||||
notification["success"]({ message: t("appointments.successes.saved") });
|
// notification["success"]({ message: t("appointments.successes.saved") });
|
||||||
} else {
|
} else {
|
||||||
notification["error"]({
|
notification["error"]({
|
||||||
message: t("appointments.errors.saving", {
|
message: t("jobs.errors.saving", {
|
||||||
error: JSON.stringify(result.errors),
|
error: JSON.stringify(result.errors),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const menu = (
|
const menu = (
|
||||||
<Menu
|
<Menu selectedKeys={[job && job.alt_transport]} onClick={onClick}>
|
||||||
selectedKeys={[event.job && event.job.alt_transport]}
|
|
||||||
onClick={onClick}
|
|
||||||
>
|
|
||||||
{bodyshop.appt_alt_transport &&
|
{bodyshop.appt_alt_transport &&
|
||||||
bodyshop.appt_alt_transport.map((alt) => (
|
bodyshop.appt_alt_transport.map((alt) => (
|
||||||
<Menu.Item key={alt}>{alt}</Menu.Item>
|
<Menu.Item key={alt}>{alt}</Menu.Item>
|
||||||
))}
|
))}
|
||||||
|
<Menu.Divider />
|
||||||
|
<Menu.Item key={"null"}>{t("general.actions.clear")}</Menu.Item>
|
||||||
</Menu>
|
</Menu>
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
@@ -53,4 +55,7 @@ export function ScheduleAtChange({ bodyshop, event }) {
|
|||||||
</Dropdown>
|
</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 onClick = async ({ key }) => {
|
||||||
const result = await updateAppointment({
|
const result = await updateAppointment({
|
||||||
variables: { appid: event.id, app: { color: key } },
|
variables: {
|
||||||
|
appid: event.id,
|
||||||
|
app: { color: key === "null" ? null : key },
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!!!result.errors) {
|
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 = (
|
const menu = (
|
||||||
<Menu selectedKeys={[event.color]} onClick={onClick}>
|
<Menu selectedKeys={[event.color]} onClick={onClick}>
|
||||||
{bodyshop.appt_colors &&
|
{bodyshop.appt_colors &&
|
||||||
@@ -42,11 +52,15 @@ export function ScheduleEventColor({ bodyshop, event }) {
|
|||||||
{color.label}
|
{color.label}
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
))}
|
))}
|
||||||
|
<Menu.Divider />
|
||||||
|
<Menu.Item key={"null"}>{t("general.actions.clear")}</Menu.Item>
|
||||||
</Menu>
|
</Menu>
|
||||||
);
|
);
|
||||||
|
console.log(`event`, event);
|
||||||
return (
|
return (
|
||||||
<Dropdown overlay={menu}>
|
<Dropdown overlay={menu}>
|
||||||
<a href=" #" onClick={(e) => e.preventDefault()}>
|
<a href=" #" onClick={(e) => e.preventDefault()}>
|
||||||
|
{selectedColor}
|
||||||
<DownOutlined />
|
<DownOutlined />
|
||||||
</a>
|
</a>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Button, Popover, Space } from "antd";
|
import { Button, Popover, Space } from "antd";
|
||||||
import React from "react";
|
import React, { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
@@ -7,10 +7,10 @@ import { setModalContext } from "../../redux/modals/modals.actions";
|
|||||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||||
import PhoneFormatter from "../../utils/PhoneFormatter";
|
import PhoneFormatter from "../../utils/PhoneFormatter";
|
||||||
import { GenerateDocument } from "../../utils/RenderTemplate";
|
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 { 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) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
setScheduleContext: (context) =>
|
setScheduleContext: (context) =>
|
||||||
dispatch(setModalContext({ context: context, modal: "schedule" })),
|
dispatch(setModalContext({ context: context, modal: "schedule" })),
|
||||||
@@ -23,6 +23,16 @@ export function ScheduleEventComponent({
|
|||||||
setScheduleContext,
|
setScheduleContext,
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
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 = (
|
const popoverContent = (
|
||||||
<div>
|
<div>
|
||||||
{!event.isintake ? (
|
{!event.isintake ? (
|
||||||
@@ -67,12 +77,12 @@ export function ScheduleEventComponent({
|
|||||||
</DataLabel>
|
</DataLabel>
|
||||||
<DataLabel label={t("jobs.fields.alt_transport")}>
|
<DataLabel label={t("jobs.fields.alt_transport")}>
|
||||||
{(event.job && event.job.alt_transport) || ""}
|
{(event.job && event.job.alt_transport) || ""}
|
||||||
<ScheduleAtChange event={event} />
|
<ScheduleAtChange job={event && event.job} />
|
||||||
</DataLabel>
|
</DataLabel>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<div className="imex-flex-row">
|
<Space wrap>
|
||||||
{event.job ? (
|
{event.job ? (
|
||||||
<Link to={`/manage/jobs/${event.job && event.job.id}`}>
|
<Link to={`/manage/jobs/${event.job && event.job.id}`}>
|
||||||
<Button>{t("appointments.actions.viewjob")}</Button>
|
<Button>{t("appointments.actions.viewjob")}</Button>
|
||||||
@@ -100,6 +110,7 @@ export function ScheduleEventComponent({
|
|||||||
<Button
|
<Button
|
||||||
disabled={event.arrived}
|
disabled={event.arrived}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
setVisible(false);
|
||||||
setScheduleContext({
|
setScheduleContext({
|
||||||
actions: { refetch: refetch },
|
actions: { refetch: refetch },
|
||||||
context: {
|
context: {
|
||||||
@@ -124,7 +135,7 @@ export function ScheduleEventComponent({
|
|||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -162,8 +173,10 @@ export function ScheduleEventComponent({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover
|
<Popover
|
||||||
|
visible={visible}
|
||||||
|
onVisibleChange={(vis) => setVisible(vis)}
|
||||||
trigger="click"
|
trigger="click"
|
||||||
content={popoverContent}
|
content={event.block ? blockContent : popoverContent}
|
||||||
style={{ height: "100%", width: "100%" }}
|
style={{ height: "100%", width: "100%" }}
|
||||||
>
|
>
|
||||||
{RegularEvent}
|
{RegularEvent}
|
||||||
@@ -30,26 +30,27 @@ export default function ScheduleEventContainer({ bodyshop, event, refetch }) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const jobUpdate = await updateJob({
|
if (event.job) {
|
||||||
variables: {
|
const jobUpdate = await updateJob({
|
||||||
jobId: event.job.id,
|
variables: {
|
||||||
|
jobId: event.job.id,
|
||||||
|
|
||||||
job: {
|
job: {
|
||||||
date_scheduled: null,
|
date_scheduled: null,
|
||||||
scheduled_in: null,
|
scheduled_in: null,
|
||||||
status: bodyshop.md_ro_statuses.default_imported,
|
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();
|
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 Dinero from "dinero.js";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@@ -50,7 +50,7 @@ export default function JobBillsTotalComponent({
|
|||||||
} else {
|
} else {
|
||||||
billCms = billCms.add(
|
billCms = billCms.add(
|
||||||
Dinero({
|
Dinero({
|
||||||
amount: Math.round((il.actual_price || 0) * -100),
|
amount: Math.round((il.actual_price || 0) * 100),
|
||||||
}).multiply(il.quantity)
|
}).multiply(il.quantity)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -73,64 +73,171 @@ export default function JobBillsTotalComponent({
|
|||||||
|
|
||||||
const discrepWithLbrAdj = discrepancy.add(lbrAdjustments);
|
const discrepWithLbrAdj = discrepancy.add(lbrAdjustments);
|
||||||
|
|
||||||
const discrepWithCms = discrepWithLbrAdj.subtract(billCms);
|
const discrepWithCms = discrepWithLbrAdj.add(billCms);
|
||||||
const creditsNotReceived = totalReturns.add(billCms); //billCms is tracked as a negative number.
|
const creditsNotReceived = totalReturns.subtract(billCms); //billCms is tracked as a negative number.
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card title={t("jobs.labels.jobtotals")}>
|
<Row gutter={16}>
|
||||||
<Space wrap size="large">
|
<Col span={18}>
|
||||||
<Statistic
|
<Card title={t("jobs.labels.jobtotals")} style={{ height: "100%" }}>
|
||||||
title={t("jobs.labels.rosaletotal")}
|
<Space wrap size="large">
|
||||||
value={totalPartsSublet.toFormat()}
|
<Tooltip
|
||||||
/>
|
title={
|
||||||
<Statistic
|
<div
|
||||||
title={t("bills.labels.retailtotal")}
|
dangerouslySetInnerHTML={{
|
||||||
value={billTotals.toFormat()}
|
__html: t("jobs.labels.plitooltips.partstotal"),
|
||||||
/>
|
}}
|
||||||
<Statistic
|
/>
|
||||||
title={t("bills.labels.discrepancy")}
|
}
|
||||||
valueStyle={{
|
>
|
||||||
color: discrepancy.getAmount() === 0 ? "green" : "red",
|
<Statistic
|
||||||
}}
|
title={t("jobs.labels.rosaletotal")}
|
||||||
value={discrepancy.toFormat()}
|
value={totalPartsSublet.toFormat()}
|
||||||
/>
|
/>
|
||||||
<Statistic
|
</Tooltip>
|
||||||
title={t("bills.labels.dedfromlbr")}
|
<Typography.Title>-</Typography.Title>
|
||||||
value={lbrAdjustments.toFormat()}
|
<Tooltip
|
||||||
/>
|
title={
|
||||||
<Statistic
|
<div
|
||||||
title={t("bills.labels.discrepwithlbradj")}
|
dangerouslySetInnerHTML={{
|
||||||
valueStyle={{
|
__html: t("jobs.labels.plitooltips.billtotal"),
|
||||||
color: discrepWithLbrAdj.getAmount() === 0 ? "green" : "red",
|
}}
|
||||||
}}
|
/>
|
||||||
value={discrepWithLbrAdj.toFormat()}
|
}
|
||||||
/>
|
>
|
||||||
<Statistic
|
<Statistic
|
||||||
title={t("bills.labels.billcmtotal")}
|
title={t("bills.labels.retailtotal")}
|
||||||
value={billCms.toFormat()}
|
value={billTotals.toFormat()}
|
||||||
/>
|
/>
|
||||||
<Statistic
|
</Tooltip>
|
||||||
title={t("bills.labels.discrepwithcms")}
|
<Typography.Title>=</Typography.Title>
|
||||||
valueStyle={{
|
<Tooltip
|
||||||
color: discrepWithCms.getAmount() === 0 ? "green" : "red",
|
title={
|
||||||
}}
|
<div
|
||||||
value={discrepWithCms.toFormat()}
|
dangerouslySetInnerHTML={{
|
||||||
/>
|
__html: t("jobs.labels.plitooltips.discrep1"),
|
||||||
<Statistic
|
}}
|
||||||
title={t("bills.labels.totalreturns")}
|
/>
|
||||||
value={totalReturns.toFormat()}
|
}
|
||||||
/>
|
>
|
||||||
<Statistic
|
<Statistic
|
||||||
title={t("bills.labels.creditsreceived")}
|
title={t("bills.labels.discrepancy")}
|
||||||
value={billCms.toFormat()}
|
valueStyle={{
|
||||||
/>
|
color: discrepancy.getAmount() === 0 ? "green" : "red",
|
||||||
<Statistic
|
}}
|
||||||
title={t("bills.labels.creditsnotreceived")}
|
value={discrepancy.toFormat()}
|
||||||
valueStyle={{
|
/>
|
||||||
color: creditsNotReceived.getAmount() === 0 ? "green" : "red",
|
</Tooltip>
|
||||||
}}
|
<Typography.Title>+</Typography.Title>
|
||||||
value={creditsNotReceived.toFormat()}
|
<Tooltip
|
||||||
/>
|
title={
|
||||||
</Space>
|
<div
|
||||||
</Card>
|
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 { 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 queryString from "query-string";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@@ -54,7 +54,12 @@ export function JobChecklistForm({
|
|||||||
(type === "intake" && bodyshop.md_ro_statuses.default_arrived) ||
|
(type === "intake" && bodyshop.md_ro_statuses.default_arrived) ||
|
||||||
(type === "deliver" && bodyshop.md_ro_statuses.default_delivered),
|
(type === "deliver" && bodyshop.md_ro_statuses.default_delivered),
|
||||||
...(type === "intake" && { actual_in: new Date() }),
|
...(type === "intake" && { actual_in: new Date() }),
|
||||||
|
...(type === "intake" && {
|
||||||
|
production_vars: {
|
||||||
|
...job.production_vars,
|
||||||
|
...values.production_vars,
|
||||||
|
},
|
||||||
|
}),
|
||||||
...(type === "intake" && {
|
...(type === "intake" && {
|
||||||
scheduled_completion: values.scheduled_completion,
|
scheduled_completion: values.scheduled_completion,
|
||||||
}),
|
}),
|
||||||
@@ -162,7 +167,7 @@ export function JobChecklistForm({
|
|||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
message: t("general.validation.required"),
|
//message: t("general.validation.required"),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
@@ -175,6 +180,13 @@ export function JobChecklistForm({
|
|||||||
>
|
>
|
||||||
<DateTimePicker />
|
<DateTimePicker />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name={["production_vars", "note"]}
|
||||||
|
label={t("jobs.fields.production_vars.note")}
|
||||||
|
disabled={readOnly}
|
||||||
|
>
|
||||||
|
<Input.TextArea rows={3} />
|
||||||
|
</Form.Item>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{type === "deliver" && (
|
{type === "deliver" && (
|
||||||
@@ -186,7 +198,7 @@ export function JobChecklistForm({
|
|||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true,
|
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"),
|
title: t("jobs.labels.sales"),
|
||||||
dataIndex: "sales",
|
dataIndex: "sales",
|
||||||
key: "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:
|
sortOrder:
|
||||||
state.sortedInfo.columnKey === "sales" && state.sortedInfo.order,
|
state.sortedInfo.columnKey === "sales" && state.sortedInfo.order,
|
||||||
},
|
},
|
||||||
@@ -37,7 +38,8 @@ export default function JobCostingPartsTable({ data, summaryData }) {
|
|||||||
title: t("jobs.labels.costs"),
|
title: t("jobs.labels.costs"),
|
||||||
dataIndex: "costs",
|
dataIndex: "costs",
|
||||||
key: "costs",
|
key: "costs",
|
||||||
sorter: (a, b) => a.costs - b.costs,
|
sorter: (a, b) =>
|
||||||
|
parseFloat(a.costs.substring(1)) - parseFloat(b.costs.substring(1)),
|
||||||
sortOrder:
|
sortOrder:
|
||||||
state.sortedInfo.columnKey === "costs" && state.sortedInfo.order,
|
state.sortedInfo.columnKey === "costs" && state.sortedInfo.order,
|
||||||
},
|
},
|
||||||
@@ -46,7 +48,10 @@ export default function JobCostingPartsTable({ data, summaryData }) {
|
|||||||
title: t("jobs.labels.gpdollars"),
|
title: t("jobs.labels.gpdollars"),
|
||||||
dataIndex: "gpdollars",
|
dataIndex: "gpdollars",
|
||||||
key: "gpdollars",
|
key: "gpdollars",
|
||||||
sorter: (a, b) => a.gpdollars - b.gpdollars,
|
sorter: (a, b) =>
|
||||||
|
parseFloat(a.gpdollars.substring(1)) -
|
||||||
|
parseFloat(b.gpdollars.substring(1)),
|
||||||
|
|
||||||
sortOrder:
|
sortOrder:
|
||||||
state.sortedInfo.columnKey === "gpdollars" && state.sortedInfo.order,
|
state.sortedInfo.columnKey === "gpdollars" && state.sortedInfo.order,
|
||||||
},
|
},
|
||||||
@@ -54,7 +59,9 @@ export default function JobCostingPartsTable({ data, summaryData }) {
|
|||||||
title: t("jobs.labels.gppercent"),
|
title: t("jobs.labels.gppercent"),
|
||||||
dataIndex: "gppercent",
|
dataIndex: "gppercent",
|
||||||
key: "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:
|
sortOrder:
|
||||||
state.sortedInfo.columnKey === "gppercent" && state.sortedInfo.order,
|
state.sortedInfo.columnKey === "gppercent" && state.sortedInfo.order,
|
||||||
},
|
},
|
||||||
@@ -70,6 +77,7 @@ export default function JobCostingPartsTable({ data, summaryData }) {
|
|||||||
.includes(searchText.toLowerCase())
|
.includes(searchText.toLowerCase())
|
||||||
);
|
);
|
||||||
|
|
||||||
|
console.log("data :>> ", data);
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Table
|
<Table
|
||||||
@@ -87,9 +95,11 @@ export default function JobCostingPartsTable({ data, summaryData }) {
|
|||||||
</Space>
|
</Space>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
scroll={{ x: "50%", y: "40rem" }}
|
scroll={{
|
||||||
|
x: "50%", //y: "40rem"
|
||||||
|
}}
|
||||||
onChange={handleTableChange}
|
onChange={handleTableChange}
|
||||||
pagination={{ position: "top", defaultPageSize: 25 }}
|
pagination={{ position: "top", defaultPageSize: 50 }}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
dataSource={filteredData}
|
dataSource={filteredData}
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ export default function JobCostingStatistics({ summaryData }) {
|
|||||||
/>
|
/>
|
||||||
<Statistic
|
<Statistic
|
||||||
value={summaryData.gppercentFormatted}
|
value={summaryData.gppercentFormatted}
|
||||||
suffix="%"
|
|
||||||
title={t("jobs.labels.gppercent")}
|
title={t("jobs.labels.gppercent")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { PrinterFilled } from "@ant-design/icons";
|
import { PrinterFilled } from "@ant-design/icons";
|
||||||
import { useQuery } from "@apollo/client";
|
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 queryString from "query-string";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
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 { QUERY_JOB_CARD_DETAILS } from "../../graphql/jobs.queries";
|
||||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||||
import AlertComponent from "../alert/alert.component";
|
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 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 JobDetailCardsDamageComponent from "./job-detail-cards.damage.component";
|
||||||
import JobDetailCardsDatesComponent from "./job-detail-cards.dates.component";
|
import JobDetailCardsDatesComponent from "./job-detail-cards.dates.component";
|
||||||
import JobDetailCardsDocumentsComponent from "./job-detail-cards.documents.component";
|
import JobDetailCardsDocumentsComponent from "./job-detail-cards.documents.component";
|
||||||
@@ -25,6 +25,12 @@ const mapDispatchToProps = (dispatch) => ({
|
|||||||
dispatch(setModalContext({ context: context, modal: "printCenter" })),
|
dispatch(setModalContext({ context: context, modal: "printCenter" })),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const span = {
|
||||||
|
sm: { span: 24 },
|
||||||
|
md: { span: 12 },
|
||||||
|
lg: { span: 8 },
|
||||||
|
};
|
||||||
|
|
||||||
export function JobDetailCards({ setPrintCenterContext }) {
|
export function JobDetailCards({ setPrintCenterContext }) {
|
||||||
const selectedBreakpoint = Object.entries(Grid.useBreakpoint())
|
const selectedBreakpoint = Object.entries(Grid.useBreakpoint())
|
||||||
.filter((screen) => !!screen[1])
|
.filter((screen) => !!screen[1])
|
||||||
@@ -34,9 +40,9 @@ export function JobDetailCards({ setPrintCenterContext }) {
|
|||||||
xs: "100%",
|
xs: "100%",
|
||||||
sm: "100%",
|
sm: "100%",
|
||||||
md: "100%",
|
md: "100%",
|
||||||
lg: "50%",
|
lg: "75%",
|
||||||
xl: "50%",
|
xl: "75%",
|
||||||
xxl: "45%",
|
xxl: "60%",
|
||||||
};
|
};
|
||||||
const drawerPercentage = selectedBreakpoint
|
const drawerPercentage = selectedBreakpoint
|
||||||
? bpoints[selectedBreakpoint[0]]
|
? bpoints[selectedBreakpoint[0]]
|
||||||
@@ -46,7 +52,6 @@ export function JobDetailCards({ setPrintCenterContext }) {
|
|||||||
const { selected } = searchParams;
|
const { selected } = searchParams;
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const { loading, error, data, refetch } = useQuery(QUERY_JOB_CARD_DETAILS, {
|
const { loading, error, data, refetch } = useQuery(QUERY_JOB_CARD_DETAILS, {
|
||||||
fetchPolicy: "network-only",
|
|
||||||
variables: { id: selected },
|
variables: { id: selected },
|
||||||
skip: !selected,
|
skip: !selected,
|
||||||
});
|
});
|
||||||
@@ -60,10 +65,7 @@ export function JobDetailCards({ setPrintCenterContext }) {
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
const gridStyle = {
|
|
||||||
width: "25%",
|
|
||||||
textAlign: "center",
|
|
||||||
};
|
|
||||||
return (
|
return (
|
||||||
<Drawer
|
<Drawer
|
||||||
visible={!!selected}
|
visible={!!selected}
|
||||||
@@ -75,99 +77,98 @@ export function JobDetailCards({ setPrintCenterContext }) {
|
|||||||
{loading ? <LoadingSpinner /> : null}
|
{loading ? <LoadingSpinner /> : null}
|
||||||
{error ? <AlertComponent message={error.message} type="error" /> : null}
|
{error ? <AlertComponent message={error.message} type="error" /> : null}
|
||||||
{data ? (
|
{data ? (
|
||||||
<PageHeader
|
<Card
|
||||||
// ghost={true}
|
title={
|
||||||
tags={[
|
<Link to={`/manage/jobs/${data.jobs_by_pk.id}`}>
|
||||||
<OwnerTagPopoverComponent key="owner" job={data.jobs_by_pk} />,
|
{data.jobs_by_pk.ro_number || t("general.labels.na")}
|
||||||
<VehicleTagPopoverComponent key="vehicle" job={data.jobs_by_pk} />,
|
</Link>
|
||||||
<Tag
|
}
|
||||||
color="#f50"
|
extra={
|
||||||
key="production"
|
<Space wrap>
|
||||||
style={{
|
<JobSyncButton job={data.jobs_by_pk} />
|
||||||
display:
|
|
||||||
data && data.jobs_by_pk && data.jobs_by_pk.inproduction
|
<Button
|
||||||
? ""
|
onClick={() => {
|
||||||
: "none",
|
setPrintCenterContext({
|
||||||
}}
|
actions: { refetch: refetch },
|
||||||
>
|
context: {
|
||||||
{t("jobs.labels.inproduction")}
|
id: data.jobs_by_pk.id,
|
||||||
</Tag>,
|
job: data.jobs_by_pk,
|
||||||
]}
|
type: "job",
|
||||||
subTitle={data.jobs_by_pk.status}
|
},
|
||||||
>
|
});
|
||||||
<Card
|
}}
|
||||||
title={
|
>
|
||||||
<Link to={`/manage/jobs/${data.jobs_by_pk.id}`}>
|
<PrinterFilled />
|
||||||
{data.jobs_by_pk.ro_number || t("general.labels.na")}
|
{t("jobs.actions.printCenter")}
|
||||||
|
</Button>
|
||||||
|
<Link to={`/manage/jobs/${data.jobs_by_pk.id}?tab=repairdata`}>
|
||||||
|
<Button>{t("parts.actions.order")}</Button>
|
||||||
</Link>
|
</Link>
|
||||||
}
|
</Space>
|
||||||
extra={
|
}
|
||||||
<Space>
|
>
|
||||||
<Button
|
<JobsDetailHeader job={data ? data.jobs_by_pk : null} />
|
||||||
onClick={() => {
|
<Divider type="horizontal" />
|
||||||
setPrintCenterContext({
|
<Row gutter={[16, 16]}>
|
||||||
actions: { refetch: refetch },
|
<Col {...span}>
|
||||||
context: {
|
<Card.Grid style={{ width: "100%", height: "100%" }}>
|
||||||
id: data.jobs_by_pk.id,
|
<JobDetailCardsInsuranceComponent
|
||||||
job: data.jobs_by_pk,
|
loading={loading}
|
||||||
type: "job",
|
data={data ? data.jobs_by_pk : null}
|
||||||
},
|
/>
|
||||||
});
|
</Card.Grid>
|
||||||
}}
|
</Col>
|
||||||
>
|
<Col {...span}>
|
||||||
<PrinterFilled />
|
<Card.Grid style={{ width: "100%", height: "100%" }}>
|
||||||
{t("jobs.actions.printCenter")}
|
<JobDetailCardsTotalsComponent
|
||||||
</Button>
|
loading={loading}
|
||||||
<Link to={`/manage/jobs/${data.jobs_by_pk.id}?tab=repairdata`}>
|
data={data ? data.jobs_by_pk : null}
|
||||||
<Button>{t("parts.actions.order")}</Button>
|
/>
|
||||||
</Link>
|
</Card.Grid>
|
||||||
</Space>
|
</Col>
|
||||||
}
|
<Col {...span}>
|
||||||
>
|
<Card.Grid style={{ width: "100%", height: "100%" }}>
|
||||||
<Card.Grid style={gridStyle}>
|
<JobDetailCardsDatesComponent
|
||||||
<JobDetailCardsInsuranceComponent
|
loading={loading}
|
||||||
loading={loading}
|
data={data ? data.jobs_by_pk : null}
|
||||||
data={data ? data.jobs_by_pk : null}
|
/>
|
||||||
/>
|
</Card.Grid>
|
||||||
</Card.Grid>
|
</Col>
|
||||||
<Card.Grid style={gridStyle}>
|
<Col {...span}>
|
||||||
<JobDetailCardsTotalsComponent
|
<Card.Grid style={{ width: "100%", height: "100%" }}>
|
||||||
loading={loading}
|
<JobDetailCardsPartsComponent
|
||||||
data={data ? data.jobs_by_pk : null}
|
loading={loading}
|
||||||
/>
|
data={data ? data.jobs_by_pk : null}
|
||||||
</Card.Grid>
|
/>
|
||||||
<Card.Grid style={gridStyle}>
|
</Card.Grid>
|
||||||
<JobDetailCardsDatesComponent
|
</Col>
|
||||||
loading={loading}
|
<Col {...span}>
|
||||||
data={data ? data.jobs_by_pk : null}
|
<Card.Grid style={{ width: "100%", height: "100%" }}>
|
||||||
/>
|
<JobDetailCardsNotesComponent
|
||||||
</Card.Grid>
|
loading={loading}
|
||||||
<Card.Grid style={gridStyle}>
|
data={data ? data.jobs_by_pk : null}
|
||||||
<JobDetailCardsPartsComponent
|
/>
|
||||||
loading={loading}
|
</Card.Grid>
|
||||||
data={data ? data.jobs_by_pk : null}
|
</Col>
|
||||||
/>
|
<Col {...span}>
|
||||||
</Card.Grid>
|
<Card.Grid style={{ width: "100%", height: "100%" }}>
|
||||||
<Card.Grid style={gridStyle}>
|
<JobDetailCardsDocumentsComponent
|
||||||
<JobDetailCardsNotesComponent
|
loading={loading}
|
||||||
loading={loading}
|
data={data ? data.jobs_by_pk : null}
|
||||||
data={data ? data.jobs_by_pk : null}
|
/>
|
||||||
/>
|
</Card.Grid>
|
||||||
</Card.Grid>
|
</Col>
|
||||||
<Card.Grid style={gridStyle}>
|
<Col {...span}>
|
||||||
<JobDetailCardsDocumentsComponent
|
<Card.Grid style={{ width: "100%", height: "100%" }}>
|
||||||
loading={loading}
|
<JobDetailCardsDamageComponent
|
||||||
data={data ? data.jobs_by_pk : null}
|
loading={loading}
|
||||||
/>
|
data={data ? data.jobs_by_pk : null}
|
||||||
</Card.Grid>
|
/>
|
||||||
<Card.Grid style={gridStyle}>
|
</Card.Grid>
|
||||||
<JobDetailCardsDamageComponent
|
</Col>
|
||||||
loading={loading}
|
</Row>
|
||||||
data={data ? data.jobs_by_pk : null}
|
</Card>
|
||||||
/>
|
|
||||||
</Card.Grid>
|
|
||||||
</Card>
|
|
||||||
</PageHeader>
|
|
||||||
) : null}
|
) : null}
|
||||||
</Drawer>
|
</Drawer>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Carousel } from "antd";
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import CardTemplate from "./job-detail-cards.template.component";
|
import CardTemplate from "./job-detail-cards.template.component";
|
||||||
|
import { DetermineFileType } from "../documents-upload/documents-upload.utility";
|
||||||
export default function JobDetailCardsDocumentsComponent({ loading, data }) {
|
export default function JobDetailCardsDocumentsComponent({ loading, data }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@@ -17,13 +17,18 @@ export default function JobDetailCardsDocumentsComponent({ loading, data }) {
|
|||||||
<CardTemplate
|
<CardTemplate
|
||||||
loading={loading}
|
loading={loading}
|
||||||
title={t("jobs.labels.cards.documents")}
|
title={t("jobs.labels.cards.documents")}
|
||||||
extraLink={`/manage/jobs/${data.id}?tab=documents`}>
|
extraLink={`/manage/jobs/${data.id}?tab=documents`}
|
||||||
|
>
|
||||||
{data.documents.length > 0 ? (
|
{data.documents.length > 0 ? (
|
||||||
<Carousel autoplay>
|
<Carousel autoplay>
|
||||||
{data.documents.map((item) => (
|
{data.documents.map((item) => (
|
||||||
<img
|
<img
|
||||||
key={item.id}
|
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}
|
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 { 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 React, { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
@@ -90,7 +104,10 @@ export function JobLinesComponent({
|
|||||||
sortOrder:
|
sortOrder:
|
||||||
state.sortedInfo.columnKey === "op_code_desc" && state.sortedInfo.order,
|
state.sortedInfo.columnKey === "op_code_desc" && state.sortedInfo.order,
|
||||||
ellipsis: true,
|
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"),
|
title: t("joblines.fields.part_type"),
|
||||||
@@ -293,10 +310,14 @@ export function JobLinesComponent({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleMark = (e) => {
|
const handleMark = (e) => {
|
||||||
setSelectedLines([
|
if (e.key === "clear") {
|
||||||
...selectedLines,
|
setSelectedLines([]);
|
||||||
...jobLines.filter((item) => item.part_type === e.key),
|
} else {
|
||||||
]);
|
setSelectedLines([
|
||||||
|
...selectedLines,
|
||||||
|
...jobLines.filter((item) => item.part_type === e.key),
|
||||||
|
]);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const markMenu = (
|
const markMenu = (
|
||||||
@@ -305,6 +326,8 @@ export function JobLinesComponent({
|
|||||||
<Menu.Item key="PAN">{t("joblines.fields.part_types.PAN")}</Menu.Item>
|
<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="PAL">{t("joblines.fields.part_types.PAL")}</Menu.Item>
|
||||||
<Menu.Item key="PAS">{t("joblines.fields.part_types.PAS")}</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>
|
</Menu>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -318,6 +341,15 @@ export function JobLinesComponent({
|
|||||||
<Button onClick={() => refetch()}>
|
<Button onClick={() => refetch()}>
|
||||||
<SyncOutlined />
|
<SyncOutlined />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
{job.special_coverage_policy && (
|
||||||
|
<Tag color="tomato">
|
||||||
|
<Space>
|
||||||
|
<WarningFilled />
|
||||||
|
<span>{t("jobs.labels.specialcoveragepolicy")}</span>
|
||||||
|
</Space>
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
<Button
|
<Button
|
||||||
disabled={
|
disabled={
|
||||||
(job && !job.converted) ||
|
(job && !job.converted) ||
|
||||||
@@ -329,6 +361,7 @@ export function JobLinesComponent({
|
|||||||
actions: { refetch: refetch },
|
actions: { refetch: refetch },
|
||||||
context: {
|
context: {
|
||||||
jobId: job.id,
|
jobId: job.id,
|
||||||
|
job: job,
|
||||||
linesToOrder: selectedLines,
|
linesToOrder: selectedLines,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -338,6 +371,7 @@ export function JobLinesComponent({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t("parts.actions.order")}
|
{t("parts.actions.order")}
|
||||||
|
{selectedLines.length > 0 && ` (${selectedLines.length})`}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -354,7 +388,6 @@ export function JobLinesComponent({
|
|||||||
<Dropdown overlay={markMenu} trigger={["click"]}>
|
<Dropdown overlay={markMenu} trigger={["click"]}>
|
||||||
<Button>{t("jobs.actions.mark")}</Button>
|
<Button>{t("jobs.actions.mark")}</Button>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
disabled={jobRO}
|
disabled={jobRO}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -366,7 +399,6 @@ export function JobLinesComponent({
|
|||||||
>
|
>
|
||||||
{t("joblines.actions.new")}
|
{t("joblines.actions.new")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Input.Search
|
<Input.Search
|
||||||
placeholder={t("general.labels.search")}
|
placeholder={t("general.labels.search")}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
@@ -387,6 +419,18 @@ export function JobLinesComponent({
|
|||||||
scroll={{
|
scroll={{
|
||||||
x: true,
|
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={{
|
rowSelection={{
|
||||||
selectedRowKeys: selectedLines.map((item) => item.id),
|
selectedRowKeys: selectedLines.map((item) => item.id),
|
||||||
onSelectAll: (selected, selectedRows, changeRows) => {
|
onSelectAll: (selected, selectedRows, changeRows) => {
|
||||||
|
|||||||
@@ -23,7 +23,9 @@ export function JobEmployeeAssignments({
|
|||||||
jobRO,
|
jobRO,
|
||||||
body,
|
body,
|
||||||
refinish,
|
refinish,
|
||||||
|
|
||||||
prep,
|
prep,
|
||||||
|
csr,
|
||||||
handleAdd,
|
handleAdd,
|
||||||
handleRemove,
|
handleRemove,
|
||||||
loading,
|
loading,
|
||||||
@@ -155,6 +157,30 @@ export function JobEmployeeAssignments({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</DataLabel>
|
</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>
|
</Spin>
|
||||||
</Popover>
|
</Popover>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ export default function JobEmployeeAssignmentsContainer({ job, refetch }) {
|
|||||||
body={job.employee_body_rel}
|
body={job.employee_body_rel}
|
||||||
refinish={job.employee_refinish_rel}
|
refinish={job.employee_refinish_rel}
|
||||||
prep={job.employee_prep_rel}
|
prep={job.employee_prep_rel}
|
||||||
|
csr={job.employee_csr_rel}
|
||||||
handleAdd={handleAdd}
|
handleAdd={handleAdd}
|
||||||
handleRemove={handleRemove}
|
handleRemove={handleRemove}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
@@ -75,6 +76,8 @@ const determineFieldName = (operation) => {
|
|||||||
return "employee_body";
|
return "employee_body";
|
||||||
case "prep":
|
case "prep":
|
||||||
return "employee_prep";
|
return "employee_prep";
|
||||||
|
case "csr":
|
||||||
|
return "employee_csr";
|
||||||
case "refinish":
|
case "refinish":
|
||||||
return "employee_refinish";
|
return "employee_refinish";
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { WarningFilled } from "@ant-design/icons";
|
||||||
export default function JobLinesBillRefernece({ jobline }) {
|
export default function JobLinesBillRefernece({ jobline }) {
|
||||||
const billLine = jobline.billlines && jobline.billlines[0];
|
const billLine = jobline.billlines && jobline.billlines[0];
|
||||||
|
|
||||||
if (!billLine) return null;
|
if (!billLine) return null;
|
||||||
|
const subletRequired = billLine.actual_price !== jobline.act_price;
|
||||||
return (
|
return (
|
||||||
<div>{`${(billLine.actual_price * billLine.quantity).toFixed(2)} (${
|
<div style={{ color: subletRequired && "tomato" }}>
|
||||||
billLine.bill.vendor.name
|
{subletRequired && <WarningFilled />}
|
||||||
})`}</div>
|
{`${(billLine.actual_price * billLine.quantity).toFixed(2)} (${
|
||||||
|
billLine.bill.vendor.name
|
||||||
|
})`}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export default function JobLinesUpsertModalComponent({
|
|||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
message: t("general.validation.required"),
|
//message: t("general.validation.required"),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
name="line_desc"
|
name="line_desc"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Checkbox, Table } from "antd";
|
import { Checkbox, PageHeader, Table } from "antd";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||||
@@ -86,7 +86,7 @@ export default function JobReconciliationBillsTable({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<PageHeader title={t("bills.labels.bills")}>
|
||||||
<Table
|
<Table
|
||||||
pagination={false}
|
pagination={false}
|
||||||
scroll={{ y: "40vh", x: true }}
|
scroll={{ y: "40vh", x: true }}
|
||||||
@@ -99,6 +99,6 @@ export default function JobReconciliationBillsTable({
|
|||||||
selectedRowKeys: selectedLines,
|
selectedRowKeys: selectedLines,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</PageHeader>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Table } from "antd";
|
import { PageHeader, Table } from "antd";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||||
@@ -102,7 +102,7 @@ export default function JobReconcilitionPartsTable({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<PageHeader title={t("jobs.labels.lines")}>
|
||||||
<Table
|
<Table
|
||||||
pagination={false}
|
pagination={false}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
@@ -122,6 +122,6 @@ export default function JobReconcilitionPartsTable({
|
|||||||
<div style={{ fontStyle: "italic", margin: "4px" }}>
|
<div style={{ fontStyle: "italic", margin: "4px" }}>
|
||||||
{t("jobs.labels.reconciliation.removedpartsstrikethrough")}
|
{t("jobs.labels.reconciliation.removedpartsstrikethrough")}
|
||||||
</div>
|
</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 FormDatePicker from "../form-date-picker/form-date-picker.component";
|
||||||
import InputNumberCalculator from "../form-input-number-calculator/form-input-number-calculator.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 { t } = useTranslation();
|
||||||
const [insertScoreboardEntry] = useMutation(INSERT_SCOREBOARD_ENTRY);
|
const [insertScoreboardEntry] = useMutation(INSERT_SCOREBOARD_ENTRY);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
@@ -53,7 +57,7 @@ export default function ScoreboardAddButton({ job, ...otherBtnProps }) {
|
|||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
message: t("general.validation.required"),
|
//message: t("general.validation.required"),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
@@ -65,7 +69,7 @@ export default function ScoreboardAddButton({ job, ...otherBtnProps }) {
|
|||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
message: t("general.validation.required"),
|
//message: t("general.validation.required"),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
@@ -77,7 +81,7 @@ export default function ScoreboardAddButton({ job, ...otherBtnProps }) {
|
|||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
message: t("general.validation.required"),
|
//message: t("general.validation.required"),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
@@ -118,7 +122,12 @@ export default function ScoreboardAddButton({ job, ...otherBtnProps }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover content={overlay} visible={visibility}>
|
<Popover content={overlay} visible={visibility}>
|
||||||
<Button loading={loading} onClick={handleClick} {...otherBtnProps}>
|
<Button
|
||||||
|
loading={loading}
|
||||||
|
disabled={disabled}
|
||||||
|
onClick={handleClick}
|
||||||
|
{...otherBtnProps}
|
||||||
|
>
|
||||||
{t("jobs.actions.addtoscoreboard")}
|
{t("jobs.actions.addtoscoreboard")}
|
||||||
</Button>
|
</Button>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { LoadingOutlined } from "@ant-design/icons";
|
|||||||
import { useLazyQuery } from "@apollo/client";
|
import { useLazyQuery } from "@apollo/client";
|
||||||
import { Empty, Select } from "antd";
|
import { Empty, Select } from "antd";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import React, { forwardRef, useEffect, useState } from "react";
|
import React, { forwardRef, useEffect } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import {
|
import {
|
||||||
SEARCH_JOBS_BY_ID_FOR_AUTOCOMPLETE,
|
SEARCH_JOBS_BY_ID_FOR_AUTOCOMPLETE,
|
||||||
@@ -13,13 +13,11 @@ const { Option } = Select;
|
|||||||
|
|
||||||
const JobSearchSelect = (
|
const JobSearchSelect = (
|
||||||
{
|
{
|
||||||
value,
|
|
||||||
onChange,
|
|
||||||
onBlur,
|
|
||||||
disabled,
|
disabled,
|
||||||
convertedOnly = false,
|
convertedOnly = false,
|
||||||
notExported = true,
|
notExported = true,
|
||||||
clm_no = false,
|
clm_no = false,
|
||||||
|
...restProps
|
||||||
},
|
},
|
||||||
ref
|
ref
|
||||||
) => {
|
) => {
|
||||||
@@ -52,20 +50,11 @@ const JobSearchSelect = (
|
|||||||
debouncedExecuteSearch({ variables: { search: value } });
|
debouncedExecuteSearch({ variables: { search: value } });
|
||||||
};
|
};
|
||||||
|
|
||||||
const [option, setOption] = useState(value);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (value === option && value) {
|
if (restProps.value) {
|
||||||
callIdSearch({ variables: { id: value } }); // Sometimes results in a no-op. Not sure how to fix.
|
callIdSearch({ variables: { id: restProps.value } }); // Sometimes results in a no-op. Not sure how to fix.
|
||||||
}
|
}
|
||||||
}, [value, option, callIdSearch]);
|
}, [restProps.value, callIdSearch]);
|
||||||
|
|
||||||
const handleSelect = (value) => {
|
|
||||||
setOption(value);
|
|
||||||
if (value !== option && onChange) {
|
|
||||||
onChange(value);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const theOptions = _.uniqBy(
|
const theOptions = _.uniqBy(
|
||||||
[
|
[
|
||||||
@@ -82,17 +71,13 @@ const JobSearchSelect = (
|
|||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
showSearch
|
showSearch
|
||||||
autoFocus
|
autoFocus
|
||||||
value={option}
|
|
||||||
style={{
|
style={{
|
||||||
width: "100%",
|
width: "100%",
|
||||||
}}
|
}}
|
||||||
filterOption={false}
|
filterOption={false}
|
||||||
onSearch={handleSearch}
|
onSearch={handleSearch}
|
||||||
// onChange={setOption}
|
|
||||||
onChange={handleSelect}
|
|
||||||
onSelect={handleSelect}
|
|
||||||
notFoundContent={loading ? <LoadingOutlined /> : <Empty />}
|
notFoundContent={loading ? <LoadingOutlined /> : <Empty />}
|
||||||
onBlur={onBlur}
|
{...restProps}
|
||||||
>
|
>
|
||||||
{theOptions
|
{theOptions
|
||||||
? theOptions.map((o) => (
|
? theOptions.map((o) => (
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||||
|
import { selectCurrentUser } from "../../redux/user/user.selectors";
|
||||||
import JobCalculateTotals from "../job-calculate-totals/job-calculate-totals.component";
|
import JobCalculateTotals from "../job-calculate-totals/job-calculate-totals.component";
|
||||||
import "./job-totals-table.styles.scss";
|
import "./job-totals-table.styles.scss";
|
||||||
import JobTotalsTableLabor from "./job-totals.table.labor.component";
|
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";
|
import JobTotalsTableTotals from "./job-totals.table.totals.component";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
//currentUser: selectCurrentUser
|
currentUser: selectCurrentUser,
|
||||||
jobRO: selectJobReadOnly,
|
jobRO: selectJobReadOnly,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -23,7 +24,7 @@ const colSpan = {
|
|||||||
lg: { span: 12 },
|
lg: { span: 12 },
|
||||||
};
|
};
|
||||||
|
|
||||||
export function JobsTotalsTableComponent({ jobRO, job }) {
|
export function JobsTotalsTableComponent({ jobRO, currentUser, job }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
if (!!!job.job_totals) {
|
if (!!!job.job_totals) {
|
||||||
@@ -66,28 +67,30 @@ export function JobsTotalsTableComponent({ jobRO, job }) {
|
|||||||
<JobTotalsTableTotals job={job} />
|
<JobTotalsTableTotals job={job} />
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={24}>
|
{currentUser.email.includes("@imex.") && (
|
||||||
<Card title="DEVELOPMENT USE ONLY">
|
<Col span={24}>
|
||||||
<JobCalculateTotals job={job} disabled={jobRO} />
|
<Card title="DEVELOPMENT USE ONLY">
|
||||||
<Collapse>
|
<JobCalculateTotals job={job} disabled={jobRO} />
|
||||||
<Collapse.Panel header="JSON Tree Totals">
|
<Collapse>
|
||||||
<div>
|
<Collapse.Panel header="JSON Tree Totals">
|
||||||
<pre>
|
<div>
|
||||||
{JSON.stringify(
|
<pre>
|
||||||
{
|
{JSON.stringify(
|
||||||
CIECA: job.cieca_ttl && job.cieca_ttl.data,
|
{
|
||||||
CIECASTL: job.cieca_stl && job.cieca_stl.data,
|
CIECA: job.cieca_ttl && job.cieca_ttl.data,
|
||||||
ImEXCalc: job.job_totals,
|
CIECASTL: job.cieca_stl && job.cieca_stl.data,
|
||||||
},
|
ImEXCalc: job.job_totals,
|
||||||
null,
|
},
|
||||||
2
|
null,
|
||||||
)}
|
2
|
||||||
</pre>
|
)}
|
||||||
</div>
|
</pre>
|
||||||
</Collapse.Panel>
|
</div>
|
||||||
</Collapse>
|
</Collapse.Panel>
|
||||||
</Card>
|
</Collapse>
|
||||||
</Col>
|
</Card>
|
||||||
|
</Col>
|
||||||
|
)}
|
||||||
</Row>
|
</Row>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|||||||
@@ -106,7 +106,12 @@ export default function JobTotalsTableLabor({ job }) {
|
|||||||
<strong>{t("jobs.labels.labor_rates_subtotal")}</strong>
|
<strong>{t("jobs.labels.labor_rates_subtotal")}</strong>
|
||||||
</Table.Summary.Cell>
|
</Table.Summary.Cell>
|
||||||
<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">
|
<Table.Summary.Cell align="right">
|
||||||
<strong>
|
<strong>
|
||||||
{Dinero(job.job_totals.rates.rates_subtotal).toFormat()}
|
{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.Row>
|
<Table.Summary.Row>
|
||||||
<Table.Summary.Cell>{t("jobs.labels.mapa")}</Table.Summary.Cell>
|
<Table.Summary.Cell>{t("jobs.labels.mapa")}</Table.Summary.Cell>
|
||||||
<Table.Summary.Cell>
|
<Table.Summary.Cell align="right">
|
||||||
{job.job_totals.rates.mapa.rate}
|
<CurrencyFormatter>
|
||||||
|
{job.job_totals.rates.mapa.rate}
|
||||||
|
</CurrencyFormatter>
|
||||||
</Table.Summary.Cell>
|
</Table.Summary.Cell>
|
||||||
<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>
|
||||||
<Table.Summary.Cell align="right">
|
<Table.Summary.Cell align="right">
|
||||||
{Dinero(job.job_totals.rates.mapa.total).toFormat()}
|
{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.Row>
|
<Table.Summary.Row>
|
||||||
<Table.Summary.Cell>{t("jobs.labels.mash")}</Table.Summary.Cell>
|
<Table.Summary.Cell>{t("jobs.labels.mash")}</Table.Summary.Cell>
|
||||||
<Table.Summary.Cell>
|
<Table.Summary.Cell align="right">
|
||||||
{job.job_totals.rates.mash.rate}
|
<CurrencyFormatter>
|
||||||
|
{job.job_totals.rates.mash.rate}
|
||||||
|
</CurrencyFormatter>
|
||||||
</Table.Summary.Cell>
|
</Table.Summary.Cell>
|
||||||
<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>
|
||||||
<Table.Summary.Cell align="right">
|
<Table.Summary.Cell align="right">
|
||||||
{Dinero(job.job_totals.rates.mash.total).toFormat()}
|
{Dinero(job.job_totals.rates.mash.total).toFormat()}
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export default function JobTotalsTableOther({ job }) {
|
|||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{
|
{
|
||||||
//title: t("joblines.fields.part_type"),
|
title: t("general.labels.item"),
|
||||||
dataIndex: "key",
|
dataIndex: "key",
|
||||||
key: "key",
|
key: "key",
|
||||||
sorter: (a, b) => alphaSort(a.key, b.key),
|
sorter: (a, b) => alphaSort(a.key, b.key),
|
||||||
@@ -82,7 +82,7 @@ export default function JobTotalsTableOther({ job }) {
|
|||||||
<strong>{t("jobs.labels.additionaltotal")}</strong>
|
<strong>{t("jobs.labels.additionaltotal")}</strong>
|
||||||
</Table.Summary.Cell>
|
</Table.Summary.Cell>
|
||||||
|
|
||||||
<Table.Summary.Cell>
|
<Table.Summary.Cell align="right">
|
||||||
<strong>
|
<strong>
|
||||||
{Dinero(job.job_totals.additional.total).toFormat()}
|
{Dinero(job.job_totals.additional.total).toFormat()}
|
||||||
</strong>
|
</strong>
|
||||||
@@ -93,7 +93,7 @@ export default function JobTotalsTableOther({ job }) {
|
|||||||
<strong>{t("jobs.labels.subletstotal")}</strong>
|
<strong>{t("jobs.labels.subletstotal")}</strong>
|
||||||
</Table.Summary.Cell>
|
</Table.Summary.Cell>
|
||||||
|
|
||||||
<Table.Summary.Cell>
|
<Table.Summary.Cell align="right">
|
||||||
<strong>
|
<strong>
|
||||||
{Dinero(job.job_totals.parts.sublets.total).toFormat()}
|
{Dinero(job.job_totals.parts.sublets.total).toFormat()}
|
||||||
</strong>
|
</strong>
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ export default function JobTotalsTableParts({ job }) {
|
|||||||
<strong>{t("jobs.labels.partstotal")}</strong>
|
<strong>{t("jobs.labels.partstotal")}</strong>
|
||||||
</Table.Summary.Cell>
|
</Table.Summary.Cell>
|
||||||
|
|
||||||
<Table.Summary.Cell>
|
<Table.Summary.Cell align="right">
|
||||||
<strong>
|
<strong>
|
||||||
{Dinero(job.job_totals.parts.parts.total).toFormat()}
|
{Dinero(job.job_totals.parts.parts.total).toFormat()}
|
||||||
</strong>
|
</strong>
|
||||||
|
|||||||
@@ -25,6 +25,11 @@ export default function JobTotalsTableTotals({ job }) {
|
|||||||
key: t("jobs.labels.federal_tax_amt"),
|
key: t("jobs.labels.federal_tax_amt"),
|
||||||
total: job.job_totals.totals.federal_tax,
|
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"),
|
key: t("jobs.fields.ded_amt"),
|
||||||
total: job.job_totals.totals.custPayable.deductible,
|
total: job.job_totals.totals.custPayable.deductible,
|
||||||
@@ -41,14 +46,11 @@ export default function JobTotalsTableTotals({ job }) {
|
|||||||
key: t("jobs.fields.depreciation_taxes"),
|
key: t("jobs.fields.depreciation_taxes"),
|
||||||
total: job.job_totals.totals.custPayable.dep_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"),
|
key: t("jobs.labels.total_cust_payable"),
|
||||||
total: job.job_totals.totals.custPayable.total,
|
total: job.job_totals.totals.custPayable.total,
|
||||||
|
bold: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: t("jobs.labels.net_repairs"),
|
key: t("jobs.labels.net_repairs"),
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { connect } from "react-redux";
|
|||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { UPDATE_JOB } from "../../graphql/jobs.queries";
|
import { UPDATE_JOB } from "../../graphql/jobs.queries";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
@@ -55,26 +54,24 @@ export function JobsAdminClass({ bodyshop, job }) {
|
|||||||
layout="vertical"
|
layout="vertical"
|
||||||
initialValues={job}
|
initialValues={job}
|
||||||
>
|
>
|
||||||
<LayoutFormRow>
|
<Form.Item
|
||||||
<Form.Item
|
name={["class"]}
|
||||||
name={["class"]}
|
label={t("jobs.fields.class")}
|
||||||
label={t("jobs.fields.class")}
|
rules={[
|
||||||
rules={[
|
{
|
||||||
{
|
required: bodyshop.enforce_class,
|
||||||
required: bodyshop.enforce_class,
|
//message: t("general.validation.required"),
|
||||||
message: t("general.validation.required"),
|
},
|
||||||
},
|
]}
|
||||||
]}
|
>
|
||||||
>
|
<Select>
|
||||||
<Select>
|
{bodyshop.md_classes.map((s) => (
|
||||||
{bodyshop.md_classes.map((s) => (
|
<Select.Option key={s} value={s}>
|
||||||
<Select.Option key={s} value={s}>
|
{s}
|
||||||
{s}
|
</Select.Option>
|
||||||
</Select.Option>
|
))}
|
||||||
))}
|
</Select>
|
||||||
</Select>
|
</Form.Item>
|
||||||
</Form.Item>
|
|
||||||
</LayoutFormRow>
|
|
||||||
</Form>
|
</Form>
|
||||||
|
|
||||||
<Popconfirm
|
<Popconfirm
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ export default function JobsAdminDatesChange({ job }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div>{t("jobs.labels.ownerassociation")}</div>
|
|
||||||
<Form
|
<Form
|
||||||
onFinish={handleFinish}
|
onFinish={handleFinish}
|
||||||
autoComplete={"off"}
|
autoComplete={"off"}
|
||||||
|
|||||||
@@ -38,12 +38,8 @@ export default function JobAdminDeleteIntake({ job }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<Button loading={loading} onClick={handleDelete}>
|
||||||
<div>{t("jobs.labels.deleteintake")}</div>
|
{t("jobs.labels.deleteintake")}
|
||||||
|
</Button>
|
||||||
<Button loading={loading} onClick={handleDelete}>
|
|
||||||
{t("general.actions.delete")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true,
|
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={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
message: t("general.validation.required"),
|
//message: t("general.validation.required"),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -5,18 +5,34 @@ import {
|
|||||||
SyncOutlined,
|
SyncOutlined,
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import { useMutation } from "@apollo/client";
|
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 React, { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { connect } from "react-redux";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
import {
|
import {
|
||||||
DELETE_ALL_AVAILABLE_JOBS,
|
DELETE_ALL_AVAILABLE_JOBS,
|
||||||
DELETE_AVAILABLE_JOB,
|
DELETE_AVAILABLE_JOB,
|
||||||
} from "../../graphql/available-jobs.queries";
|
} from "../../graphql/available-jobs.queries";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||||
import { TimeAgoFormatter } from "../../utils/DateFormatter";
|
import { TimeAgoFormatter } from "../../utils/DateFormatter";
|
||||||
import { alphaSort } from "../../utils/sorters";
|
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,
|
loading,
|
||||||
data,
|
data,
|
||||||
refetch,
|
refetch,
|
||||||
@@ -58,11 +74,11 @@ export default function JobsAvailableComponent({
|
|||||||
render: (text, record) =>
|
render: (text, record) =>
|
||||||
record.job ? (
|
record.job ? (
|
||||||
<Link to={`/manage/jobs/${record.job.id}`}>
|
<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>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
<div>
|
<div>
|
||||||
{(record.job && record.job_ro_number) || t("general.labels.na")}
|
{(record.job && record.job.ro_number) || t("general.labels.na")}
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@@ -132,31 +148,47 @@ export default function JobsAvailableComponent({
|
|||||||
{
|
{
|
||||||
title: t("general.labels.actions"),
|
title: t("general.labels.actions"),
|
||||||
key: "actions",
|
key: "actions",
|
||||||
render: (text, record) => (
|
render: (text, record) => {
|
||||||
<Space wrap>
|
const isClosed =
|
||||||
<Button
|
record.job &&
|
||||||
onClick={() => {
|
(record.job.status === bodyshop.md_ro_statuses.default_exported ||
|
||||||
deleteJob({ variables: { id: record.id } }).then((r) => {
|
record.job.status === bodyshop.md_ro_statuses.default_invoiced);
|
||||||
notification["success"]({
|
return (
|
||||||
message: t("jobs.successes.deleted"),
|
<Space wrap>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
deleteJob({ variables: { id: record.id } }).then((r) => {
|
||||||
|
notification["success"]({
|
||||||
|
message: t("jobs.successes.deleted"),
|
||||||
|
});
|
||||||
|
refetch();
|
||||||
});
|
});
|
||||||
refetch();
|
}}
|
||||||
});
|
>
|
||||||
}}
|
<DeleteFilled />
|
||||||
>
|
</Button>
|
||||||
<DeleteFilled />
|
{!isClosed && (
|
||||||
</Button>
|
<>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => addJobAsNew(record)}
|
onClick={() => addJobAsNew(record)}
|
||||||
disabled={record.issupplement}
|
disabled={record.issupplement}
|
||||||
>
|
>
|
||||||
<PlusCircleFilled />
|
<PlusCircleFilled />
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={() => addJobAsSupp(record)}>
|
<Button onClick={() => addJobAsSupp(record)}>
|
||||||
<DownloadOutlined />
|
<DownloadOutlined />
|
||||||
</Button>
|
</Button>
|
||||||
</Space>
|
</>
|
||||||
),
|
)}
|
||||||
|
{isClosed && (
|
||||||
|
<Alert
|
||||||
|
type="error"
|
||||||
|
message={t("jobs.labels.alreadyclosed")}
|
||||||
|
></Alert>
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -56,7 +56,13 @@ export function JobsChangeStatus({ job, bodyshop, jobRO }) {
|
|||||||
} else if (
|
} else if (
|
||||||
bodyshop.md_ro_statuses.post_production_statuses.includes(job.status)
|
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])
|
if (bodyshop.md_ro_statuses.production_statuses[0])
|
||||||
setOtherStages([bodyshop.md_ro_statuses.production_statuses[0]]);
|
setOtherStages([bodyshop.md_ro_statuses.production_statuses[0]]);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export function JobsCloseAutoAllocate({ bodyshop, joblines, form, disabled }) {
|
|||||||
} else {
|
} else {
|
||||||
ret.profitcenter_labor = null;
|
ret.profitcenter_labor = null;
|
||||||
}
|
}
|
||||||
|
//Verify that this is also manually updated in server/job-costing
|
||||||
if (!jl.part_type && !jl.mod_lbr_ty) {
|
if (!jl.part_type && !jl.mod_lbr_ty) {
|
||||||
const lineDesc = jl.line_desc.toLowerCase();
|
const lineDesc = jl.line_desc.toLowerCase();
|
||||||
if (lineDesc.includes("shop materials")) {
|
if (lineDesc.includes("shop materials")) {
|
||||||
|
|||||||
@@ -7,16 +7,27 @@ import { connect } from "react-redux";
|
|||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { auth } from "../../firebase/firebase.utils";
|
import { auth } from "../../firebase/firebase.utils";
|
||||||
import { UPDATE_JOB } from "../../graphql/jobs.queries";
|
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 { logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
|
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
|
currentUser: selectCurrentUser,
|
||||||
});
|
});
|
||||||
|
|
||||||
export function JobsCloseExportButton({ bodyshop, jobId, disabled }) {
|
export function JobsCloseExportButton({
|
||||||
|
bodyshop,
|
||||||
|
currentUser,
|
||||||
|
jobId,
|
||||||
|
disabled,
|
||||||
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [updateJob] = useMutation(UPDATE_JOB);
|
const [updateJob] = useMutation(UPDATE_JOB);
|
||||||
|
const [insertExportLog] = useMutation(INSERT_EXPORT_LOG);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const handleQbxml = async () => {
|
const handleQbxml = async () => {
|
||||||
logImEXEvent("jobs_close_export");
|
logImEXEvent("jobs_close_export");
|
||||||
@@ -72,14 +83,47 @@ export function JobsCloseExportButton({ bodyshop, jobId, disabled }) {
|
|||||||
const failedTransactions = PartnerResponse.data.filter((r) => !r.success);
|
const failedTransactions = PartnerResponse.data.filter((r) => !r.success);
|
||||||
if (failedTransactions.length > 0) {
|
if (failedTransactions.length > 0) {
|
||||||
//Uh oh. At least one was no good.
|
//Uh oh. At least one was no good.
|
||||||
failedTransactions.map((ft) =>
|
failedTransactions.forEach((ft) => {
|
||||||
notification["error"]({
|
//insert failed export log
|
||||||
|
notification.open({
|
||||||
|
// key: "failedexports",
|
||||||
|
type: "error",
|
||||||
message: t("jobs.errors.exporting", {
|
message: t("jobs.errors.exporting", {
|
||||||
error: ft.errorMessage || "",
|
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 {
|
} else {
|
||||||
|
//Insert success export log.
|
||||||
|
await insertExportLog({
|
||||||
|
variables: {
|
||||||
|
logs: [
|
||||||
|
{
|
||||||
|
bodyshopid: bodyshop.id,
|
||||||
|
jobid: jobId,
|
||||||
|
successful: true,
|
||||||
|
useremail: currentUser.email,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const jobUpdateResponse = await updateJob({
|
const jobUpdateResponse = await updateJob({
|
||||||
variables: {
|
variables: {
|
||||||
jobId: jobId,
|
jobId: jobId,
|
||||||
@@ -90,8 +134,10 @@ export function JobsCloseExportButton({ bodyshop, jobId, disabled }) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!!!jobUpdateResponse.errors) {
|
if (!jobUpdateResponse.errors) {
|
||||||
notification["success"]({
|
notification.open({
|
||||||
|
type: "success",
|
||||||
|
key: "jobsuccessexport",
|
||||||
message: t("jobs.successes.exported"),
|
message: t("jobs.successes.exported"),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ export function JobsCloseLines({ bodyshop, job, jobRO }) {
|
|||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: !!job.joblines[index].act_price,
|
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={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: !!job.joblines[index].mod_lbr_ty,
|
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 }) {
|
export function JobsConvertButton({ bodyshop, job, refetch, jobRO }) {
|
||||||
const [visible, setVisible] = useState(false);
|
const [visible, setVisible] = useState(false);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
const [mutationConvertJob] = useMutation(CONVERT_JOB_TO_RO);
|
const [mutationConvertJob] = useMutation(CONVERT_JOB_TO_RO);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
|
||||||
const handleConvert = async (values) => {
|
const handleConvert = async (values) => {
|
||||||
|
setLoading(true);
|
||||||
const res = await mutationConvertJob({
|
const res = await mutationConvertJob({
|
||||||
variables: { jobId: job.id, ...values },
|
variables: { jobId: job.id, ...values },
|
||||||
});
|
});
|
||||||
@@ -42,12 +45,14 @@ export function JobsConvertButton({ bodyshop, job, refetch, jobRO }) {
|
|||||||
});
|
});
|
||||||
setVisible(false);
|
setVisible(false);
|
||||||
}
|
}
|
||||||
|
setLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const popMenu = (
|
const popMenu = (
|
||||||
<div>
|
<div>
|
||||||
<Form
|
<Form
|
||||||
layout="vertical"
|
layout="vertical"
|
||||||
|
form={form}
|
||||||
onFinish={handleConvert}
|
onFinish={handleConvert}
|
||||||
initialValues={{ driveable: true, towin: false }}
|
initialValues={{ driveable: true, towin: false }}
|
||||||
>
|
>
|
||||||
@@ -57,7 +62,7 @@ export function JobsConvertButton({ bodyshop, job, refetch, jobRO }) {
|
|||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
message: t("general.validation.required"),
|
//message: t("general.validation.required"),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
@@ -69,24 +74,46 @@ export function JobsConvertButton({ bodyshop, job, refetch, jobRO }) {
|
|||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
{bodyshop.enforce_class && (
|
||||||
name={"class"}
|
<Form.Item
|
||||||
label={t("jobs.fields.class")}
|
name={"class"}
|
||||||
rules={[
|
label={t("jobs.fields.class")}
|
||||||
{
|
rules={[
|
||||||
required: bodyshop.enforce_class,
|
{
|
||||||
message: t("general.validation.required"),
|
required: bodyshop.enforce_class,
|
||||||
},
|
//message: t("general.validation.required"),
|
||||||
]}
|
},
|
||||||
>
|
]}
|
||||||
<Select>
|
>
|
||||||
{bodyshop.md_classes.map((s) => (
|
<Select>
|
||||||
<Select.Option key={s} value={s}>
|
{bodyshop.md_classes.map((s) => (
|
||||||
{s}
|
<Select.Option key={s} value={s}>
|
||||||
</Select.Option>
|
{s}
|
||||||
))}
|
</Select.Option>
|
||||||
</Select>
|
))}
|
||||||
</Form.Item>
|
</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
|
<Form.Item
|
||||||
label={t("jobs.fields.ca_gst_registrant")}
|
label={t("jobs.fields.ca_gst_registrant")}
|
||||||
name="ca_gst_registrant"
|
name="ca_gst_registrant"
|
||||||
@@ -109,7 +136,7 @@ export function JobsConvertButton({ bodyshop, job, refetch, jobRO }) {
|
|||||||
<Switch />
|
<Switch />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
<Button type="danger" htmlType="submit">
|
<Button type="danger" onClick={() => form.submit()} loading={loading}>
|
||||||
{t("jobs.actions.convert")}
|
{t("jobs.actions.convert")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={() => setVisible(false)}>
|
<Button onClick={() => setVisible(false)}>
|
||||||
@@ -129,6 +156,7 @@ export function JobsConvertButton({ bodyshop, job, refetch, jobRO }) {
|
|||||||
type="danger"
|
type="danger"
|
||||||
// style={{ display: job.converted ? "none" : "" }}
|
// style={{ display: job.converted ? "none" : "" }}
|
||||||
disabled={job.converted || jobRO}
|
disabled={job.converted || jobRO}
|
||||||
|
loading={loading}
|
||||||
onClick={() => setVisible(true)}
|
onClick={() => setVisible(true)}
|
||||||
>
|
>
|
||||||
{t("jobs.actions.convert")}
|
{t("jobs.actions.convert")}
|
||||||
|
|||||||
@@ -146,9 +146,6 @@ export function JobsCreateJobsInfo({ bodyshop, form, selected }) {
|
|||||||
</Collapse.Panel>
|
</Collapse.Panel>
|
||||||
<Collapse.Panel key="claim" header={t("menus.jobsdetail.claimdetail")}>
|
<Collapse.Panel key="claim" header={t("menus.jobsdetail.claimdetail")}>
|
||||||
<LayoutFormRow>
|
<LayoutFormRow>
|
||||||
<Form.Item label={t("jobs.fields.csr")} name="csr">
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item label={t("jobs.fields.loss_desc")} name="loss_desc">
|
<Form.Item label={t("jobs.fields.loss_desc")} name="loss_desc">
|
||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
@@ -298,12 +295,14 @@ export function JobsCreateJobsInfo({ bodyshop, form, selected }) {
|
|||||||
<Form.Item label={t("jobs.fields.rate_ma3s")} name="rate_ma3s">
|
<Form.Item label={t("jobs.fields.rate_ma3s")} name="rate_ma3s">
|
||||||
<CurrencyInput />
|
<CurrencyInput />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.rate_mabl")} name="rate_mabl">
|
{
|
||||||
<CurrencyInput />
|
// <Form.Item label={t("jobs.fields.rate_mabl")} name="rate_mabl">
|
||||||
</Form.Item>
|
// <CurrencyInput />
|
||||||
<Form.Item label={t("jobs.fields.rate_macs")} name="rate_macs">
|
// </Form.Item>
|
||||||
<CurrencyInput />
|
// <Form.Item label={t("jobs.fields.rate_macs")} name="rate_macs">
|
||||||
</Form.Item>
|
// <CurrencyInput />
|
||||||
|
// </Form.Item>
|
||||||
|
}
|
||||||
<Form.Item label={t("jobs.fields.rate_matd")} name="rate_matd">
|
<Form.Item label={t("jobs.fields.rate_matd")} name="rate_matd">
|
||||||
<CurrencyInput />
|
<CurrencyInput />
|
||||||
</Form.Item>
|
</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