Merged in test (pull request #53)

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

View File

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

View File

@@ -20,3 +20,5 @@ npx deadfile ./src/index.js --exclude build templates
cd client && yarn build && cd build && scp -r \*\* imex@prod-tor1.imex.online:~/bodyshop/client/build && cd .. &&cd .. 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!'

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,13 +3,14 @@
ssh-keygen -t rsa -C "your_email@example.com" 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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

25134
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,43 +4,44 @@
"private": true, "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",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -147,16 +147,6 @@ function BillEnterModalContainer({
}) })
); );
// await updateJobLines({
// variables: {
// ids: remainingValues.billlines
// .filter((il) => il.joblineid !== "noline")
// .map((li) => li.joblineid),
// status: bodyshop.md_order_statuses.default_received || "Received*",
// location: location,
// },
// });
///////////////////////// /////////////////////////
if (upload && upload.length > 0) { 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}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,26 +1,13 @@
import { EyeFilled, SyncOutlined } from "@ant-design/icons"; import { 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}

View File

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

View File

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

View File

@@ -24,6 +24,7 @@ export function ChatMediaSelector({
conversation, 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);
}; };

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 : []}
/> />

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,14 +1,33 @@
import { SyncOutlined } from "@ant-design/icons"; import { 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,

View File

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

View File

@@ -1,16 +1,22 @@
import { Button, Form, Input, InputNumber, PageHeader } from "antd"; import { WarningFilled } from "@ant-design/icons";
import { useApolloClient } from "@apollo/client";
import { Button, Form, Input, InputNumber, PageHeader, Space } from "antd";
import moment from "moment";
import React from "react"; import 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"),
}, },
]} ]}
> >

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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) => (

View File

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

View File

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

View File

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

View File

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

View File

@@ -137,6 +137,28 @@ export default function GlobalSearch() {
}; };
}), }),
}, },
{
label: renderTitle(t("menus.header.search.phonebook")),
options: data.search_phonebook.map((pb) => {
return {
key: pb.id,
value: `${pb.firstname || ""} ${pb.lastname || ""} ${
pb.company || ""
}`,
label: (
<Link to={`/manage/phonebook?phonebookentry=${pb.id}`}>
<Space wrap split={<Divider type="vertical" />}>
<span>{`${pb.firstname || ""} ${pb.lastname || ""} ${
pb.company || ""
}`}</span>
<PhoneNumberFormatter>{pb.phone1}</PhoneNumberFormatter>
<span>{pb.email}</span>
</Space>
</Link>
),
};
}),
},
] ]
: []; : [];
@@ -144,12 +166,12 @@ export default function GlobalSearch() {
return ( 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>
); );
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
import { Card, Space, Statistic } from "antd"; import { Card, Col, Row, Space, Statistic, Tooltip, Typography } from "antd";
import Dinero from "dinero.js"; import 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>
); );
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,20 @@
import { DeleteFilled, FilterFilled, SyncOutlined } from "@ant-design/icons"; import {
DeleteFilled,
FilterFilled,
SyncOutlined,
WarningFilled,
} from "@ant-design/icons";
import { useMutation } from "@apollo/client"; import { 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) => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -8,7 +8,11 @@ import { INSERT_SCOREBOARD_ENTRY } from "../../graphql/scoreboard.queries";
import FormDatePicker from "../form-date-picker/form-date-picker.component"; import 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>

View File

@@ -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) => (

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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")) {

View File

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

View File

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

View File

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

View File

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