Compare commits
249 Commits
bugfix/Pro
...
IO-2825-No
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
18da11f593 | ||
|
|
82ecb5533f | ||
|
|
d3289d85f1 | ||
|
|
e628b1364c | ||
|
|
6c421c1447 | ||
|
|
99369e7040 | ||
|
|
01cbdf14a9 | ||
|
|
f691aca241 | ||
|
|
85495a11e3 | ||
|
|
134ce05d27 | ||
|
|
3498fbc8f1 | ||
|
|
f49f72ce7f | ||
|
|
a5e3b6ce33 | ||
|
|
0fd945b859 | ||
|
|
879eba0247 | ||
|
|
bb49dd77a1 | ||
|
|
ae705322f8 | ||
|
|
36d92d4060 | ||
|
|
3ce2b1ab19 | ||
|
|
52e756a78a | ||
|
|
5a36cb7cf1 | ||
|
|
9138f4be16 | ||
|
|
df93357cec | ||
|
|
8ab23c4ca6 | ||
|
|
f179d69420 | ||
|
|
730a7a233d | ||
|
|
84ad10fa9c | ||
|
|
b1cda41f56 | ||
|
|
0bce921f69 | ||
|
|
97282740f5 | ||
|
|
150ae02978 | ||
|
|
abeffb2a19 | ||
|
|
70058b6bd4 | ||
|
|
67c63f81d9 | ||
|
|
fb8452b2bb | ||
|
|
1f281e8439 | ||
|
|
223c705e8f | ||
|
|
00d8b533f4 | ||
|
|
3b25a8fe07 | ||
|
|
a57b9cddb5 | ||
|
|
064ed1bb8b | ||
|
|
efda254981 | ||
|
|
3fa6b8b6ac | ||
|
|
4603643240 | ||
|
|
f82b02958f | ||
|
|
5d1f61753b | ||
|
|
883043cde3 | ||
|
|
85ce7c638d | ||
|
|
547e279693 | ||
|
|
b9bdefe898 | ||
|
|
c963cb6fcc | ||
|
|
fa6d4cce2a | ||
|
|
c7188d5a71 | ||
|
|
c333d72743 | ||
|
|
2a1ec4eff3 | ||
|
|
190b2e5b5c | ||
|
|
3e2a517272 | ||
|
|
a6cd35bd06 | ||
|
|
ba3032e553 | ||
|
|
20fd452335 | ||
|
|
8f83a5f5e6 | ||
|
|
4e417d1b10 | ||
|
|
3ee63a503a | ||
|
|
8dd0e12398 | ||
|
|
253b4d0ddc | ||
|
|
a8b0931659 | ||
|
|
11a182c68a | ||
|
|
2583d21cf1 | ||
|
|
aa6032d74f | ||
|
|
e1bac5fe9f | ||
|
|
0c74848c54 | ||
|
|
1c4f2d2de0 | ||
|
|
d67d2aa064 | ||
|
|
208f3281a9 | ||
|
|
fb7e3bc812 | ||
|
|
884adb963c | ||
|
|
29c4df9f76 | ||
|
|
f1940a320c | ||
|
|
a297bba193 | ||
|
|
375a3fe386 | ||
|
|
d837754ef6 | ||
|
|
49a1f0c42c | ||
|
|
a5033e10cd | ||
|
|
00f0952ab2 | ||
|
|
ec5b636c26 | ||
|
|
7126cc0d56 | ||
|
|
568d8887ce | ||
|
|
4927e84a88 | ||
|
|
d2d8039cb9 | ||
|
|
5da242d785 | ||
|
|
87f25b1ed4 | ||
|
|
4f9afc8578 | ||
|
|
defadf70e3 | ||
|
|
82be3853f0 | ||
|
|
ff184926fc | ||
|
|
0e9768c0f6 | ||
|
|
805c924a15 | ||
|
|
10ff8fc432 | ||
|
|
e2db0bab9f | ||
|
|
92ee5169ab | ||
|
|
5cf2345dca | ||
|
|
0accf6a18f | ||
|
|
52e46aad86 | ||
|
|
43f49e5e51 | ||
|
|
60908b123d | ||
|
|
5169fd178a | ||
|
|
bbc446ef01 | ||
|
|
65bb8c6f26 | ||
|
|
171f40819c | ||
|
|
4baa26cb25 | ||
|
|
01da7fde31 | ||
|
|
3c8234d715 | ||
|
|
866f242465 | ||
|
|
f690596825 | ||
|
|
5f494e4b78 | ||
|
|
7b814329da | ||
|
|
2fc0e1cf50 | ||
|
|
ba3f98aeb4 | ||
|
|
f49818ade3 | ||
|
|
b37c70494b | ||
|
|
ff1db26f41 | ||
|
|
3550c97966 | ||
|
|
956c686360 | ||
|
|
508b0d7711 | ||
|
|
abb1464e30 | ||
|
|
704d5415d6 | ||
|
|
6cc1cfd1b0 | ||
|
|
e6455a7fd3 | ||
|
|
a9aad2a751 | ||
|
|
1bf745345d | ||
|
|
63c71ed923 | ||
|
|
3a844aefa0 | ||
|
|
4a16df36dd | ||
|
|
93c92e8976 | ||
|
|
136d52ec0b | ||
|
|
9523e5534d | ||
|
|
d20048026c | ||
|
|
7ba3ed2b89 | ||
|
|
2cc0b7d741 | ||
|
|
970275e62a | ||
|
|
15c7b6f1c8 | ||
|
|
aa076da255 | ||
|
|
3149c3cfc6 | ||
|
|
f91e0ec39f | ||
|
|
c31d4096c6 | ||
|
|
118f14ed4c | ||
|
|
0d0791cc41 | ||
|
|
f45aa69917 | ||
|
|
4b44ff29ee | ||
|
|
f15f5d7309 | ||
|
|
ea55b1a797 | ||
|
|
668366e886 | ||
|
|
eb1b2aef55 | ||
|
|
906430add5 | ||
|
|
f8fbbd2323 | ||
|
|
adab6ef0c6 | ||
|
|
ddfd91617f | ||
|
|
8418b4cb91 | ||
|
|
e593943d99 | ||
|
|
b58b5e65dc | ||
|
|
6d393987e8 | ||
|
|
852231b503 | ||
|
|
870878d151 | ||
|
|
cdf7bcf839 | ||
|
|
8a4fee7aea | ||
|
|
fa578efee4 | ||
|
|
4b6b4c0c63 | ||
|
|
8199ab83ef | ||
|
|
8e50d0ba53 | ||
|
|
7d72d66a97 | ||
|
|
3454694887 | ||
|
|
2a51c8a2bf | ||
|
|
524f623fd4 | ||
|
|
7ab7e18d96 | ||
|
|
6f2c8dba5a | ||
|
|
162d8bfffe | ||
|
|
61569d97cb | ||
|
|
975622a31c | ||
|
|
7524d06126 | ||
|
|
7f75eeadb9 | ||
|
|
4db2b73397 | ||
|
|
2f493c63f8 | ||
|
|
2761356f02 | ||
|
|
8207a52b6b | ||
|
|
8bbe7a1f0f | ||
|
|
2b052b3d43 | ||
|
|
94e7bf3f92 | ||
|
|
64e3f5f2ac | ||
|
|
bb7f7deb50 | ||
|
|
55c10890bd | ||
|
|
edef0b194d | ||
|
|
37f0c98e75 | ||
|
|
48f18514af | ||
|
|
76c13700be | ||
|
|
8ae8737fe2 | ||
|
|
36f7b7a1a1 | ||
|
|
b0a5f2d998 | ||
|
|
19c980bc3b | ||
|
|
78d6b9699b | ||
|
|
82d8cb22df | ||
|
|
9c41af82d7 | ||
|
|
2997dd4e4d | ||
|
|
4602648fe5 | ||
|
|
a8f2ca5643 | ||
|
|
cd055f7344 | ||
|
|
c43709395f | ||
|
|
10f60752c8 | ||
|
|
870f8463db | ||
|
|
5567a69142 | ||
|
|
762fec1d9a | ||
|
|
0946b0d3a3 | ||
|
|
976dd2cbfb | ||
|
|
b6956b3649 | ||
|
|
6cf4a50a83 | ||
|
|
b2b4902d23 | ||
|
|
6f69129829 | ||
|
|
d846894d22 | ||
|
|
7d3101c877 | ||
|
|
c3f0e6f10c | ||
|
|
d0da2dd66c | ||
|
|
877b3ab39c | ||
|
|
8f031a78c1 | ||
|
|
69b36a4c34 | ||
|
|
c7b8df5655 | ||
|
|
d85768b2ac | ||
|
|
a569c1f4f9 | ||
|
|
07a8e5b216 | ||
|
|
38bf58c613 | ||
|
|
ba90d72d55 | ||
|
|
9889bee924 | ||
|
|
a19e4e8f16 | ||
|
|
5121852fbc | ||
|
|
1d1ff5d20b | ||
|
|
ec00697d31 | ||
|
|
c25714b68e | ||
|
|
dc0147c5f9 | ||
|
|
296afdbeee | ||
|
|
2f8f058c5c | ||
|
|
133d593689 | ||
|
|
68784018e6 | ||
|
|
19dfec2a34 | ||
|
|
55d729339f | ||
|
|
c3108a17f4 | ||
|
|
d47ae64bd6 | ||
|
|
5206ba7a74 | ||
|
|
095e1e9789 | ||
|
|
a0b9f99dd3 | ||
|
|
a33a92207b | ||
|
|
f647e1ff11 |
@@ -2,7 +2,7 @@ NGROK TEsting:
|
||||
./ngrok.exe http http://localhost:4000 -host-header="localhost:4000"
|
||||
|
||||
Finding deadfiles - run from client directory
|
||||
npx deadfile ./src/index.js --exclude build templates
|
||||
npx deadfile ./src/index.jsx --exclude build templates
|
||||
|
||||
#Crushing all hasura migrations by creating a new initialization from the server.
|
||||
hasura migrate create "Init" --from-server --endpoint https://db.imex.online/ --admin-secret 'Production-ImEXOnline!@#'
|
||||
@@ -11,4 +11,4 @@ Production-ImEXOnline!@#'
|
||||
hasura migrate status --endpoint https://db.imex.online/ --admin-secret 'Production-ImEXOnline!@#'
|
||||
|
||||
Generate the license file:
|
||||
$ generate-license-file --input package.json --output third-party-licenses.txt --overwrite
|
||||
$ generate-license-file --input package.json --output third-party-licenses.txt --overwrite
|
||||
|
||||
41
_reference/productionBoardNotes.md
Normal file
41
_reference/productionBoardNotes.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# Production Board Notes:
|
||||
|
||||
## General Notes
|
||||
|
||||
- You can single click the lane footer to collapse/un-collapse the lane
|
||||
- You can double click the lane header to collapse/un-collapse the lane
|
||||
- If you need to scroll horizontally, you can hold shift and use the mouse scroll wheel, or press the mouse scroll wheel while scrolling
|
||||
|
||||
## Board Settings
|
||||
|
||||
#### Layout
|
||||
|
||||
- Board Orientation (Vertical or Horizontal)
|
||||
- This determines the orientation of the card layout on the board.
|
||||
- Horizontal is the default setting, and how the prior board was set up.
|
||||
- Vertical is the new setting and allows lanes to be displayed vertically, with a grid of cards
|
||||
- Card Size (Small, Medium, Large)
|
||||
- This determines the size of the cards on the board.
|
||||
- Small is the default setting, and how the prior board was set up.
|
||||
- Medium and Large are new settings and allow for larger cards to be displayed on the board.
|
||||
- Compact Cards (Tall or Wide)
|
||||
- Formally called 'Compact'
|
||||
- When on, data is displayed on the card vertically
|
||||
- when turned off, some fields may share horizontal space, tightening the card layout
|
||||
- Colored Cards (On or Off)
|
||||
- When on, cards are colored based on the Status color
|
||||
- Kiosk Mode (On or Off)
|
||||
- This should be turned on if the shop is using it on a tablet (Ipad)
|
||||
|
||||
#### Information
|
||||
|
||||
These allow users to turn fields on or off, turning them all off will show the card in the most minimal form
|
||||
|
||||
|
||||
### Statistics
|
||||
|
||||
- The statistics section allows users to see accumulations of both jobs on the board, and jobs in production.
|
||||
- you can click a statistic to turn it on and off, and drag and drop the statistics to rearrange them
|
||||
|
||||
### Filters
|
||||
- Allows you to set, and persist filters for estimators and insurance companies
|
||||
@@ -4,7 +4,7 @@ Clone Repository for:
|
||||
{
|
||||
"name": "node-webhook-scripts",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"main": "index.jsx",
|
||||
"dependencies": {
|
||||
"express": "^4.16.4"
|
||||
},
|
||||
|
||||
@@ -11,7 +11,7 @@ module.exports = {
|
||||
|
||||
{
|
||||
name: "Bitbucket Webhook",
|
||||
script: "./webhook/index.js",
|
||||
script: "./webhook/index.jsx",
|
||||
env: {
|
||||
NODE_ENV: "production"
|
||||
}
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
// craco.config.js
|
||||
const TerserPlugin = require("terser-webpack-plugin");
|
||||
const CracoLessPlugin = require("craco-less");
|
||||
const { convertLegacyToken } = require("@ant-design/compatible/lib");
|
||||
const { theme } = require("antd/lib");
|
||||
|
||||
const { defaultAlgorithm, defaultSeed } = theme;
|
||||
|
||||
const mapToken = defaultAlgorithm(defaultSeed);
|
||||
const v4Token = convertLegacyToken(mapToken);
|
||||
|
||||
// TODO, At the moment we are using less in the Dashboard. Once we remove this we can remove the less processor entirely.
|
||||
|
||||
module.exports = {
|
||||
plugins: [
|
||||
{
|
||||
plugin: CracoLessPlugin,
|
||||
options: {
|
||||
lessLoaderOptions: {
|
||||
lessOptions: {
|
||||
modifyVars: { ...v4Token },
|
||||
javascriptEnabled: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
webpack: {
|
||||
configure: (webpackConfig) => {
|
||||
return {
|
||||
...webpackConfig,
|
||||
// Required for Dev Server
|
||||
devServer: {
|
||||
...webpackConfig.devServer,
|
||||
allowedHosts: "all"
|
||||
},
|
||||
optimization: {
|
||||
...webpackConfig.optimization,
|
||||
// Workaround for CircleCI bug caused by the number of CPUs shown
|
||||
// https://github.com/facebook/create-react-app/issues/8320
|
||||
minimizer: webpackConfig.optimization.minimizer.map((item) => {
|
||||
if (item instanceof TerserPlugin) {
|
||||
item.options.parallel = 2;
|
||||
}
|
||||
|
||||
return item;
|
||||
})
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
devtool: "source-map"
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
/// <reference types="cypress" />
|
||||
// ***********************************************************
|
||||
// This example plugins/index.js can be used to load plugins
|
||||
// This example plugins/index.jsx can be used to load plugins
|
||||
//
|
||||
// You can change the location of this file or turn off loading
|
||||
// the plugins file with the 'pluginsFile' configuration option.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// ***********************************************************
|
||||
// This example support/index.js is processed and
|
||||
// This example support/index.jsx is processed and
|
||||
// loaded automatically before your test files.
|
||||
//
|
||||
// This is a great place to put global configuration and
|
||||
|
||||
@@ -1,174 +1,177 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<% if (env.VITE_APP_INSTANCE === 'IMEX') { %>
|
||||
<link rel="icon" href="/favicon.png" />
|
||||
<% } %> <% if (env.VITE_APP_INSTANCE === 'ROME') { %>
|
||||
<link rel="icon" href="/ro-favicon.png" />
|
||||
<% } %> <% if (env.VITE_APP_INSTANCE === 'PROMANAGER') { %>
|
||||
<link rel="icon" href="/pm/pm-favicon.ico" />
|
||||
<% } %>
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<% if (env.VITE_APP_INSTANCE === 'IMEX') { %>
|
||||
<link rel="icon" href="/favicon.png"/>
|
||||
<% } %> <% if (env.VITE_APP_INSTANCE === 'ROME') { %>
|
||||
<link rel="icon" href="/ro-favicon.png"/>
|
||||
<% } %> <% if (env.VITE_APP_INSTANCE === 'PROMANAGER') { %>
|
||||
<link rel="icon" href="/pm/pm-favicon.ico"/>
|
||||
<% } %>
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#1690ff" />
|
||||
<!-- <link rel="apple-touch-icon" href="logo192.png" /> -->
|
||||
<!-- TODO:AIo Update the individual logos for each.-->
|
||||
<link rel="apple-touch-icon" href="public/logo192.png" />
|
||||
<link rel="mask-icon" href="/mask-icon.svg" color="#FFFFFF">
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<meta name="theme-color" content="#1690ff"/>
|
||||
<!-- <link rel="apple-touch-icon" href="logo192.png" /> -->
|
||||
<!-- TODO:AIo Update the individual logos for each.-->
|
||||
<link rel="apple-touch-icon" href="public/logo192.png"/>
|
||||
<link rel="mask-icon" href="/mask-icon.svg" color="#FFFFFF">
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<% if (env.VITE_APP_INSTANCE === 'IMEX') { %>
|
||||
<meta name="description" content="ImEX Online" />
|
||||
<title>ImEX Online</title>
|
||||
<script type="text/javascript">
|
||||
window.$crisp = [];
|
||||
window.CRISP_WEBSITE_ID = '36724f62-2eb0-4b29-9cdd-9905fb99913e';
|
||||
(function () {
|
||||
d = document;
|
||||
s = d.createElement('script');
|
||||
s.src = 'https://client.crisp.chat/l.js';
|
||||
s.async = 1;
|
||||
d.getElementsByTagName('head')[0].appendChild(s);
|
||||
})();
|
||||
</script>
|
||||
<% } %> <% if (env.VITE_APP_INSTANCE === 'ROME') { %>
|
||||
<meta name="description" content="Rome Online" />
|
||||
<title>Rome Online</title>
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<% if (env.VITE_APP_INSTANCE === 'IMEX') { %>
|
||||
<meta name="description" content="ImEX Online"/>
|
||||
<title>ImEX Online</title>
|
||||
<script type="text/javascript">
|
||||
window.$crisp = [];
|
||||
window.CRISP_WEBSITE_ID = '36724f62-2eb0-4b29-9cdd-9905fb99913e';
|
||||
(function () {
|
||||
d = document;
|
||||
s = d.createElement('script');
|
||||
s.src = 'https://client.crisp.chat/l.js';
|
||||
s.async = 1;
|
||||
d.getElementsByTagName('head')[0].appendChild(s);
|
||||
})();
|
||||
</script>
|
||||
<% } %> <% if (env.VITE_APP_INSTANCE === 'ROME') { %>
|
||||
<meta name="description" content="Rome Online"/>
|
||||
<title>Rome Online</title>
|
||||
|
||||
<!--Use the below code snippet to provide real time updates to the live chat plugin without the need of copying and paste each time to your website when changes are made via PBX-->
|
||||
<!--Use the below code snippet to provide real time updates to the live chat plugin without the need of copying and paste each time to your website when changes are made via PBX-->
|
||||
|
||||
<call-us-selector phonesystem-url=https://rometech.east.3cx.us:5001 party="LiveChat528346"></call-us-selector>
|
||||
<call-us-selector phonesystem-url=https://rometech.east.3cx.us:5001
|
||||
party="LiveChat528346"></call-us-selector>
|
||||
|
||||
<!--Incase you don't want real time updates to the live chat plugin when options are changed, use the below code snippet. Please note that each time you change the settings you will need to copy and paste the snippet code to your website-->
|
||||
|
||||
<!--<call-us
|
||||
|
||||
phonesystem-url=https://rometech.east.3cx.us:5001
|
||||
|
||||
style="position:fixed;font-size:16px;line-height:17px;z-index: 99999;right: 20px; bottom: 20px;"
|
||||
|
||||
id="wp-live-chat-by-3CX"
|
||||
|
||||
minimized="true"
|
||||
|
||||
animation-style="noanimation"
|
||||
|
||||
party="LiveChat528346"
|
||||
|
||||
minimized-style="bubbleright"
|
||||
|
||||
allow-call="true"
|
||||
|
||||
allow-video="false"
|
||||
|
||||
allow-soundnotifications="true"
|
||||
|
||||
enable-mute="true"
|
||||
|
||||
enable-onmobile="true"
|
||||
|
||||
offline-enabled="true"
|
||||
|
||||
enable="true"
|
||||
|
||||
ignore-queueownership="false"
|
||||
|
||||
authentication="both"
|
||||
|
||||
show-operator-actual-name="true"
|
||||
|
||||
aknowledge-received="true"
|
||||
|
||||
gdpr-enabled="false"
|
||||
|
||||
message-userinfo-format="name"
|
||||
|
||||
message-dateformat="both"
|
||||
|
||||
lang="browser"
|
||||
|
||||
button-icon-type="default"
|
||||
|
||||
greeting-visibility="none"
|
||||
|
||||
greeting-offline-visibility="none"
|
||||
|
||||
chat-delay="2000"
|
||||
|
||||
enable-direct-call="true"
|
||||
|
||||
enable-ga="false"
|
||||
|
||||
></call-us>-->
|
||||
|
||||
<script defer src=https://downloads-global.3cx.com/downloads/livechatandtalk/v1/callus.js id="tcx-callus-js" charset="utf-8"></script>
|
||||
<!--Incase you don't want real time updates to the live chat plugin when options are changed, use the below code snippet. Please note that each time you change the settings you will need to copy and paste the snippet code to your website-->
|
||||
|
||||
<!--<call-us
|
||||
|
||||
phonesystem-url=https://rometech.east.3cx.us:5001
|
||||
|
||||
style="position:fixed;font-size:16px;line-height:17px;z-index: 99999;right: 20px; bottom: 20px;"
|
||||
|
||||
id="wp-live-chat-by-3CX"
|
||||
|
||||
minimized="true"
|
||||
|
||||
animation-style="noanimation"
|
||||
|
||||
party="LiveChat528346"
|
||||
|
||||
minimized-style="bubbleright"
|
||||
|
||||
allow-call="true"
|
||||
|
||||
allow-video="false"
|
||||
|
||||
allow-soundnotifications="true"
|
||||
|
||||
enable-mute="true"
|
||||
|
||||
enable-onmobile="true"
|
||||
|
||||
offline-enabled="true"
|
||||
|
||||
enable="true"
|
||||
|
||||
ignore-queueownership="false"
|
||||
|
||||
authentication="both"
|
||||
|
||||
show-operator-actual-name="true"
|
||||
|
||||
aknowledge-received="true"
|
||||
|
||||
gdpr-enabled="false"
|
||||
|
||||
message-userinfo-format="name"
|
||||
|
||||
message-dateformat="both"
|
||||
|
||||
lang="browser"
|
||||
|
||||
button-icon-type="default"
|
||||
|
||||
greeting-visibility="none"
|
||||
|
||||
greeting-offline-visibility="none"
|
||||
|
||||
chat-delay="2000"
|
||||
|
||||
enable-direct-call="true"
|
||||
|
||||
enable-ga="false"
|
||||
|
||||
></call-us>-->
|
||||
|
||||
<script defer src=https://downloads-global.3cx.com/downloads/livechatandtalk/v1/callus.js
|
||||
id="tcx-callus-js" charset="utf-8"></script>
|
||||
|
||||
|
||||
<% } %> <% if (env.VITE_APP_INSTANCE === 'PROMANAGER') { %>
|
||||
<title>ProManager</title>
|
||||
<meta name="description" content="ProManager"/>
|
||||
|
||||
<% } %> <% if (env.VITE_APP_INSTANCE === 'PROMANAGER') { %>
|
||||
<title>ProManager</title>
|
||||
<meta name="description" content="ProManager" />
|
||||
<% } %>
|
||||
<script>
|
||||
!(function () {
|
||||
'use strict';
|
||||
var e = [
|
||||
'debug',
|
||||
'destroy',
|
||||
'do',
|
||||
'help',
|
||||
'identify',
|
||||
'is',
|
||||
'off',
|
||||
'on',
|
||||
'ready',
|
||||
'render',
|
||||
'reset',
|
||||
'safe',
|
||||
'set',
|
||||
];
|
||||
if (window.noticeable) console.warn('Noticeable SDK code snippet loaded more than once');
|
||||
else {
|
||||
var n = (window.noticeable = window.noticeable || []);
|
||||
|
||||
<% } %>
|
||||
<script>
|
||||
!(function () {
|
||||
'use strict';
|
||||
var e = [
|
||||
'debug',
|
||||
'destroy',
|
||||
'do',
|
||||
'help',
|
||||
'identify',
|
||||
'is',
|
||||
'off',
|
||||
'on',
|
||||
'ready',
|
||||
'render',
|
||||
'reset',
|
||||
'safe',
|
||||
'set',
|
||||
];
|
||||
if (window.noticeable) console.warn('Noticeable SDK code snippet loaded more than once');
|
||||
else {
|
||||
var n = (window.noticeable = window.noticeable || []);
|
||||
function t(e) {
|
||||
return function () {
|
||||
var t = Array.prototype.slice.call(arguments);
|
||||
return t.unshift(e), n.push(t), n;
|
||||
};
|
||||
}
|
||||
!(function () {
|
||||
for (var o = 0; o < e.length; o++) {
|
||||
var r = e[o];
|
||||
n[r] = t(r);
|
||||
}
|
||||
})(),
|
||||
(function () {
|
||||
var e = document.createElement('script');
|
||||
(e.async = !0), (e.src = 'https://sdk.noticeable.io/l.js');
|
||||
var n = document.head;
|
||||
n.insertBefore(e, n.firstChild);
|
||||
})();
|
||||
function t(e) {
|
||||
return function () {
|
||||
var t = Array.prototype.slice.call(arguments);
|
||||
return t.unshift(e), n.push(t), n;
|
||||
};
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
|
||||
<script type="module" src="src/index.jsx"></script>
|
||||
</body>
|
||||
!(function () {
|
||||
for (var o = 0; o < e.length; o++) {
|
||||
var r = e[o];
|
||||
n[r] = t(r);
|
||||
}
|
||||
})(),
|
||||
(function () {
|
||||
var e = document.createElement('script');
|
||||
(e.async = !0), (e.src = 'https://sdk.noticeable.io/l.js');
|
||||
var n = document.head;
|
||||
n.insertBefore(e, n.firstChild);
|
||||
})();
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
|
||||
<script type="module" src="src/index.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
22629
client/package-lock.json
generated
22629
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -2,90 +2,91 @@
|
||||
"name": "bodyshop",
|
||||
"version": "0.2.1",
|
||||
"engines": {
|
||||
"node": "18.18.2"
|
||||
"node": ">=18.18.2"
|
||||
},
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"proxy": "http://localhost:4000",
|
||||
"dependencies": {
|
||||
"@ant-design/compatible": "^5.1.2",
|
||||
"@ant-design/pro-layout": "^7.17.16",
|
||||
"@apollo/client": "^3.8.10",
|
||||
"@asseinfo/react-kanban": "^2.2.0",
|
||||
"@fingerprintjs/fingerprintjs": "^4.2.2",
|
||||
"@ant-design/pro-layout": "^7.19.12",
|
||||
"@apollo/client": "^3.11.4",
|
||||
"@emotion/is-prop-valid": "^1.3.0",
|
||||
"@fingerprintjs/fingerprintjs": "^4.4.3",
|
||||
"@jsreport/browser-client": "^3.1.0",
|
||||
"@reduxjs/toolkit": "^2.2.1",
|
||||
"@sentry/cli": "^2.28.6",
|
||||
"@sentry/react": "^7.104.0",
|
||||
"@splitsoftware/splitio-react": "^1.11.0",
|
||||
"@reduxjs/toolkit": "^2.2.7",
|
||||
"@sentry/cli": "^2.33.1",
|
||||
"@sentry/react": "^7.114.0",
|
||||
"@splitsoftware/splitio-react": "^1.12.1",
|
||||
"@tanem/react-nprogress": "^5.0.51",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"antd": "^5.15.3",
|
||||
"@vitejs/plugin-react": "^4.3.1",
|
||||
"antd": "^5.20.2",
|
||||
"apollo-link-logger": "^2.0.1",
|
||||
"apollo-link-sentry": "^3.3.0",
|
||||
"axios": "^1.6.7",
|
||||
"dayjs": "^1.11.10",
|
||||
"autosize": "^6.0.1",
|
||||
"axios": "^1.7.4",
|
||||
"classnames": "^2.5.1",
|
||||
"css-box-model": "^1.2.1",
|
||||
"dayjs": "^1.11.12",
|
||||
"dayjs-business-days2": "^1.2.2",
|
||||
"dinero.js": "^1.9.1",
|
||||
"dotenv": "^16.4.5",
|
||||
"env-cmd": "^10.1.0",
|
||||
"exifr": "^7.1.3",
|
||||
"firebase": "^10.8.1",
|
||||
"graphql": "^16.6.0",
|
||||
"i18next": "^23.10.0",
|
||||
"i18next-browser-languagedetector": "^7.0.2",
|
||||
"libphonenumber-js": "^1.10.57",
|
||||
"logrocket": "^8.0.1",
|
||||
"markerjs2": "^2.32.0",
|
||||
"normalize-url": "^8.0.0",
|
||||
"firebase": "^10.13.0",
|
||||
"graphql": "^16.9.0",
|
||||
"i18next": "^23.14.0",
|
||||
"i18next-browser-languagedetector": "^8.0.0",
|
||||
"immutability-helper": "^3.1.1",
|
||||
"libphonenumber-js": "^1.11.7",
|
||||
"logrocket": "^8.1.2",
|
||||
"markerjs2": "^2.32.1",
|
||||
"memoize-one": "^6.0.0",
|
||||
"normalize-url": "^8.0.1",
|
||||
"object-hash": "^3.0.0",
|
||||
"prop-types": "^15.8.1",
|
||||
"query-string": "^9.0.0",
|
||||
"react": "^18.2.0",
|
||||
"react-big-calendar": "^1.11.0",
|
||||
"query-string": "^9.1.0",
|
||||
"raf-schd": "^4.0.3",
|
||||
"react": "^18.3.1",
|
||||
"react-big-calendar": "^1.13.3",
|
||||
"react-color": "^2.19.3",
|
||||
"react-cookie": "^7.1.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-cookie": "^7.2.0",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-drag-listview": "^2.0.0",
|
||||
"react-grid-gallery": "^1.0.0",
|
||||
"react-grid-gallery": "^1.0.1",
|
||||
"react-grid-layout": "1.3.4",
|
||||
"react-i18next": "^14.0.5",
|
||||
"react-icons": "^5.0.1",
|
||||
"react-i18next": "^14.1.3",
|
||||
"react-icons": "^5.3.0",
|
||||
"react-image-lightbox": "^5.1.4",
|
||||
"react-joyride": "^2.7.4",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-number-format": "^5.3.3",
|
||||
"react-product-fruits": "^2.2.6",
|
||||
"react-redux": "^9.1.0",
|
||||
"react-number-format": "^5.4.0",
|
||||
"react-popopo": "^2.1.9",
|
||||
"react-product-fruits": "^2.2.61",
|
||||
"react-redux": "^9.1.2",
|
||||
"react-resizable": "^3.0.5",
|
||||
"react-router-dom": "^6.22.2",
|
||||
"react-scripts": "^5.0.1",
|
||||
"react-router-dom": "^6.26.1",
|
||||
"react-sticky": "^6.0.3",
|
||||
"react-virtualized": "^9.22.5",
|
||||
"recharts": "^2.12.2",
|
||||
"react-virtuoso": "^4.10.1",
|
||||
"recharts": "^2.12.7",
|
||||
"redux": "^5.0.1",
|
||||
"redux-actions": "^3.0.3",
|
||||
"redux-persist": "^6.0.0",
|
||||
"redux-saga": "^1.3.0",
|
||||
"redux-state-sync": "^3.1.4",
|
||||
"reselect": "^5.1.0",
|
||||
"sass": "^1.71.1",
|
||||
"socket.io-client": "^4.7.4",
|
||||
"styled-components": "^6.1.8",
|
||||
"reselect": "^5.1.1",
|
||||
"sass": "^1.77.8",
|
||||
"socket.io-client": "^4.7.5",
|
||||
"styled-components": "^6.1.12",
|
||||
"subscriptions-transport-ws": "^0.11.0",
|
||||
"terser-webpack-plugin": "^5.3.10",
|
||||
"userpilot": "^1.3.1",
|
||||
"use-memo-one": "^1.1.3",
|
||||
"userpilot": "^1.3.5",
|
||||
"vite-plugin-ejs": "^1.7.0",
|
||||
"web-vitals": "^3.5.2",
|
||||
"workbox-core": "^7.0.0",
|
||||
"workbox-expiration": "^7.0.0",
|
||||
"workbox-navigation-preload": "^7.0.0",
|
||||
"workbox-precaching": "^7.0.0",
|
||||
"workbox-routing": "^7.0.0",
|
||||
"workbox-strategies": "^7.0.0"
|
||||
"web-vitals": "^3.5.2"
|
||||
},
|
||||
"scripts": {
|
||||
"analyze": "source-map-explorer 'build/static/js/*.js'",
|
||||
"start": "vite",
|
||||
"build": "vite build",
|
||||
"build": "dotenvx run --env-file=.env.development.imex -- vite build",
|
||||
"start:imex": "dotenvx run --env-file=.env.development.imex -- vite",
|
||||
"start:rome": "dotenvx run --env-file=.env.development.rome -- vite",
|
||||
"start:promanager": "dotenvx run --env-file=.env.development.promanager -- vite",
|
||||
@@ -128,32 +129,30 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||
"@babel/preset-react": "^7.23.3",
|
||||
"@dotenvx/dotenvx": "^0.15.4",
|
||||
"@emotion/babel-plugin": "^11.11.0",
|
||||
"@emotion/react": "^11.11.3",
|
||||
"@sentry/webpack-plugin": "^2.14.2",
|
||||
"@swc/core": "^1.3.107",
|
||||
"@swc/plugin-styled-components": "^1.5.108",
|
||||
"@testing-library/cypress": "^10.0.1",
|
||||
"browserslist": "^4.22.3",
|
||||
"@babel/preset-react": "^7.24.7",
|
||||
"@dotenvx/dotenvx": "^1.7.0",
|
||||
"@emotion/babel-plugin": "^11.12.0",
|
||||
"@emotion/react": "^11.13.0",
|
||||
"@sentry/webpack-plugin": "^2.22.2",
|
||||
"@testing-library/cypress": "^10.0.2",
|
||||
"browserslist": "^4.23.3",
|
||||
"browserslist-to-esbuild": "^2.1.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"cypress": "^13.6.6",
|
||||
"cypress": "^13.13.3",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-react-app": "^7.0.1",
|
||||
"eslint-plugin-cypress": "^2.15.1",
|
||||
"memfs": "^4.6.0",
|
||||
"memfs": "^4.11.1",
|
||||
"os-browserify": "^0.3.0",
|
||||
"react-error-overlay": "6.0.11",
|
||||
"redux-logger": "^3.0.6",
|
||||
"source-map-explorer": "^2.5.3",
|
||||
"vite": "^5.0.11",
|
||||
"vite": "^5.4.1",
|
||||
"vite-plugin-babel": "^1.2.0",
|
||||
"vite-plugin-eslint": "^1.8.1",
|
||||
"vite-plugin-legacy": "^2.1.0",
|
||||
"vite-plugin-node-polyfills": "^0.19.0",
|
||||
"vite-plugin-pwa": "^0.19.0",
|
||||
"vite-plugin-node-polyfills": "^0.22.0",
|
||||
"vite-plugin-pwa": "^0.20.1",
|
||||
"vite-plugin-style-import": "^2.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16422,7 +16422,7 @@ For when you don't want to write the same thing over and over to cache a method
|
||||
$ npm install --save-dev stubs
|
||||
```
|
||||
```js
|
||||
var mylib = require('./lib/index.js')
|
||||
var mylib = require('./lib/index.jsx')
|
||||
var stubs = require('stubs')
|
||||
|
||||
// make it a noop
|
||||
|
||||
@@ -16567,7 +16567,7 @@ even more slower.
|
||||
## Benchmarks
|
||||
|
||||
```bash
|
||||
$ node benchmarks/index.js
|
||||
$ node benchmarks/index.jsx
|
||||
Benchmarking: sign
|
||||
elliptic#sign x 262 ops/sec ±0.51% (177 runs sampled)
|
||||
eccjs#sign x 55.91 ops/sec ±0.90% (144 runs sampled)
|
||||
|
||||
@@ -18,7 +18,7 @@ import { checkUserSession } from "../redux/user/user.actions";
|
||||
import { selectBodyshop, selectCurrentEula, selectCurrentUser } from "../redux/user/user.selectors";
|
||||
import PrivateRoute from "../components/PrivateRoute";
|
||||
import "./App.styles.scss";
|
||||
import handleBeta from "../utils/betaHandler";
|
||||
import handleBeta from "../utils/handleBeta";
|
||||
import Eula from "../components/eula/eula.component";
|
||||
import InstanceRenderMgr from "../utils/instanceRenderMgr";
|
||||
import ProductFruitsWrapper from "./ProductFruitsWrapper.jsx";
|
||||
|
||||
@@ -161,3 +161,15 @@
|
||||
.rowWithColor > td {
|
||||
background-color: var(--bgColor) !important;
|
||||
}
|
||||
|
||||
.muted-button {
|
||||
color: lightgray;
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
font-size: 16px; /* Adjust as needed */
|
||||
}
|
||||
|
||||
.muted-button:hover {
|
||||
color: darkgrey;
|
||||
}
|
||||
|
||||
@@ -27,6 +27,6 @@ ProductFruitsWrapper.propTypes = {
|
||||
currentUser: PropTypes.shape({
|
||||
authorized: PropTypes.bool,
|
||||
email: PropTypes.string
|
||||
}).isRequired,
|
||||
workspaceCode: PropTypes.string.isRequired
|
||||
}),
|
||||
workspaceCode: PropTypes.string
|
||||
};
|
||||
|
||||
@@ -13,6 +13,7 @@ import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { DateFormatter } from "../../utils/DateFormatter";
|
||||
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
|
||||
import JobMarkSelectedExported from "../jobs-mark-selected-exported/jobs-mark-selected-exported";
|
||||
import OwnerNameDisplay, { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
|
||||
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
|
||||
|
||||
@@ -170,13 +171,22 @@ export function AccountingReceivablesTableComponent({ bodyshop, loading, jobs, r
|
||||
extra={
|
||||
<Space wrap>
|
||||
{!bodyshop.cdk_dealerid && !bodyshop.pbs_serialnumber && (
|
||||
<JobsExportAllButton
|
||||
jobIds={selectedJobs}
|
||||
disabled={transInProgress || selectedJobs.length === 0}
|
||||
loadingCallback={setTransInProgress}
|
||||
completedCallback={setSelectedJobs}
|
||||
refetch={refetch}
|
||||
/>
|
||||
<>
|
||||
<JobMarkSelectedExported
|
||||
jobIds={selectedJobs}
|
||||
disabled={transInProgress || selectedJobs.length === 0}
|
||||
loadingCallback={setTransInProgress}
|
||||
completedCallback={setSelectedJobs}
|
||||
refetch={refetch}
|
||||
/>
|
||||
<JobsExportAllButton
|
||||
jobIds={selectedJobs}
|
||||
disabled={transInProgress || selectedJobs.length === 0}
|
||||
loadingCallback={setTransInProgress}
|
||||
completedCallback={setSelectedJobs}
|
||||
refetch={refetch}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && <QboAuthorizeComponent />}
|
||||
<Input.Search
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { PageHeader } from "@ant-design/pro-layout";
|
||||
import { useMutation, useQuery } from "@apollo/client";
|
||||
import { Button, Divider, Form, Popconfirm, Space } from "antd";
|
||||
import dayjs from "../../utils/day";
|
||||
import queryString from "query-string";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -13,6 +13,7 @@ import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
import dayjs from "../../utils/day";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
import BillFormContainer from "../bill-form/bill-form.container";
|
||||
import BillMarkExportedButton from "../bill-mark-exported-button/bill-mark-exported-button.component";
|
||||
@@ -22,7 +23,6 @@ import JobDocumentsGallery from "../jobs-documents-gallery/jobs-documents-galler
|
||||
import JobsDocumentsLocalGallery from "../jobs-documents-local-gallery/jobs-documents-local-gallery.container";
|
||||
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
|
||||
import BillDetailEditReturn from "./bill-detail-edit-return.component";
|
||||
import { PageHeader } from "@ant-design/pro-layout";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
@@ -153,6 +153,7 @@ export function BillDetailEditcontainer({ setPartsOrderContext, insertAuditTrail
|
||||
if (!search.billid) return <></>; //<div>{t("bills.labels.noneselected")}</div>;
|
||||
|
||||
const exported = data && data.bills_by_pk && data.bills_by_pk.exported;
|
||||
const isinhouse = data && data.bills_by_pk && data.bills_by_pk.isinhouse;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -188,7 +189,7 @@ export function BillDetailEditcontainer({ setPartsOrderContext, insertAuditTrail
|
||||
}
|
||||
/>
|
||||
<Form form={form} onFinish={handleFinish} initialValues={transformData(data)} layout="vertical">
|
||||
<BillFormContainer form={form} billEdit disabled={exported} />
|
||||
<BillFormContainer form={form} billEdit disabled={exported} disableInHouse={isinhouse} />
|
||||
<Divider orientation="left">{t("general.labels.media")}</Divider>
|
||||
{bodyshop.uselocalmediaserver ? (
|
||||
<JobsDocumentsLocalGallery
|
||||
|
||||
@@ -41,7 +41,8 @@ export function BillFormComponent({
|
||||
job,
|
||||
loadOutstandingReturns,
|
||||
loadInventory,
|
||||
preferredMake
|
||||
preferredMake,
|
||||
disableInHouse
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const client = useApolloClient();
|
||||
@@ -177,7 +178,7 @@ export function BillFormComponent({
|
||||
]}
|
||||
>
|
||||
<VendorSearchSelect
|
||||
disabled={disabled}
|
||||
disabled={disabled || disableInHouse}
|
||||
options={vendorAutoCompleteOptions}
|
||||
preferredMake={preferredMake}
|
||||
onSelect={handleVendorSelect}
|
||||
@@ -243,7 +244,7 @@ export function BillFormComponent({
|
||||
})
|
||||
]}
|
||||
>
|
||||
<Input disabled={disabled || disableInvNumber} />
|
||||
<Input disabled={disabled || disableInvNumber || disableInHouse} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bills.fields.date")}
|
||||
|
||||
@@ -16,7 +16,7 @@ const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
|
||||
export function BillFormContainer({ bodyshop, form, billEdit, disabled, disableInvNumber }) {
|
||||
export function BillFormContainer({ bodyshop, form, billEdit, disabled, disableInvNumber, disableInHouse }) {
|
||||
const {
|
||||
treatments: { Simple_Inventory }
|
||||
} = useSplitTreatments({
|
||||
@@ -47,6 +47,7 @@ export function BillFormContainer({ bodyshop, form, billEdit, disabled, disableI
|
||||
job={lineData ? lineData.jobs_by_pk : null}
|
||||
responsibilityCenters={bodyshop.md_responsibility_centers || null}
|
||||
disableInvNumber={disableInvNumber}
|
||||
disableInHouse={disableInHouse}
|
||||
loadOutstandingReturns={loadOutstandingReturns}
|
||||
loadInventory={loadInventory}
|
||||
preferredMake={lineData ? lineData.jobs_by_pk.v_make_desc : null}
|
||||
|
||||
@@ -3,7 +3,6 @@ import React, { forwardRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import InstanceRenderMgr from "../../utils/instanceRenderMgr";
|
||||
|
||||
//To be used as a form element only.
|
||||
const BillLineSearchSelect = ({ options, disabled, allowRemoved, ...restProps }, ref) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import axios from "axios";
|
||||
const fortyFiveDaysAgo = () => dayjs().subtract(45, "day").toLocaleString();
|
||||
|
||||
export default function JobLifecycleDashboardComponent({ data, bodyshop, ...cardProps }) {
|
||||
console.log("🚀 ~ JobLifecycleDashboardComponent ~ bodyshop:", bodyshop);
|
||||
const { t } = useTranslation();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [lifecycleData, setLifecycleData] = useState(null);
|
||||
@@ -143,7 +142,7 @@ export default function JobLifecycleDashboardComponent({ data, bodyshop, ...card
|
||||
>
|
||||
<div>
|
||||
{lifecycleData.summations.map((key) => (
|
||||
<Tag color={key.color} style={{ width: "13vh", padding: "4px", margin: "4px" }}>
|
||||
<Tag key={key.status} color={key.color} style={{ width: "13vh", padding: "4px", margin: "4px" }}>
|
||||
<div
|
||||
aria-label={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`}
|
||||
title={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`}
|
||||
@@ -165,6 +164,7 @@ export default function JobLifecycleDashboardComponent({ data, bodyshop, ...card
|
||||
size="small"
|
||||
pagination={false}
|
||||
columns={columns}
|
||||
rowKey={(record) => record.status}
|
||||
dataSource={lifecycleData.summations.sort((a, b) => b.value - a.value).slice(0, 3)}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
@@ -36,9 +36,6 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) {
|
||||
};
|
||||
});
|
||||
|
||||
console.log("Scheduled Out Today");
|
||||
console.dir(scheduledOutToday);
|
||||
|
||||
const tvFontSize = 18;
|
||||
const tvFontWeight = "bold";
|
||||
|
||||
@@ -89,8 +86,6 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) {
|
||||
sorter: (a, b) => alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
|
||||
sortOrder: state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
|
||||
render: (text, record) => {
|
||||
console.log("Render record out today");
|
||||
console.dir(record);
|
||||
return record.ownerid ? (
|
||||
<Link to={"/manage/owners/" + record.ownerid} onClick={(e) => e.stopPropagation()}>
|
||||
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
|
||||
|
||||
@@ -43,7 +43,7 @@ import { selectRecentItems, selectSelectedHeader } from "../../redux/application
|
||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||
import { signOutStart } from "../../redux/user/user.actions";
|
||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||
import { checkBeta, handleBeta, setBeta } from "../../utils/betaHandler";
|
||||
import { checkBeta, handleBeta, setBeta } from "../../utils/handleBeta";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import React from "react";
|
||||
import { useQuery } from "@apollo/client";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { QUERY_BILLS_BY_JOBID } from "../../graphql/bills.queries";
|
||||
import { QUERY_PARTS_BILLS_BY_JOBID } from "../../graphql/bills.queries";
|
||||
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
@@ -19,7 +19,7 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(JobCloseRoGuardBills);
|
||||
|
||||
export function JobCloseRoGuardBills({ job, jobRO, bodyshop, form, warningCallback }) {
|
||||
const { loading, error, data } = useQuery(QUERY_BILLS_BY_JOBID, {
|
||||
const { loading, error, data } = useQuery(QUERY_PARTS_BILLS_BY_JOBID, {
|
||||
variables: { jobid: job.id },
|
||||
fetchPolicy: "network-only",
|
||||
nextFetchPolicy: "network-only"
|
||||
|
||||
@@ -12,7 +12,6 @@ import JobCloseRoGuardBills from "./job-close-ro-guard.bills";
|
||||
import JobCloseRoGuardPpd from "./job-close-ro-guard.ppd";
|
||||
import JobCloseRoGuardProfit from "./job-close-ro-guard.profit";
|
||||
import "./job-close-ro-guard.styles.scss";
|
||||
// import JobCloseRoGuardSublet from "./job-close-ro-guard.sublet";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
import JobCloseRoGuardTtLifecycle from "./job-close-ro-guard.tt-lifecycle";
|
||||
|
||||
|
||||
@@ -2,27 +2,30 @@ import { useQuery } from "@apollo/client";
|
||||
import { Col, Row, Skeleton, Space, Timeline, Typography } from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { Link } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { GET_JOB_LINE_ORDERS } from "../../graphql/jobs.queries";
|
||||
import { QUERY_JOBLINE_TASKS_PAGINATED } from "../../graphql/tasks.queries.js";
|
||||
import { selectTechnician } from "../../redux/tech/tech.selectors.js";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import { DateFormatter } from "../../utils/DateFormatter";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { QUERY_JOBLINE_TASKS_PAGINATED } from "../../graphql/tasks.queries.js";
|
||||
import TaskListContainer from "../task-list/task-list.container.jsx";
|
||||
import BillDetailEditcontainer from "../bill-detail-edit/bill-detail-edit.container.jsx";
|
||||
import FeatureWrapper from "../feature-wrapper/feature-wrapper.component.jsx";
|
||||
import TaskListContainer from "../task-list/task-list.container.jsx";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
bodyshop: selectBodyshop,
|
||||
technician: selectTechnician
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(JobLinesExpander);
|
||||
|
||||
export function JobLinesExpander({ jobline, jobid, bodyshop }) {
|
||||
export function JobLinesExpander({ jobline, jobid, bodyshop, technician }) {
|
||||
const { t } = useTranslation();
|
||||
const { loading, error, data } = useQuery(GET_JOB_LINE_ORDERS, {
|
||||
fetchPolicy: "network-only",
|
||||
@@ -47,9 +50,15 @@ export function JobLinesExpander({ jobline, jobid, bodyshop }) {
|
||||
children: (
|
||||
<Row wrap>
|
||||
<Col span={4}>
|
||||
<Link to={`/manage/jobs/${jobid}?partsorderid=${line.parts_order.id}`}>
|
||||
{line.parts_order.order_number}
|
||||
</Link>
|
||||
{!technician ? (
|
||||
<>
|
||||
<Link to={`/manage/jobs/${jobid}?partsorderid=${line.parts_order.id}`}>
|
||||
{line.parts_order.order_number}
|
||||
</Link>
|
||||
</>
|
||||
) : (
|
||||
`${line.parts_order.order_number}`
|
||||
)}
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<DateFormatter>{line.parts_order.order_date}</DateFormatter>
|
||||
@@ -84,17 +93,17 @@ export function JobLinesExpander({ jobline, jobid, bodyshop }) {
|
||||
key: line.id,
|
||||
children: (
|
||||
<Row>
|
||||
<Col span={8}>
|
||||
<Link to={`/manage/jobs/${jobid}?partsorderid=${line.id}`}>{line.parts_dispatch.number}</Link>
|
||||
</Col>
|
||||
<Col span={8}>{line.parts_dispatch.number}</Col>
|
||||
<Col span={8}>
|
||||
{bodyshop.employees.find((e) => e.id === line.parts_dispatch.employeeid)?.first_name}
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Space>
|
||||
{t("parts_dispatch_lines.fields.accepted_at")}
|
||||
<DateFormatter>{line.accepted_at}</DateFormatter>
|
||||
</Space>
|
||||
{line.accepted_at ? (
|
||||
<Space>
|
||||
{t("parts_dispatch_lines.fields.accepted_at")}
|
||||
<DateFormatter>{line.accepted_at}</DateFormatter>
|
||||
</Space>
|
||||
) : null}
|
||||
</Col>
|
||||
</Row>
|
||||
)
|
||||
@@ -111,6 +120,7 @@ export function JobLinesExpander({ jobline, jobid, bodyshop }) {
|
||||
<FeatureWrapper featureName="bills" noauth={() => null}>
|
||||
<Col md={24} lg={8}>
|
||||
<Typography.Title level={4}>{t("bills.labels.bills")}</Typography.Title>
|
||||
<BillDetailEditcontainer />
|
||||
<Timeline
|
||||
items={
|
||||
data.billlines.length > 0
|
||||
@@ -119,9 +129,15 @@ export function JobLinesExpander({ jobline, jobid, bodyshop }) {
|
||||
children: (
|
||||
<Row wrap>
|
||||
<Col span={4}>
|
||||
<Link to={`/manage/jobs/${jobid}?tab=partssublet&billid=${line.bill.id}`}>
|
||||
{line.bill.invoice_number}
|
||||
</Link>
|
||||
{!technician ? (
|
||||
<>
|
||||
<Link to={`/manage/jobs/${jobid}?tab=partssublet&billid=${line.bill.id}`}>
|
||||
{line.bill.invoice_number}
|
||||
</Link>
|
||||
</>
|
||||
) : (
|
||||
`${line.bill.invoice_number}`
|
||||
)}
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<span>
|
||||
|
||||
@@ -3,13 +3,21 @@ import { Button, Form, notification, Popover, Tooltip } from "antd";
|
||||
import axios from "axios";
|
||||
import { t } from "i18next";
|
||||
import React, { useState } from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { UPDATE_LINE_PPC } from "../../graphql/jobs-lines.queries";
|
||||
import { selectTechnician } from "../../redux/tech/tech.selectors";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
import CurrencyFormItemComponent from "../form-items-formatted/currency-form-item.component";
|
||||
import JobLineConvertToLabor from "../job-line-convert-to-labor/job-line-convert-to-labor.component";
|
||||
|
||||
export default function JobLinesPartPriceChange({ job, line, refetch }) {
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
technician: selectTechnician
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({});
|
||||
|
||||
export function JobLinesPartPriceChange({ job, line, refetch, technician }) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [updatePartPrice] = useMutation(UPDATE_LINE_PPC);
|
||||
|
||||
@@ -52,7 +60,7 @@ export default function JobLinesPartPriceChange({ job, line, refetch }) {
|
||||
}
|
||||
};
|
||||
|
||||
const popcontent = InstanceRenderManager({
|
||||
const popcontent = !technician && InstanceRenderManager({
|
||||
imex: null,
|
||||
rome: (
|
||||
<Form layout="vertical" onFinish={handleFinish} initialValues={{ act_price: line.act_price }}>
|
||||
@@ -95,3 +103,4 @@ export default function JobLinesPartPriceChange({ job, line, refetch }) {
|
||||
</JobLineConvertToLabor>
|
||||
);
|
||||
}
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(JobLinesPartPriceChange);
|
||||
|
||||
@@ -31,19 +31,20 @@ import JobLinesBillRefernece from "../job-lines-bill-reference/job-lines-bill-re
|
||||
// import AllocationsEmployeeLabelContainer from "../allocations-employee-label/allocations-employee-label.container";
|
||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||
import _ from "lodash";
|
||||
import { FaTasks } from "react-icons/fa";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import dayjs from "../../utils/day";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
||||
import JobCreateIOU from "../job-create-iou/job-create-iou.component";
|
||||
import JobLineBulkAssignComponent from "../job-line-bulk-assign/job-line-bulk-assign.component";
|
||||
import JobLineDispatchButton from "../job-line-dispatch-button/job-line-dispatch-button.component";
|
||||
import JoblineTeamAssignment from "../job-line-team-assignment/job-line-team-assignmnent.component";
|
||||
import JobSendPartPriceChangeComponent from "../job-send-parts-price-change/job-send-parts-price-change.component";
|
||||
import PartsOrderDrawer from "../parts-order-list-table/parts-order-list-table-drawer.component";
|
||||
import PartsOrderModalContainer from "../parts-order-modal/parts-order-modal.container";
|
||||
import JobLinesExpander from "./job-lines-expander.component";
|
||||
import JobLinesPartPriceChange from "./job-lines-part-price-change.component";
|
||||
import { FaTasks } from "react-icons/fa";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -54,6 +55,7 @@ const mapStateToProps = createStructuredSelector({
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setJobLineEditContext: (context) => dispatch(setModalContext({ context: context, modal: "jobLineEdit" })),
|
||||
setPartsOrderContext: (context) => dispatch(setModalContext({ context: context, modal: "partsOrder" })),
|
||||
setPartsReceiveContext: (context) => dispatch(setModalContext({ context: context, modal: "partsReceive" })),
|
||||
setBillEnterContext: (context) => dispatch(setModalContext({ context: context, modal: "billEnter" })),
|
||||
setTaskUpsertContext: (context) => dispatch(setModalContext({ context, modal: "taskUpsert" }))
|
||||
});
|
||||
@@ -63,6 +65,7 @@ export function JobLinesComponent({
|
||||
jobRO,
|
||||
technician,
|
||||
setPartsOrderContext,
|
||||
setPartsReceiveContext,
|
||||
loading,
|
||||
refetch,
|
||||
jobLines,
|
||||
@@ -71,7 +74,11 @@ export function JobLinesComponent({
|
||||
setJobLineEditContext,
|
||||
form,
|
||||
setBillEnterContext,
|
||||
setTaskUpsertContext
|
||||
setTaskUpsertContext,
|
||||
billsQuery,
|
||||
handleBillOnRowClick,
|
||||
handlePartsOrderOnRowClick,
|
||||
handlePartsDispatchOnRowClick
|
||||
}) {
|
||||
const [deleteJobLine] = useMutation(DELETE_JOB_LINE_BY_PK);
|
||||
const {
|
||||
@@ -198,7 +205,6 @@ export function JobLinesComponent({
|
||||
onFilter: (value, record) => value.includes(record.part_type),
|
||||
render: (text, record) => (record.part_type ? t(`joblines.fields.part_types.${record.part_type}`) : null)
|
||||
},
|
||||
|
||||
{
|
||||
title: t("joblines.fields.act_price"),
|
||||
dataIndex: "act_price",
|
||||
@@ -213,7 +219,6 @@ export function JobLinesComponent({
|
||||
dataIndex: "part_qty",
|
||||
key: "part_qty"
|
||||
},
|
||||
|
||||
// {
|
||||
// title: t('joblines.fields.tax_part'),
|
||||
// dataIndex: 'tax_part',
|
||||
@@ -322,7 +327,7 @@ export function JobLinesComponent({
|
||||
key: "actions",
|
||||
render: (text, record) => (
|
||||
<Space>
|
||||
{(record.manual_line || jobIsPrivate) && (
|
||||
{(record.manual_line || jobIsPrivate) && !technician && (
|
||||
<>
|
||||
<Button
|
||||
disabled={jobRO}
|
||||
@@ -337,7 +342,6 @@ export function JobLinesComponent({
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Button
|
||||
title={t("tasks.buttons.create")}
|
||||
onClick={() => {
|
||||
@@ -351,7 +355,7 @@ export function JobLinesComponent({
|
||||
>
|
||||
<FaTasks />
|
||||
</Button>
|
||||
{(record.manual_line || jobIsPrivate) && (
|
||||
{(record.manual_line || jobIsPrivate) && !technician && (
|
||||
<>
|
||||
<Button
|
||||
disabled={jobRO}
|
||||
@@ -437,6 +441,15 @@ export function JobLinesComponent({
|
||||
return (
|
||||
<div>
|
||||
<PartsOrderModalContainer />
|
||||
{!technician && (
|
||||
<PartsOrderDrawer
|
||||
job={job}
|
||||
billsQuery={billsQuery}
|
||||
handleOnRowClick={handlePartsOrderOnRowClick}
|
||||
setPartsReceiveContext={setPartsReceiveContext}
|
||||
setTaskUpsertContext={setTaskUpsertContext}
|
||||
/>
|
||||
)}
|
||||
<PageHeader
|
||||
title={t("jobs.labels.estimatelines")}
|
||||
extra={
|
||||
@@ -553,7 +566,7 @@ export function JobLinesComponent({
|
||||
>
|
||||
{t("joblines.actions.new")}
|
||||
</Button>
|
||||
{InstanceRenderManager({ rome: <JobSendPartPriceChangeComponent job={job} /> })}
|
||||
{InstanceRenderManager({ rome: <JobSendPartPriceChangeComponent job={job} disabled={technician} /> })}
|
||||
<JobCreateIOU job={job} selectedJobLines={selectedLines} />
|
||||
<Input.Search
|
||||
placeholder={t("general.labels.search")}
|
||||
|
||||
@@ -1,7 +1,17 @@
|
||||
import React, { useMemo, useState } from "react";
|
||||
import JobLinesComponent from "./job-lines.component";
|
||||
|
||||
function JobLinesContainer({ job, joblines, refetch, form, ...rest }) {
|
||||
function JobLinesContainer({
|
||||
job,
|
||||
joblines,
|
||||
billsQuery,
|
||||
handleBillOnRowClick,
|
||||
handlePartsOrderOnRowClick,
|
||||
handlePartsDispatchOnRowClick,
|
||||
refetch,
|
||||
form,
|
||||
...rest
|
||||
}) {
|
||||
const [searchText, setSearchText] = useState("");
|
||||
|
||||
const jobLines = useMemo(() => {
|
||||
@@ -22,7 +32,19 @@ function JobLinesContainer({ job, joblines, refetch, form, ...rest }) {
|
||||
}, [joblines, searchText]);
|
||||
|
||||
return (
|
||||
<JobLinesComponent refetch={refetch} jobLines={jobLines} setSearchText={setSearchText} job={job} form={form} />
|
||||
<div>
|
||||
<JobLinesComponent
|
||||
refetch={refetch}
|
||||
jobLines={jobLines}
|
||||
billsQuery={billsQuery}
|
||||
handleBillOnRowClick={handleBillOnRowClick}
|
||||
handlePartsOrderOnRowClick={handlePartsOrderOnRowClick}
|
||||
handlePartsDispatchOnRowClick={handlePartsDispatchOnRowClick}
|
||||
setSearchText={setSearchText}
|
||||
job={job}
|
||||
form={form}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -11,17 +11,18 @@ import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { UPDATE_JOB_LINE } from "../../graphql/jobs-lines.queries";
|
||||
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||
import { selectTechnician } from "../../redux/tech/tech.selectors";
|
||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
technician: selectTechnician
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type }))
|
||||
});
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(JobLineConvertToLabor);
|
||||
|
||||
export function JobLineConvertToLabor({ children, jobline, job, insertAuditTrail, ...otherBtnProps }) {
|
||||
export function JobLineConvertToLabor({ children, jobline, job, insertAuditTrail, technician, ...otherBtnProps }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -165,7 +166,7 @@ export function JobLineConvertToLabor({ children, jobline, job, insertAuditTrail
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
{jobline.act_price !== 0 && (
|
||||
{jobline.act_price !== 0 && !technician && (
|
||||
<Popover disabled={jobline.convertedtolbr} content={overlay} open={visibility} placement="bottom">
|
||||
<Tooltip title={t("joblines.actions.converttolabor")}>
|
||||
<Button
|
||||
|
||||
@@ -3,7 +3,7 @@ import axios from "axios";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function JobSendPartPriceChangeComponent({ job }) {
|
||||
export default function JobSendPartPriceChangeComponent({ job, disabled }) {
|
||||
const { t } = useTranslation();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const handleClick = async () => {
|
||||
@@ -24,7 +24,7 @@ export default function JobSendPartPriceChangeComponent({ job }) {
|
||||
};
|
||||
|
||||
return (
|
||||
<Button onClick={handleClick} loading={loading}>
|
||||
<Button onClick={handleClick} loading={loading} disabled={disabled}>
|
||||
{t("jobs.actions.sendpartspricechange")}
|
||||
</Button>
|
||||
);
|
||||
|
||||
@@ -3,8 +3,8 @@ import Dinero from "dinero.js";
|
||||
import React, { useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import { alphaSort } from "../../utils/sorters";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
import { alphaSort } from "../../utils/sorters";
|
||||
|
||||
export default function JobTotalsTableLabor({ job }) {
|
||||
const { t } = useTranslation();
|
||||
@@ -56,16 +56,49 @@ export default function JobTotalsTableLabor({ job }) {
|
||||
sortOrder: state.sortedInfo.columnKey === "mod_lb_hrs" && state.sortedInfo.order,
|
||||
render: (text, record) => record.hours.toFixed(1)
|
||||
},
|
||||
{
|
||||
title: t("joblines.fields.total"),
|
||||
dataIndex: "total",
|
||||
key: "total",
|
||||
align: "right",
|
||||
sorter: (a, b) => a.total.amount - b.total.amount,
|
||||
sortOrder: state.sortedInfo.columnKey === "total" && state.sortedInfo.order,
|
||||
|
||||
render: (text, record) => Dinero(record.total).toFormat()
|
||||
}
|
||||
...InstanceRenderManager({
|
||||
imex: [
|
||||
{
|
||||
title: t("joblines.fields.total"),
|
||||
dataIndex: "total",
|
||||
key: "total",
|
||||
align: "right",
|
||||
sorter: (a, b) => a.total.amount - b.total.amount,
|
||||
sortOrder: state.sortedInfo.columnKey === "total" && state.sortedInfo.order,
|
||||
render: (text, record) => Dinero(record.total).toFormat()
|
||||
}
|
||||
],
|
||||
rome: [
|
||||
{
|
||||
title: t("joblines.fields.amount"),
|
||||
dataIndex: "base",
|
||||
key: "base",
|
||||
align: "right",
|
||||
sorter: (a, b) => a.base.amount - b.base.amount,
|
||||
sortOrder: state.sortedInfo.columnKey === "base" && state.sortedInfo.order,
|
||||
render: (text, record) => Dinero(record.base).toFormat()
|
||||
},
|
||||
{
|
||||
title: t("joblines.fields.adjustment"),
|
||||
dataIndex: "adjustment",
|
||||
key: "adjustment",
|
||||
align: "right",
|
||||
sorter: (a, b) => a.adjustment.amount - b.adjustment.amount,
|
||||
sortOrder: state.sortedInfo.columnKey === "adjustment" && state.sortedInfo.order,
|
||||
render: (text, record) => Dinero(record.adjustment).toFormat()
|
||||
},
|
||||
{
|
||||
title: t("joblines.fields.total"),
|
||||
dataIndex: "total",
|
||||
key: "total",
|
||||
align: "right",
|
||||
sorter: (a, b) => a.total.amount - b.total.amount,
|
||||
sortOrder: state.sortedInfo.columnKey === "total" && state.sortedInfo.order,
|
||||
render: (text, record) => Dinero(record.total).toFormat()
|
||||
}
|
||||
],
|
||||
promanager: "USE_ROME"
|
||||
})
|
||||
];
|
||||
|
||||
const handleTableChange = (pagination, filters, sorter) => {
|
||||
@@ -91,6 +124,16 @@ export default function JobTotalsTableLabor({ job }) {
|
||||
<Table.Summary.Cell>
|
||||
{(job.job_totals.rates.mapa.hours + job.job_totals.rates.mash.hours).toFixed(1)}
|
||||
</Table.Summary.Cell>
|
||||
{InstanceRenderManager({
|
||||
imex: null,
|
||||
rome: (
|
||||
<>
|
||||
<Table.Summary.Cell />
|
||||
<Table.Summary.Cell />
|
||||
</>
|
||||
),
|
||||
promanager: "USE_ROME"
|
||||
})}
|
||||
<Table.Summary.Cell align="right">
|
||||
<strong>{Dinero(job.job_totals.rates.rates_subtotal).toFormat()}</strong>
|
||||
</Table.Summary.Cell>
|
||||
@@ -122,7 +165,29 @@ export default function JobTotalsTableLabor({ job }) {
|
||||
<CurrencyFormatter>{job.job_totals.rates.mapa.rate}</CurrencyFormatter>
|
||||
</Table.Summary.Cell>
|
||||
<Table.Summary.Cell>{job.job_totals.rates.mapa.hours.toFixed(1)}</Table.Summary.Cell>
|
||||
<Table.Summary.Cell align="right">{Dinero(job.job_totals.rates.mapa.total).toFormat()}</Table.Summary.Cell>
|
||||
{InstanceRenderManager({
|
||||
imex: (
|
||||
<>
|
||||
<Table.Summary.Cell align="right">
|
||||
{Dinero(job.job_totals.rates.mapa.total).toFormat()}
|
||||
</Table.Summary.Cell>
|
||||
</>
|
||||
),
|
||||
rome: (
|
||||
<>
|
||||
<Table.Summary.Cell align="right">
|
||||
{Dinero(job.job_totals.rates.mapa.base).toFormat()}
|
||||
</Table.Summary.Cell>
|
||||
<Table.Summary.Cell align="right">
|
||||
{Dinero(job.job_totals.rates.mapa.adjustment).toFormat()}
|
||||
</Table.Summary.Cell>
|
||||
<Table.Summary.Cell align="right">
|
||||
{Dinero(job.job_totals.rates.mapa.total).toFormat()}
|
||||
</Table.Summary.Cell>
|
||||
</>
|
||||
),
|
||||
promanager: "USE_ROME"
|
||||
})}
|
||||
</Table.Summary.Row>
|
||||
<Table.Summary.Row>
|
||||
<Table.Summary.Cell>
|
||||
@@ -151,7 +216,29 @@ export default function JobTotalsTableLabor({ job }) {
|
||||
<CurrencyFormatter>{job.job_totals.rates.mash.rate}</CurrencyFormatter>
|
||||
</Table.Summary.Cell>
|
||||
<Table.Summary.Cell>{job.job_totals.rates.mash.hours.toFixed(1)}</Table.Summary.Cell>
|
||||
<Table.Summary.Cell align="right">{Dinero(job.job_totals.rates.mash.total).toFormat()}</Table.Summary.Cell>
|
||||
{InstanceRenderManager({
|
||||
imex: (
|
||||
<>
|
||||
<Table.Summary.Cell align="right">
|
||||
{Dinero(job.job_totals.rates.mash.total).toFormat()}
|
||||
</Table.Summary.Cell>
|
||||
</>
|
||||
),
|
||||
rome: (
|
||||
<>
|
||||
<Table.Summary.Cell align="right">
|
||||
{Dinero(job.job_totals.rates.mash.base).toFormat()}
|
||||
</Table.Summary.Cell>
|
||||
<Table.Summary.Cell align="right">
|
||||
{Dinero(job.job_totals.rates.mash.adjustment).toFormat()}
|
||||
</Table.Summary.Cell>
|
||||
<Table.Summary.Cell align="right">
|
||||
{Dinero(job.job_totals.rates.mash.total).toFormat()}
|
||||
</Table.Summary.Cell>
|
||||
</>
|
||||
),
|
||||
promanager: "USE_ROME"
|
||||
})}
|
||||
</Table.Summary.Row>
|
||||
<Table.Summary.Row>
|
||||
<Table.Summary.Cell>
|
||||
@@ -159,6 +246,16 @@ export default function JobTotalsTableLabor({ job }) {
|
||||
</Table.Summary.Cell>
|
||||
<Table.Summary.Cell />
|
||||
<Table.Summary.Cell />
|
||||
{InstanceRenderManager({
|
||||
imex: null,
|
||||
rome: (
|
||||
<>
|
||||
<Table.Summary.Cell />
|
||||
<Table.Summary.Cell />
|
||||
</>
|
||||
),
|
||||
promanager: "USE_ROME"
|
||||
})}
|
||||
<Table.Summary.Cell align="right">
|
||||
<strong>{Dinero(job.job_totals.rates.subtotal).toFormat()}</strong>
|
||||
</Table.Summary.Cell>
|
||||
|
||||
@@ -3,7 +3,6 @@ import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||
import { Col, Row, notification } from "antd";
|
||||
import Axios from "axios";
|
||||
import _ from "lodash";
|
||||
import dayjs from "../../utils/day";
|
||||
import queryString from "query-string";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -24,6 +23,8 @@ import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selecto
|
||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
import confirmDialog from "../../utils/asyncConfirm";
|
||||
import CriticalPartsScan from "../../utils/criticalPartsScan";
|
||||
import dayjs from "../../utils/day";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
import JobsAvailableScan from "../jobs-available-scan/jobs-available-scan.component";
|
||||
import JobsFindModalContainer from "../jobs-find-modal/jobs-find-modal.container";
|
||||
@@ -32,7 +33,6 @@ import OwnerFindModalContainer from "../owner-find-modal/owner-find-modal.contai
|
||||
import { GetSupplementDelta } from "./jobs-available-supplement.estlines.util";
|
||||
import HeaderFields from "./jobs-available-supplement.headerfields";
|
||||
import JobsAvailableTableComponent from "./jobs-available-table.component";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -580,12 +580,13 @@ function ResolveCCCLineIssues(estData, bodyshop) {
|
||||
InstanceRenderManager({
|
||||
executeFunction: true,
|
||||
args: [],
|
||||
promanager: () => {
|
||||
rome: () => {
|
||||
if (line.mod_lbr_ty === "LAET" || line.mod_lbr_ty === "LAUT") {
|
||||
// line.notes += ` | ET/UT Update (prev = ${line.mod_lbr_ty})`;
|
||||
line.mod_lbr_ty = "LAR";
|
||||
}
|
||||
}
|
||||
},
|
||||
promanager: "USE_ROME"
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,55 +1,13 @@
|
||||
import { useQuery } from "@apollo/client";
|
||||
import queryString from "query-string";
|
||||
import React from "react";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import { QUERY_BILLS_BY_JOBID } from "../../graphql/bills.queries";
|
||||
import JobsDetailPliComponent from "./jobs-detail-pli.component";
|
||||
|
||||
export default function JobsDetailPliContainer({ job }) {
|
||||
const billsQuery = useQuery(QUERY_BILLS_BY_JOBID, {
|
||||
variables: { jobid: job.id },
|
||||
fetchPolicy: "network-only",
|
||||
nextFetchPolicy: "network-only"
|
||||
});
|
||||
|
||||
const search = queryString.parse(useLocation().search);
|
||||
const history = useNavigate();
|
||||
|
||||
const handleBillOnRowClick = (record) => {
|
||||
if (record) {
|
||||
if (record.id) {
|
||||
search.billid = record.id;
|
||||
history({ search: queryString.stringify(search) });
|
||||
}
|
||||
} else {
|
||||
delete search.billid;
|
||||
history({ search: queryString.stringify(search) });
|
||||
}
|
||||
};
|
||||
|
||||
const handlePartsOrderOnRowClick = (record) => {
|
||||
if (record) {
|
||||
if (record.id) {
|
||||
search.partsorderid = record.id;
|
||||
history({ search: queryString.stringify(search) });
|
||||
}
|
||||
} else {
|
||||
delete search.partsorderid;
|
||||
history({ search: queryString.stringify(search) });
|
||||
}
|
||||
};
|
||||
|
||||
const handlePartsDispatchOnRowClick = (record) => {
|
||||
if (record) {
|
||||
if (record.id) {
|
||||
search.partsdispatchid = record.id;
|
||||
history.push({ search: queryString.stringify(search) });
|
||||
}
|
||||
} else {
|
||||
delete search.partsdispatchid;
|
||||
history.push({ search: queryString.stringify(search) });
|
||||
}
|
||||
};
|
||||
export default function JobsDetailPliContainer({
|
||||
job,
|
||||
billsQuery,
|
||||
handleBillOnRowClick,
|
||||
handlePartsOrderOnRowClick,
|
||||
handlePartsDispatchOnRowClick
|
||||
}) {
|
||||
return (
|
||||
<JobsDetailPliComponent
|
||||
job={job}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Collapse, Form, Switch } from "antd";
|
||||
import { Collapse, Form, InputNumber, Switch } from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
@@ -17,6 +17,9 @@ export function JobsDetailRatesLabor({ jobRO, expanded, required = true, form })
|
||||
<Collapse defaultActiveKey={expanded && "rates"}>
|
||||
<Collapse.Panel forceRender header={t("jobs.labels.cieca_pfl")} key="cieca_pfl">
|
||||
<LayoutFormRow header={t("joblines.fields.lbr_types.LAB")}>
|
||||
<Form.Item label={t("jobs.fields.cieca_pfl.lbr_adjp")} name={["cieca_pfl", "LAB", "lbr_adjp"]}>
|
||||
<InputNumber min={-100} max={100} precision={4} disabled={jobRO} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.cieca_pfl.lbr_tax_in")}
|
||||
name={["cieca_pfl", "LAB", "lbr_tax_in"]}
|
||||
@@ -24,6 +27,24 @@ export function JobsDetailRatesLabor({ jobRO, expanded, required = true, form })
|
||||
>
|
||||
<Switch disabled={jobRO} />
|
||||
</Form.Item>
|
||||
<Form.Item shouldUpdate>
|
||||
{() => {
|
||||
return (
|
||||
<Form.Item
|
||||
label={t("jobs.fields.cieca_pfl.lbr_taxp")}
|
||||
name={["cieca_pfl", "LAB", "lbr_taxp"]}
|
||||
rules={[
|
||||
{
|
||||
required: form.getFieldValue(["cieca_pfl", "LAB", "lbr_tax_in"])
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
|
||||
</Form.Item>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.cieca_pfl.lbr_tx_in1")}
|
||||
name={["cieca_pfl", "LAB", "lbr_tx_in1"]}
|
||||
@@ -61,6 +82,9 @@ export function JobsDetailRatesLabor({ jobRO, expanded, required = true, form })
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<LayoutFormRow header={t("joblines.fields.lbr_types.LAD")}>
|
||||
<Form.Item label={t("jobs.fields.cieca_pfl.lbr_adjp")} name={["cieca_pfl", "LAD", "lbr_adjp"]}>
|
||||
<InputNumber min={-100} max={100} precision={4} disabled={jobRO} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.cieca_pfl.lbr_tax_in")}
|
||||
name={["cieca_pfl", "LAD", "lbr_tax_in"]}
|
||||
@@ -68,6 +92,24 @@ export function JobsDetailRatesLabor({ jobRO, expanded, required = true, form })
|
||||
>
|
||||
<Switch disabled={jobRO} />
|
||||
</Form.Item>
|
||||
<Form.Item shouldUpdate>
|
||||
{() => {
|
||||
return (
|
||||
<Form.Item
|
||||
label={t("jobs.fields.cieca_pfl.lbr_taxp")}
|
||||
name={["cieca_pfl", "LAD", "lbr_taxp"]}
|
||||
rules={[
|
||||
{
|
||||
required: form.getFieldValue(["cieca_pfl", "LAD", "lbr_tax_in"])
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
|
||||
</Form.Item>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.cieca_pfl.lbr_tx_in1")}
|
||||
name={["cieca_pfl", "LAD", "lbr_tx_in1"]}
|
||||
@@ -105,6 +147,9 @@ export function JobsDetailRatesLabor({ jobRO, expanded, required = true, form })
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<LayoutFormRow header={t("joblines.fields.lbr_types.LAE")}>
|
||||
<Form.Item label={t("jobs.fields.cieca_pfl.lbr_adjp")} name={["cieca_pfl", "LAE", "lbr_adjp"]}>
|
||||
<InputNumber min={-100} max={100} precision={4} disabled={jobRO} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.cieca_pfl.lbr_tax_in")}
|
||||
name={["cieca_pfl", "LAE", "lbr_tax_in"]}
|
||||
@@ -112,6 +157,24 @@ export function JobsDetailRatesLabor({ jobRO, expanded, required = true, form })
|
||||
>
|
||||
<Switch disabled={jobRO} />
|
||||
</Form.Item>
|
||||
<Form.Item shouldUpdate>
|
||||
{() => {
|
||||
return (
|
||||
<Form.Item
|
||||
label={t("jobs.fields.cieca_pfl.lbr_taxp")}
|
||||
name={["cieca_pfl", "LAE", "lbr_taxp"]}
|
||||
rules={[
|
||||
{
|
||||
required: form.getFieldValue(["cieca_pfl", "LAE", "lbr_tax_in"])
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
|
||||
</Form.Item>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.cieca_pfl.lbr_tx_in1")}
|
||||
name={["cieca_pfl", "LAE", "lbr_tx_in1"]}
|
||||
@@ -149,6 +212,9 @@ export function JobsDetailRatesLabor({ jobRO, expanded, required = true, form })
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<LayoutFormRow header={t("joblines.fields.lbr_types.LAF")}>
|
||||
<Form.Item label={t("jobs.fields.cieca_pfl.lbr_adjp")} name={["cieca_pfl", "LAF", "lbr_adjp"]}>
|
||||
<InputNumber min={-100} max={100} precision={4} disabled={jobRO} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.cieca_pfl.lbr_tax_in")}
|
||||
name={["cieca_pfl", "LAF", "lbr_tax_in"]}
|
||||
@@ -156,6 +222,24 @@ export function JobsDetailRatesLabor({ jobRO, expanded, required = true, form })
|
||||
>
|
||||
<Switch disabled={jobRO} />
|
||||
</Form.Item>
|
||||
<Form.Item shouldUpdate>
|
||||
{() => {
|
||||
return (
|
||||
<Form.Item
|
||||
label={t("jobs.fields.cieca_pfl.lbr_taxp")}
|
||||
name={["cieca_pfl", "LAF", "lbr_taxp"]}
|
||||
rules={[
|
||||
{
|
||||
required: form.getFieldValue(["cieca_pfl", "LAF", "lbr_tax_in"])
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
|
||||
</Form.Item>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.cieca_pfl.lbr_tx_in1")}
|
||||
name={["cieca_pfl", "LAF", "lbr_tx_in1"]}
|
||||
@@ -193,6 +277,9 @@ export function JobsDetailRatesLabor({ jobRO, expanded, required = true, form })
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<LayoutFormRow header={t("joblines.fields.lbr_types.LAG")}>
|
||||
<Form.Item label={t("jobs.fields.cieca_pfl.lbr_adjp")} name={["cieca_pfl", "LAG", "lbr_adjp"]}>
|
||||
<InputNumber min={-100} max={100} precision={4} disabled={jobRO} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.cieca_pfl.lbr_tax_in")}
|
||||
name={["cieca_pfl", "LAG", "lbr_tax_in"]}
|
||||
@@ -200,6 +287,24 @@ export function JobsDetailRatesLabor({ jobRO, expanded, required = true, form })
|
||||
>
|
||||
<Switch disabled={jobRO} />
|
||||
</Form.Item>
|
||||
<Form.Item shouldUpdate>
|
||||
{() => {
|
||||
return (
|
||||
<Form.Item
|
||||
label={t("jobs.fields.cieca_pfl.lbr_taxp")}
|
||||
name={["cieca_pfl", "LAG", "lbr_taxp"]}
|
||||
rules={[
|
||||
{
|
||||
required: form.getFieldValue(["cieca_pfl", "LAG", "lbr_tax_in"])
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
|
||||
</Form.Item>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.cieca_pfl.lbr_tx_in1")}
|
||||
name={["cieca_pfl", "LAG", "lbr_tx_in1"]}
|
||||
@@ -237,6 +342,9 @@ export function JobsDetailRatesLabor({ jobRO, expanded, required = true, form })
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<LayoutFormRow header={t("joblines.fields.lbr_types.LAM")}>
|
||||
<Form.Item label={t("jobs.fields.cieca_pfl.lbr_adjp")} name={["cieca_pfl", "LAM", "lbr_adjp"]}>
|
||||
<InputNumber min={-100} max={100} precision={4} disabled={jobRO} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.cieca_pfl.lbr_tax_in")}
|
||||
name={["cieca_pfl", "LAM", "lbr_tax_in"]}
|
||||
@@ -244,6 +352,24 @@ export function JobsDetailRatesLabor({ jobRO, expanded, required = true, form })
|
||||
>
|
||||
<Switch disabled={jobRO} />
|
||||
</Form.Item>
|
||||
<Form.Item shouldUpdate>
|
||||
{() => {
|
||||
return (
|
||||
<Form.Item
|
||||
label={t("jobs.fields.cieca_pfl.lbr_taxp")}
|
||||
name={["cieca_pfl", "LAM", "lbr_taxp"]}
|
||||
rules={[
|
||||
{
|
||||
required: form.getFieldValue(["cieca_pfl", "LAM", "lbr_tax_in"])
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
|
||||
</Form.Item>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.cieca_pfl.lbr_tx_in1")}
|
||||
name={["cieca_pfl", "LAM", "lbr_tx_in1"]}
|
||||
@@ -281,6 +407,9 @@ export function JobsDetailRatesLabor({ jobRO, expanded, required = true, form })
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<LayoutFormRow header={t("joblines.fields.lbr_types.LAR")}>
|
||||
<Form.Item label={t("jobs.fields.cieca_pfl.lbr_adjp")} name={["cieca_pfl", "LAR", "lbr_adjp"]}>
|
||||
<InputNumber min={-100} max={100} precision={4} disabled={jobRO} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.cieca_pfl.lbr_tax_in")}
|
||||
name={["cieca_pfl", "LAR", "lbr_tax_in"]}
|
||||
@@ -288,6 +417,24 @@ export function JobsDetailRatesLabor({ jobRO, expanded, required = true, form })
|
||||
>
|
||||
<Switch disabled={jobRO} />
|
||||
</Form.Item>
|
||||
<Form.Item shouldUpdate>
|
||||
{() => {
|
||||
return (
|
||||
<Form.Item
|
||||
label={t("jobs.fields.cieca_pfl.lbr_taxp")}
|
||||
name={["cieca_pfl", "LAR", "lbr_taxp"]}
|
||||
rules={[
|
||||
{
|
||||
required: form.getFieldValue(["cieca_pfl", "LAR", "lbr_tax_in"])
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
|
||||
</Form.Item>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.cieca_pfl.lbr_tx_in1")}
|
||||
name={["cieca_pfl", "LAR", "lbr_tx_in1"]}
|
||||
@@ -325,6 +472,9 @@ export function JobsDetailRatesLabor({ jobRO, expanded, required = true, form })
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<LayoutFormRow header={t("joblines.fields.lbr_types.LAS")}>
|
||||
<Form.Item label={t("jobs.fields.cieca_pfl.lbr_adjp")} name={["cieca_pfl", "LAS", "lbr_adjp"]}>
|
||||
<InputNumber min={-100} max={100} precision={4} disabled={jobRO} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.cieca_pfl.lbr_tax_in")}
|
||||
name={["cieca_pfl", "LAS", "lbr_tax_in"]}
|
||||
@@ -332,6 +482,24 @@ export function JobsDetailRatesLabor({ jobRO, expanded, required = true, form })
|
||||
>
|
||||
<Switch disabled={jobRO} />
|
||||
</Form.Item>
|
||||
<Form.Item shouldUpdate>
|
||||
{() => {
|
||||
return (
|
||||
<Form.Item
|
||||
label={t("jobs.fields.cieca_pfl.lbr_taxp")}
|
||||
name={["cieca_pfl", "LAS", "lbr_taxp"]}
|
||||
rules={[
|
||||
{
|
||||
required: form.getFieldValue(["cieca_pfl", "LAS", "lbr_tax_in"])
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
|
||||
</Form.Item>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.cieca_pfl.lbr_tx_in1")}
|
||||
name={["cieca_pfl", "LAS", "lbr_tx_in1"]}
|
||||
@@ -369,6 +537,9 @@ export function JobsDetailRatesLabor({ jobRO, expanded, required = true, form })
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<LayoutFormRow header={t("joblines.fields.lbr_types.LAU")}>
|
||||
<Form.Item label={t("jobs.fields.cieca_pfl.lbr_adjp")} name={["cieca_pfl", "LAU", "lbr_adjp"]}>
|
||||
<InputNumber min={-100} max={100} precision={4} disabled={jobRO} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.cieca_pfl.lbr_tax_in")}
|
||||
name={["cieca_pfl", "LAU", "lbr_tax_in"]}
|
||||
@@ -376,6 +547,24 @@ export function JobsDetailRatesLabor({ jobRO, expanded, required = true, form })
|
||||
>
|
||||
<Switch disabled={jobRO} />
|
||||
</Form.Item>
|
||||
<Form.Item shouldUpdate>
|
||||
{() => {
|
||||
return (
|
||||
<Form.Item
|
||||
label={t("jobs.fields.cieca_pfl.lbr_taxp")}
|
||||
name={["cieca_pfl", "LAU", "lbr_taxp"]}
|
||||
rules={[
|
||||
{
|
||||
required: form.getFieldValue(["cieca_pfl", "LAU", "lbr_tax_in"])
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
|
||||
</Form.Item>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.cieca_pfl.lbr_tx_in1")}
|
||||
name={["cieca_pfl", "LAU", "lbr_tx_in1"]}
|
||||
|
||||
@@ -23,7 +23,9 @@ export function JobsDetailRatesMaterials({ jobRO, expanded, required = true, for
|
||||
<Form.Item label={t("jobs.fields.materials.cal_opcode")} name={["materials", "MAPA", "cal_opcode"]}>
|
||||
<Input disabled={jobRO} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={t("jobs.fields.materials.mat_adjp")} name={["materials", "MAPA", "mat_adjp"]}>
|
||||
<InputNumber min={-100} max={100} precision={4} disabled={jobRO} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.materials.tax_ind")}
|
||||
name={["materials", "MAPA", "tax_ind"]}
|
||||
@@ -31,6 +33,24 @@ export function JobsDetailRatesMaterials({ jobRO, expanded, required = true, for
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item shouldUpdate>
|
||||
{() => {
|
||||
return (
|
||||
<Form.Item
|
||||
label={t("jobs.fields.materials.mat_taxp")}
|
||||
name={["materials", "MAPA", "mat_taxp"]}
|
||||
rules={[
|
||||
{
|
||||
required: form.getFieldValue(["materials", "MAPA", "tax_ind"])
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
|
||||
</Form.Item>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.materials.mat_tx_in1")}
|
||||
name={["materials", "MAPA", "mat_tx_in1"]}
|
||||
@@ -74,7 +94,9 @@ export function JobsDetailRatesMaterials({ jobRO, expanded, required = true, for
|
||||
<Form.Item label={t("jobs.fields.materials.cal_opcode")} name={["materials", "MASH", "cal_opcode"]}>
|
||||
<Input disabled={jobRO} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={t("jobs.fields.materials.mat_adjp")} name={["materials", "MAPA", "mat_adjp"]}>
|
||||
<InputNumber min={-100} max={100} precision={4} disabled={jobRO} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.materials.tax_ind")}
|
||||
name={["materials", "MASH", "tax_ind"]}
|
||||
@@ -82,6 +104,24 @@ export function JobsDetailRatesMaterials({ jobRO, expanded, required = true, for
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item shouldUpdate>
|
||||
{() => {
|
||||
return (
|
||||
<Form.Item
|
||||
label={t("jobs.fields.materials.mat_taxp")}
|
||||
name={["materials", "MASH", "mat_taxp"]}
|
||||
rules={[
|
||||
{
|
||||
required: form.getFieldValue(["materials", "MASH", "tax_ind"])
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
|
||||
</Form.Item>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.materials.mat_tx_in1")}
|
||||
name={["materials", "MASH", "mat_tx_in1"]}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
@@ -988,18 +989,24 @@ export function JobsDetailRatesParts({ jobRO, expanded, required = true, form })
|
||||
<Form.Item label={t("jobs.fields.tax_str_rt")} name="tax_str_rt">
|
||||
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("jobs.fields.tax_paint_mat_rt")} name="tax_paint_mat_rt">
|
||||
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("jobs.fields.tax_shop_mat_rt")} name="tax_shop_mat_rt">
|
||||
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
|
||||
</Form.Item>
|
||||
{InstanceRenderManager({ imex: true, rome: false, promanager: "USE_ROME" }) ? (
|
||||
<>
|
||||
<Form.Item label={t("jobs.fields.tax_paint_mat_rt")} name="tax_paint_mat_rt">
|
||||
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("jobs.fields.tax_shop_mat_rt")} name="tax_shop_mat_rt">
|
||||
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
|
||||
</Form.Item>{" "}
|
||||
</>
|
||||
) : null}
|
||||
<Form.Item label={t("jobs.fields.tax_sub_rt")} name="tax_sub_rt">
|
||||
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("jobs.fields.tax_lbr_rt")} name="tax_lbr_rt">
|
||||
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
|
||||
</Form.Item>
|
||||
{InstanceRenderManager({ imex: true, rome: false, promanager: "USE_ROME" }) ? (
|
||||
<Form.Item label={t("jobs.fields.tax_lbr_rt")} name="tax_lbr_rt">
|
||||
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
|
||||
</Form.Item>
|
||||
) : null}
|
||||
<Form.Item label={t("jobs.fields.tax_levies_rt")} name="tax_levies_rt">
|
||||
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
|
||||
</Form.Item>
|
||||
|
||||
@@ -11,6 +11,7 @@ import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import { pageLimit } from "../../utils/config";
|
||||
import { alphaSort, statusSort } from "../../utils/sorters";
|
||||
import useLocalStorage from "../../utils/useLocalStorage";
|
||||
import StartChatButton from "../chat-open-button/chat-open-button.component";
|
||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||
@@ -37,7 +38,10 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
|
||||
title: t("jobs.fields.ro_number"),
|
||||
dataIndex: "ro_number",
|
||||
key: "ro_number",
|
||||
sorter: true, //(a, b) => alphaSort(a.ro_number, b.ro_number),
|
||||
sorter: search?.search
|
||||
? (a, b) =>
|
||||
parseInt((a.ro_number || "0").replace(/\D/g, "")) - parseInt((b.ro_number || "0").replace(/\D/g, ""))
|
||||
: true,
|
||||
sortOrder: sortcolumn === "ro_number" && sortorder,
|
||||
render: (text, record) => (
|
||||
<Link to={"/manage/jobs/" + record.id}>{record.ro_number || t("general.labels.na")}</Link>
|
||||
@@ -49,7 +53,6 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
|
||||
key: "ownr_ln",
|
||||
ellipsis: true,
|
||||
//sorter: true, // (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
|
||||
|
||||
//sortOrder: sortcolumn === "ownr_ln" && sortorder,
|
||||
render: (text, record) => {
|
||||
return record.ownerid ? (
|
||||
@@ -67,7 +70,6 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
|
||||
title: t("jobs.fields.ownr_ph1"),
|
||||
dataIndex: "ownr_ph1",
|
||||
key: "ownr_ph1",
|
||||
|
||||
ellipsis: true,
|
||||
render: (text, record) => <StartChatButton phone={record.ownr_ph1} jobid={record.id} />
|
||||
},
|
||||
@@ -75,7 +77,6 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
|
||||
title: t("jobs.fields.ownr_ph2"),
|
||||
dataIndex: "ownr_ph2",
|
||||
key: "ownr_ph2",
|
||||
|
||||
ellipsis: true,
|
||||
render: (text, record) => <StartChatButton phone={record.ownr_ph2} jobid={record.id} />
|
||||
},
|
||||
@@ -85,7 +86,7 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
|
||||
key: "status",
|
||||
|
||||
ellipsis: true,
|
||||
sorter: true, // (a, b) => alphaSort(a.status, b.status),
|
||||
sorter: search?.search ? (a, b) => statusSort(a.status, b.status, bodyshop.md_ro_statuses.active_statuses) : true,
|
||||
sortOrder: sortcolumn === "status" && sortorder,
|
||||
render: (text, record) => {
|
||||
return record.status || t("general.labels.na");
|
||||
@@ -100,7 +101,6 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
|
||||
title: t("jobs.fields.vehicle"),
|
||||
dataIndex: "vehicle",
|
||||
key: "vehicle",
|
||||
|
||||
ellipsis: true,
|
||||
render: (text, record) => {
|
||||
return record.vehicleid ? (
|
||||
@@ -117,7 +117,7 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
|
||||
dataIndex: "plate_no",
|
||||
key: "plate_no",
|
||||
ellipsis: true,
|
||||
sorter: true, //(a, b) => alphaSort(a.plate_no, b.plate_no),
|
||||
sorter: search?.search ? (a, b) => alphaSort(a.plate_no, b.plate_no) : true,
|
||||
sortOrder: sortcolumn === "plate_no" && sortorder,
|
||||
render: (text, record) => {
|
||||
return record.plate_no ? record.plate_no : "";
|
||||
@@ -128,7 +128,7 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
|
||||
dataIndex: "clm_no",
|
||||
key: "clm_no",
|
||||
ellipsis: true,
|
||||
sorter: true, //(a, b) => alphaSort(a.clm_no, b.clm_no),
|
||||
sorter: search?.search ? (a, b) => alphaSort(a.clm_no, b.clm_no) : true,
|
||||
sortOrder: sortcolumn === "clm_no" && sortorder,
|
||||
render: (text, record) => `${record.clm_no || ""}${record.po_number ? ` (PO: ${record.po_number})` : ""}`
|
||||
},
|
||||
@@ -142,8 +142,7 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
|
||||
title: t("jobs.fields.clm_total"),
|
||||
dataIndex: "clm_total",
|
||||
key: "clm_total",
|
||||
|
||||
sorter: true, //(a, b) => a.clm_total - b.clm_total,
|
||||
sorter: search?.search ? (a, b) => a.clm_total - b.clm_total : true,
|
||||
sortOrder: sortcolumn === "clm_total" && sortorder,
|
||||
render: (text, record) => {
|
||||
return record.clm_total ? (
|
||||
@@ -157,7 +156,6 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
|
||||
title: t("jobs.fields.owner_owing"),
|
||||
dataIndex: "owner_owing",
|
||||
key: "owner_owing",
|
||||
|
||||
render: (text, record) => <CurrencyFormatter>{record.owner_owing}</CurrencyFormatter>
|
||||
},
|
||||
{
|
||||
|
||||
@@ -16,17 +16,15 @@ import useLocalStorage from "../../utils/useLocalStorage";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
import ChatOpenButton from "../chat-open-button/chat-open-button.component";
|
||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||
import { setJoyRideSteps } from "../../redux/application/application.actions";
|
||||
import { OwnerNameDisplayFunction } from "./../owner-name-display/owner-name-display.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setJoyRideSteps: (steps) => dispatch(setJoyRideSteps(steps))
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({});
|
||||
|
||||
export function JobsList({ bodyshop, setJoyRideSteps }) {
|
||||
export function JobsList({ bodyshop }) {
|
||||
const searchParams = queryString.parse(useLocation().search);
|
||||
const { selected } = searchParams;
|
||||
const selectedBreakpoint = Object.entries(Grid.useBreakpoint())
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { Button, notification, Popconfirm } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
|
||||
import { UPDATE_JOBS } from "../../graphql/jobs.queries";
|
||||
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
currentUser: selectCurrentUser
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type }))
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(JobMarkSelectedExported);
|
||||
|
||||
export function JobMarkSelectedExported({
|
||||
bodyshop,
|
||||
currentUser,
|
||||
jobIds,
|
||||
disabled,
|
||||
loadingCallback,
|
||||
completedCallback,
|
||||
refetch,
|
||||
insertAuditTrail
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [insertExportLog] = useMutation(INSERT_EXPORT_LOG);
|
||||
|
||||
const [updateJob] = useMutation(UPDATE_JOBS);
|
||||
const handleUpdate = async () => {
|
||||
setLoading(true);
|
||||
loadingCallback(true);
|
||||
const result = await updateJob({
|
||||
variables: {
|
||||
jobIds: jobIds,
|
||||
fields: {
|
||||
status: bodyshop.md_ro_statuses.default_exported || "Exported*",
|
||||
date_exported: new Date()
|
||||
}
|
||||
},
|
||||
update(cache) {}
|
||||
});
|
||||
|
||||
await insertExportLog({
|
||||
variables: {
|
||||
logs: jobIds.map((id) => {
|
||||
return {
|
||||
bodyshopid: bodyshop.id,
|
||||
jobid: id,
|
||||
successful: true,
|
||||
message: JSON.stringify([t("general.labels.markedexported")]),
|
||||
useremail: currentUser.email
|
||||
};
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
if (!result.errors) {
|
||||
notification["success"]({ message: t("jobs.successes.save") });
|
||||
result.data.update_jobs.returning.forEach((job) => {
|
||||
console.log("results job", job.id, "audit: ", AuditTrailMapping.admin_jobmarkexported());
|
||||
insertAuditTrail({
|
||||
jobid: job.id,
|
||||
operation: AuditTrailMapping.admin_jobmarkexported(),
|
||||
type: "admin_jobmarkexported"
|
||||
});
|
||||
});
|
||||
} else {
|
||||
notification["error"]({
|
||||
message: t("jobs.errors.saving", {
|
||||
error: JSON.stringify(result.errors)
|
||||
})
|
||||
});
|
||||
}
|
||||
loadingCallback(false);
|
||||
completedCallback && completedCallback([]);
|
||||
setLoading(false);
|
||||
refetch && refetch();
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Popconfirm
|
||||
open={open}
|
||||
title={t("general.labels.areyousure")}
|
||||
onCancel={() => setOpen(false)}
|
||||
onConfirm={handleUpdate}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Button loading={loading} disabled={disabled} onClick={() => setOpen(true)} type="primary" danger>
|
||||
{t("jobs.actions.markasexported")}
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,411 @@
|
||||
import { DeleteFilled, EyeFilled } from "@ant-design/icons";
|
||||
import { PageHeader } from "@ant-design/pro-layout";
|
||||
import { useLazyQuery, useMutation } from "@apollo/client";
|
||||
import { Button, Drawer, Grid, Popconfirm, Space, Table } from "antd";
|
||||
|
||||
import queryString from "query-string";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { FaTasks } from "react-icons/fa";
|
||||
import { connect } from "react-redux";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { QUERY_BILL_BY_PK } from "../../graphql/bills.queries";
|
||||
import { DELETE_PARTS_ORDER } from "../../graphql/parts-orders.queries";
|
||||
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import { DateFormatter } from "../../utils/DateFormatter";
|
||||
import { TemplateList } from "../../utils/TemplateConstants";
|
||||
import { alphaSort } from "../../utils/sorters";
|
||||
import DataLabel from "../data-label/data-label.component";
|
||||
import FeatureWrapperComponent from "../feature-wrapper/feature-wrapper.component";
|
||||
import PartsOrderBackorderEta from "../parts-order-backorder-eta/parts-order-backorder-eta.component";
|
||||
import PartsOrderCmReceived from "../parts-order-cm-received/parts-order-cm-received.component";
|
||||
import PartsOrderDeleteLine from "../parts-order-delete-line/parts-order-delete-line.component";
|
||||
import PartsOrderLineBackorderButton from "../parts-order-line-backorder-button/parts-order-line-backorder-button.component";
|
||||
import PartsReceiveModalContainer from "../parts-receive-modal/parts-receive-modal.container";
|
||||
import PrintWrapper from "../print-wrapper/print-wrapper.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
jobRO: selectJobReadOnly,
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setBillEnterContext: (context) =>
|
||||
dispatch(
|
||||
setModalContext({
|
||||
context: context,
|
||||
modal: "billEnter"
|
||||
})
|
||||
),
|
||||
setPartsReceiveContext: (context) =>
|
||||
dispatch(
|
||||
setModalContext({
|
||||
context: context,
|
||||
modal: "partsReceive"
|
||||
})
|
||||
),
|
||||
setTaskUpsertContext: (context) => dispatch(setModalContext({ context, modal: "taskUpsert" }))
|
||||
});
|
||||
|
||||
export function PartsOrderListTableDrawerComponent({
|
||||
setBillEnterContext,
|
||||
bodyshop,
|
||||
jobRO,
|
||||
job,
|
||||
billsQuery,
|
||||
handleOnRowClick,
|
||||
setPartsReceiveContext,
|
||||
setTaskUpsertContext
|
||||
}) {
|
||||
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 responsibilityCenters = bodyshop.md_responsibility_centers;
|
||||
const Templates = TemplateList("partsorder", { job });
|
||||
|
||||
const { t } = useTranslation();
|
||||
const [state, setState] = useState({
|
||||
sortedInfo: {}
|
||||
});
|
||||
|
||||
const [returnfrombill, setReturnFromBill] = useState();
|
||||
const [billData, setBillData] = useState();
|
||||
const search = queryString.parse(useLocation().search);
|
||||
const selectedpartsorder = search.partsorderid;
|
||||
|
||||
const [billQuery] = useLazyQuery(QUERY_BILL_BY_PK);
|
||||
const [deletePartsOrder] = useMutation(DELETE_PARTS_ORDER);
|
||||
const parts_orders = billsQuery.data ? billsQuery.data.parts_orders : [];
|
||||
const { refetch } = billsQuery;
|
||||
|
||||
useEffect(() => {
|
||||
if (returnfrombill === null) {
|
||||
setBillData(null);
|
||||
} else {
|
||||
const fetchData = async () => {
|
||||
const result = await billQuery({
|
||||
variables: { billid: returnfrombill }
|
||||
});
|
||||
setBillData(result.data);
|
||||
};
|
||||
fetchData();
|
||||
}
|
||||
}, [returnfrombill, billQuery]);
|
||||
|
||||
const recordActions = (record, showView = false) => (
|
||||
<Space direction="horizontal" wrap>
|
||||
{showView && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (record.returnfrombill) {
|
||||
setReturnFromBill(record.returnfrombill);
|
||||
} else {
|
||||
setReturnFromBill(null);
|
||||
}
|
||||
handleOnRowClick(record);
|
||||
}}
|
||||
>
|
||||
<EyeFilled />
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
disabled={jobRO || record.return || record.vendor.id === bodyshop.inhousevendorid}
|
||||
onClick={() => {
|
||||
logImEXEvent("parts_order_receive_bill");
|
||||
setPartsReceiveContext({
|
||||
actions: { refetch: refetch },
|
||||
context: {
|
||||
jobId: job.id,
|
||||
job: job,
|
||||
partsorderlines: record.parts_order_lines.map((pol) => {
|
||||
return {
|
||||
joblineid: pol.job_line_id,
|
||||
id: pol.id,
|
||||
line_desc: pol.line_desc,
|
||||
quantity: pol.quantity,
|
||||
act_price: pol.act_price,
|
||||
oem_partno: pol.oem_partno
|
||||
};
|
||||
})
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t("parts_orders.actions.receive")}
|
||||
</Button>
|
||||
<Button
|
||||
title={t("tasks.buttons.create")}
|
||||
onClick={() => {
|
||||
setTaskUpsertContext({
|
||||
context: {
|
||||
jobid: job.id,
|
||||
partsorderid: record.id
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
<FaTasks />
|
||||
</Button>
|
||||
<Popconfirm
|
||||
title={t("parts_orders.labels.confirmdelete")}
|
||||
disabled={jobRO}
|
||||
onConfirm={async () => {
|
||||
//Delete the parts return.!
|
||||
|
||||
await deletePartsOrder({
|
||||
variables: { partsOrderId: record.id },
|
||||
update(cache) {
|
||||
cache.modify({
|
||||
fields: {
|
||||
parts_orders(existingPartsOrders, { readField }) {
|
||||
return existingPartsOrders.filter((billref) => record.id !== readField("id", billref));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button disabled={jobRO}>
|
||||
<DeleteFilled />
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
<FeatureWrapperComponent featureName="bills" noauth={() => null}>
|
||||
<Button
|
||||
disabled={(jobRO ? !record.return : jobRO) || record.vendor.id === bodyshop.inhousevendorid}
|
||||
onClick={() => {
|
||||
logImEXEvent("parts_order_receive_bill");
|
||||
|
||||
setBillEnterContext({
|
||||
actions: { refetch: refetch },
|
||||
context: {
|
||||
job: job,
|
||||
bill: {
|
||||
vendorid: record.vendor.id,
|
||||
is_credit_memo: record.return,
|
||||
billlines: record.parts_order_lines.map((pol) => {
|
||||
return {
|
||||
joblineid: pol.job_line_id || "noline",
|
||||
line_desc: pol.line_desc,
|
||||
quantity: pol.quantity,
|
||||
|
||||
actual_price: pol.act_price,
|
||||
|
||||
cost_center: pol.jobline?.part_type
|
||||
? bodyshop.pbs_serialnumber || bodyshop.cdk_dealerid
|
||||
? pol.jobline.part_type !== "PAE"
|
||||
? pol.jobline.part_type
|
||||
: null
|
||||
: responsibilityCenters.defaults &&
|
||||
(responsibilityCenters.defaults.costs[pol.jobline.part_type] || null)
|
||||
: null
|
||||
};
|
||||
})
|
||||
}
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t("parts_orders.actions.receivebill")}
|
||||
</Button>
|
||||
</FeatureWrapperComponent>
|
||||
<PrintWrapper
|
||||
templateObject={{
|
||||
name: record.return ? Templates.parts_return_slip.key : Templates.parts_order.key,
|
||||
variables: { id: record.id }
|
||||
}}
|
||||
messageObject={{
|
||||
subject: record.return ? Templates.parts_return_slip.subject : Templates.parts_order.subject,
|
||||
to: record.vendor.email
|
||||
}}
|
||||
id={job.id}
|
||||
/>
|
||||
</Space>
|
||||
);
|
||||
|
||||
const handleTableChange = (pagination, filters, sorter) => {
|
||||
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
||||
};
|
||||
|
||||
const selectedPartsOrderRecord = parts_orders.find((r) => r.id === selectedpartsorder);
|
||||
|
||||
const rowExpander = (record) => {
|
||||
const columns = [
|
||||
{
|
||||
title: t("parts_orders.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("parts_orders.fields.quantity"),
|
||||
dataIndex: "quantity",
|
||||
key: "quantity",
|
||||
sorter: (a, b) => a.quantity - b.quantity,
|
||||
sortOrder: state.sortedInfo.columnKey === "quantity" && state.sortedInfo.order
|
||||
},
|
||||
{
|
||||
title: t("parts_orders.fields.act_price"),
|
||||
dataIndex: "act_price",
|
||||
key: "act_price",
|
||||
sorter: (a, b) => a.act_price - b.act_price,
|
||||
sortOrder: state.sortedInfo.columnKey === "act_price" && state.sortedInfo.order,
|
||||
render: (text, record) => <CurrencyFormatter>{record.act_price}</CurrencyFormatter>
|
||||
},
|
||||
...(selectedPartsOrderRecord && selectedPartsOrderRecord.return
|
||||
? [
|
||||
{
|
||||
title: t("parts_orders.fields.cost"),
|
||||
dataIndex: "cost",
|
||||
key: "cost",
|
||||
sorter: (a, b) => a.cost - b.cost,
|
||||
sortOrder: state.sortedInfo.columnKey === "cost" && state.sortedInfo.order,
|
||||
render: (text, record) => <CurrencyFormatter>{record.cost}</CurrencyFormatter>
|
||||
}
|
||||
]
|
||||
: []),
|
||||
{
|
||||
title: t("parts_orders.fields.part_type"),
|
||||
dataIndex: "part_type",
|
||||
key: "part_type",
|
||||
render: (text, record) => (record.part_type ? t(`joblines.fields.part_types.${record.part_type}`) : null)
|
||||
},
|
||||
{
|
||||
title: t("parts_orders.fields.oem_partno"),
|
||||
dataIndex: "oem_partno",
|
||||
key: "oem_partno",
|
||||
sorter: (a, b) => alphaSort(a.oem_partno, b.oem_partno),
|
||||
sortOrder: state.sortedInfo.columnKey === "oem_partno" && state.sortedInfo.order
|
||||
},
|
||||
{
|
||||
title: t("parts_orders.fields.line_remarks"),
|
||||
dataIndex: "line_remarks",
|
||||
key: "line_remarks"
|
||||
},
|
||||
{
|
||||
title: t("parts_orders.fields.status"),
|
||||
dataIndex: "status",
|
||||
key: "status"
|
||||
},
|
||||
|
||||
...(selectedPartsOrderRecord && selectedPartsOrderRecord.return
|
||||
? [
|
||||
{
|
||||
title: t("parts_orders.fields.cm_received"),
|
||||
dataIndex: "cm_received",
|
||||
key: "cm_received",
|
||||
render: (text, record) => (
|
||||
<PartsOrderCmReceived
|
||||
orderLineId={record.id}
|
||||
checked={record.cm_received}
|
||||
partsorderid={selectedPartsOrderRecord.id}
|
||||
/>
|
||||
)
|
||||
}
|
||||
]
|
||||
: []),
|
||||
{
|
||||
title: t("parts_orders.fields.backordered_on"),
|
||||
dataIndex: "backordered_on",
|
||||
key: "backordered_on",
|
||||
render: (text, record) => <DateFormatter>{text}</DateFormatter>
|
||||
},
|
||||
{
|
||||
title: t("parts_orders.fields.backordered_eta"),
|
||||
dataIndex: "backordered_eta",
|
||||
key: "backordered_eta",
|
||||
render: (text, record) => (
|
||||
<PartsOrderBackorderEta
|
||||
backordered_eta={record.backordered_eta}
|
||||
disabled={jobRO}
|
||||
partsOrderStatus={record.status}
|
||||
partsLineId={record.id}
|
||||
jobLineId={record.job_line_id}
|
||||
/>
|
||||
)
|
||||
},
|
||||
|
||||
{
|
||||
title: t("general.labels.actions"),
|
||||
dataIndex: "actions",
|
||||
key: "actions",
|
||||
render: (text, record) => (
|
||||
<Space wrap>
|
||||
<PartsOrderDeleteLine
|
||||
disabled={jobRO}
|
||||
partsOrderStatus={record.status}
|
||||
partsLineId={record.id}
|
||||
partsOrderId={selectedpartsorder}
|
||||
jobLineId={record.job_line_id}
|
||||
/>
|
||||
<PartsOrderLineBackorderButton
|
||||
disabled={jobRO}
|
||||
partsOrderStatus={record.status}
|
||||
partsLineId={record.id}
|
||||
jobLineId={record.job_line_id}
|
||||
/>
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PageHeader
|
||||
title={
|
||||
billData
|
||||
? `${record.vendor.name} - ${record.order_number} - ${t("bills.labels.returnfrombill")}: ${billData.bills_by_pk.invoice_number}`
|
||||
: `${record.vendor.name} - ${record.order_number}`
|
||||
}
|
||||
extra={recordActions(record)}
|
||||
/>
|
||||
<Table
|
||||
scroll={{
|
||||
x: true //y: "50rem"
|
||||
}}
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
dataSource={record.parts_order_lines}
|
||||
onChange={handleTableChange}
|
||||
/>
|
||||
<DataLabel label={t("parts_orders.fields.comments")}>
|
||||
<div style={{ whiteSpace: "pre" }}>{record.comments}</div>
|
||||
</DataLabel>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PartsReceiveModalContainer />
|
||||
<Drawer
|
||||
placement="right"
|
||||
onClose={() => handleOnRowClick(null)}
|
||||
open={selectedpartsorder}
|
||||
closable
|
||||
width={drawerPercentage}
|
||||
>
|
||||
{selectedPartsOrderRecord && rowExpander(selectedPartsOrderRecord)}
|
||||
</Drawer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(PartsOrderListTableDrawerComponent);
|
||||
@@ -1,33 +1,23 @@
|
||||
import { DeleteFilled, EyeFilled, SyncOutlined } from "@ant-design/icons";
|
||||
import { useLazyQuery, useMutation } from "@apollo/client";
|
||||
import { Button, Card, Checkbox, Drawer, Grid, Input, Popconfirm, Space, Table } from "antd";
|
||||
import { PageHeader } from "@ant-design/pro-layout";
|
||||
|
||||
import queryString from "query-string";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { Button, Card, Checkbox, Input, Popconfirm, Space, Table } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { FaTasks } from "react-icons/fa";
|
||||
import { connect } from "react-redux";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { QUERY_BILL_BY_PK } from "../../graphql/bills.queries";
|
||||
import { DELETE_PARTS_ORDER } from "../../graphql/parts-orders.queries";
|
||||
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import { DateFormatter } from "../../utils/DateFormatter";
|
||||
import { TemplateList } from "../../utils/TemplateConstants";
|
||||
import { alphaSort } from "../../utils/sorters";
|
||||
import DataLabel from "../data-label/data-label.component";
|
||||
import PartsOrderBackorderEta from "../parts-order-backorder-eta/parts-order-backorder-eta.component";
|
||||
import PartsOrderCmReceived from "../parts-order-cm-received/parts-order-cm-received.component";
|
||||
import PartsOrderDeleteLine from "../parts-order-delete-line/parts-order-delete-line.component";
|
||||
import PartsOrderLineBackorderButton from "../parts-order-line-backorder-button/parts-order-line-backorder-button.component";
|
||||
import FeatureWrapperComponent from "../feature-wrapper/feature-wrapper.component";
|
||||
import PartsReceiveModalContainer from "../parts-receive-modal/parts-receive-modal.container";
|
||||
import PrintWrapper from "../print-wrapper/print-wrapper.component";
|
||||
import FeatureWrapperComponent from "../feature-wrapper/feature-wrapper.component";
|
||||
import { FaTasks } from "react-icons/fa";
|
||||
import PartsOrderDrawer from "./parts-order-list-table-drawer.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
jobRO: selectJobReadOnly,
|
||||
@@ -62,19 +52,6 @@ export function PartsOrderListTableComponent({
|
||||
setPartsReceiveContext,
|
||||
setTaskUpsertContext
|
||||
}) {
|
||||
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 responsibilityCenters = bodyshop.md_responsibility_centers;
|
||||
const Templates = TemplateList("partsorder", { job });
|
||||
|
||||
@@ -83,42 +60,17 @@ export function PartsOrderListTableComponent({
|
||||
sortedInfo: {}
|
||||
});
|
||||
|
||||
const [returnfrombill, setReturnFromBill] = useState();
|
||||
const [billData, setBillData] = useState();
|
||||
const search = queryString.parse(useLocation().search);
|
||||
const selectedpartsorder = search.partsorderid;
|
||||
const [searchText, setSearchText] = useState("");
|
||||
|
||||
const [billQuery] = useLazyQuery(QUERY_BILL_BY_PK);
|
||||
const [deletePartsOrder] = useMutation(DELETE_PARTS_ORDER);
|
||||
|
||||
const parts_orders = billsQuery.data ? billsQuery.data.parts_orders : [];
|
||||
const { refetch } = billsQuery;
|
||||
|
||||
useEffect(() => {
|
||||
if (returnfrombill === null) {
|
||||
setBillData(null);
|
||||
} else {
|
||||
const fetchData = async () => {
|
||||
const result = await billQuery({
|
||||
variables: { billid: returnfrombill }
|
||||
});
|
||||
setBillData(result.data);
|
||||
};
|
||||
fetchData();
|
||||
}
|
||||
}, [returnfrombill, billQuery]);
|
||||
|
||||
const recordActions = (record, showView = false) => (
|
||||
<Space direction="horizontal" wrap>
|
||||
{showView && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (record.returnfrombill) {
|
||||
setReturnFromBill(record.returnfrombill);
|
||||
} else {
|
||||
setReturnFromBill(null);
|
||||
}
|
||||
handleOnRowClick(record);
|
||||
}}
|
||||
>
|
||||
@@ -298,154 +250,6 @@ export function PartsOrderListTableComponent({
|
||||
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
||||
};
|
||||
|
||||
const selectedPartsOrderRecord = parts_orders.find((r) => r.id === selectedpartsorder);
|
||||
|
||||
const rowExpander = (record) => {
|
||||
const columns = [
|
||||
{
|
||||
title: t("parts_orders.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("parts_orders.fields.quantity"),
|
||||
dataIndex: "quantity",
|
||||
key: "quantity",
|
||||
sorter: (a, b) => a.quantity - b.quantity,
|
||||
sortOrder: state.sortedInfo.columnKey === "quantity" && state.sortedInfo.order
|
||||
},
|
||||
{
|
||||
title: t("parts_orders.fields.act_price"),
|
||||
dataIndex: "act_price",
|
||||
key: "act_price",
|
||||
sorter: (a, b) => a.act_price - b.act_price,
|
||||
sortOrder: state.sortedInfo.columnKey === "act_price" && state.sortedInfo.order,
|
||||
render: (text, record) => <CurrencyFormatter>{record.act_price}</CurrencyFormatter>
|
||||
},
|
||||
...(selectedPartsOrderRecord && selectedPartsOrderRecord.return
|
||||
? [
|
||||
{
|
||||
title: t("parts_orders.fields.cost"),
|
||||
dataIndex: "cost",
|
||||
key: "cost",
|
||||
sorter: (a, b) => a.cost - b.cost,
|
||||
sortOrder: state.sortedInfo.columnKey === "cost" && state.sortedInfo.order,
|
||||
render: (text, record) => <CurrencyFormatter>{record.cost}</CurrencyFormatter>
|
||||
}
|
||||
]
|
||||
: []),
|
||||
{
|
||||
title: t("parts_orders.fields.part_type"),
|
||||
dataIndex: "part_type",
|
||||
key: "part_type",
|
||||
render: (text, record) => (record.part_type ? t(`joblines.fields.part_types.${record.part_type}`) : null)
|
||||
},
|
||||
{
|
||||
title: t("parts_orders.fields.oem_partno"),
|
||||
dataIndex: "oem_partno",
|
||||
key: "oem_partno",
|
||||
sorter: (a, b) => alphaSort(a.oem_partno, b.oem_partno),
|
||||
sortOrder: state.sortedInfo.columnKey === "oem_partno" && state.sortedInfo.order
|
||||
},
|
||||
{
|
||||
title: t("parts_orders.fields.line_remarks"),
|
||||
dataIndex: "line_remarks",
|
||||
key: "line_remarks"
|
||||
},
|
||||
{
|
||||
title: t("parts_orders.fields.status"),
|
||||
dataIndex: "status",
|
||||
key: "status"
|
||||
},
|
||||
|
||||
...(selectedPartsOrderRecord && selectedPartsOrderRecord.return
|
||||
? [
|
||||
{
|
||||
title: t("parts_orders.fields.cm_received"),
|
||||
dataIndex: "cm_received",
|
||||
key: "cm_received",
|
||||
render: (text, record) => (
|
||||
<PartsOrderCmReceived
|
||||
orderLineId={record.id}
|
||||
checked={record.cm_received}
|
||||
partsorderid={selectedPartsOrderRecord.id}
|
||||
/>
|
||||
)
|
||||
}
|
||||
]
|
||||
: []),
|
||||
{
|
||||
title: t("parts_orders.fields.backordered_on"),
|
||||
dataIndex: "backordered_on",
|
||||
key: "backordered_on",
|
||||
render: (text, record) => <DateFormatter>{text}</DateFormatter>
|
||||
},
|
||||
{
|
||||
title: t("parts_orders.fields.backordered_eta"),
|
||||
dataIndex: "backordered_eta",
|
||||
key: "backordered_eta",
|
||||
render: (text, record) => (
|
||||
<PartsOrderBackorderEta
|
||||
backordered_eta={record.backordered_eta}
|
||||
disabled={jobRO}
|
||||
partsOrderStatus={record.status}
|
||||
partsLineId={record.id}
|
||||
jobLineId={record.job_line_id}
|
||||
/>
|
||||
)
|
||||
},
|
||||
|
||||
{
|
||||
title: t("general.labels.actions"),
|
||||
dataIndex: "actions",
|
||||
key: "actions",
|
||||
render: (text, record) => (
|
||||
<Space wrap>
|
||||
<PartsOrderDeleteLine
|
||||
disabled={jobRO}
|
||||
partsOrderStatus={record.status}
|
||||
partsLineId={record.id}
|
||||
partsOrderId={selectedpartsorder}
|
||||
jobLineId={record.job_line_id}
|
||||
/>
|
||||
<PartsOrderLineBackorderButton
|
||||
disabled={jobRO}
|
||||
partsOrderStatus={record.status}
|
||||
partsLineId={record.id}
|
||||
jobLineId={record.job_line_id}
|
||||
/>
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PageHeader
|
||||
title={
|
||||
billData
|
||||
? `${record.vendor.name} - ${record.order_number} - ${t("bills.labels.returnfrombill")}: ${billData.bills_by_pk.invoice_number}`
|
||||
: `${record.vendor.name} - ${record.order_number}`
|
||||
}
|
||||
extra={recordActions(record)}
|
||||
/>
|
||||
<Table
|
||||
scroll={{
|
||||
x: true //y: "50rem"
|
||||
}}
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
dataSource={record.parts_order_lines}
|
||||
/>
|
||||
<DataLabel label={t("parts_orders.fields.comments")}>
|
||||
<div style={{ whiteSpace: "pre" }}>{record.comments}</div>
|
||||
</DataLabel>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const filteredPartsOrders = parts_orders
|
||||
? searchText === ""
|
||||
? parts_orders
|
||||
@@ -476,15 +280,13 @@ export function PartsOrderListTableComponent({
|
||||
}
|
||||
>
|
||||
<PartsReceiveModalContainer />
|
||||
<Drawer
|
||||
placement="right"
|
||||
onClose={() => handleOnRowClick(null)}
|
||||
open={selectedpartsorder}
|
||||
closable
|
||||
width={drawerPercentage}
|
||||
>
|
||||
{selectedPartsOrderRecord && rowExpander(selectedPartsOrderRecord)}
|
||||
</Drawer>
|
||||
<PartsOrderDrawer
|
||||
job={job}
|
||||
billsQuery={billsQuery}
|
||||
handleOnRowClick={handleOnRowClick}
|
||||
setPartsReceiveContext={setPartsReceiveContext}
|
||||
setTaskUpsertContext={setTaskUpsertContext}
|
||||
/>
|
||||
<Table
|
||||
loading={billsQuery.loading}
|
||||
scroll={{
|
||||
|
||||
@@ -90,7 +90,7 @@ export function BillMarkSelectedExported({
|
||||
onConfirm={handleUpdate}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Button loading={loading} disabled={disabled} onClick={() => setOpen(true)}>
|
||||
<Button loading={loading} disabled={disabled} onClick={() => setOpen(true)} type="primary" danger>
|
||||
{t("bills.labels.markexported")}
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
|
||||
@@ -90,7 +90,7 @@ export function PaymentMarkSelectedExported({
|
||||
onConfirm={handleUpdate}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Button loading={loading} disabled={disabled} onClick={() => setOpen(true)}>
|
||||
<Button loading={loading} disabled={disabled} onClick={() => setOpen(true)} type="primary" danger>
|
||||
{t("bills.labels.markexported")}
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
|
||||
@@ -1,22 +1,31 @@
|
||||
import { Input, Space, Spin } from "antd";
|
||||
import React from "react";
|
||||
import { Button, Input, Space, Spin } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { ExclamationCircleFilled, ExclamationCircleOutlined } from "@ant-design/icons";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import EmployeeSearchSelectComponent from "../employee-search-select/employee-search-select.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ProductionBoardFilters);
|
||||
|
||||
export function ProductionBoardFilters({ bodyshop, filter, setFilter, loading }) {
|
||||
const { t } = useTranslation();
|
||||
const [alertFilter, setAlertFilter] = useState(false);
|
||||
|
||||
const toggleAlertFilter = () => {
|
||||
const newAlertFilter = !alertFilter;
|
||||
setAlertFilter(newAlertFilter);
|
||||
setFilter({ ...filter, alert: newAlertFilter });
|
||||
};
|
||||
|
||||
return (
|
||||
<Space wrap>
|
||||
@@ -36,6 +45,13 @@ export function ProductionBoardFilters({ bodyshop, filter, setFilter, loading })
|
||||
onChange={(emp) => setFilter({ ...filter, employeeId: emp })}
|
||||
allowClear
|
||||
/>
|
||||
<Button
|
||||
type={alertFilter ? "primary" : "default"}
|
||||
onClick={toggleAlertFilter}
|
||||
icon={alertFilter ? <ExclamationCircleFilled /> : <ExclamationCircleOutlined />}
|
||||
>
|
||||
{t("production.labels.alerts")}
|
||||
</Button>
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
.imex-kanban-card {
|
||||
padding: 0px !important;
|
||||
|
||||
.ant-card-body {
|
||||
padding: 0.8rem;
|
||||
}
|
||||
|
||||
.ant-card-head {
|
||||
padding: 0rem 0.8rem;
|
||||
}
|
||||
}
|
||||
@@ -1,201 +0,0 @@
|
||||
import {
|
||||
BranchesOutlined,
|
||||
CalendarOutlined,
|
||||
DownloadOutlined,
|
||||
EyeFilled,
|
||||
PauseCircleOutlined
|
||||
} from "@ant-design/icons";
|
||||
import { Card, Col, Row, Space, Tooltip } from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
||||
import ProductionAlert from "../production-list-columns/production-list-columns.alert.component";
|
||||
import ProductionListColumnProductionNote from "../production-list-columns/production-list-columns.productionnote.component";
|
||||
import ProductionSubletsManageComponent from "../production-sublets-manage/production-sublets-manage.component";
|
||||
import "./production-board-card.styles.scss";
|
||||
import dayjs from "../../utils/day";
|
||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||
import JobPartsQueueCount from "../job-parts-queue-count/job-parts-queue-count.component";
|
||||
|
||||
const cardColor = (ssbuckets, totalHrs) => {
|
||||
const bucket = ssbuckets.filter((bucket) => bucket.gte <= totalHrs && (!!bucket.lt ? bucket.lt > totalHrs : true))[0];
|
||||
|
||||
let color = { r: 255, g: 255, b: 255 };
|
||||
|
||||
if (bucket && bucket.color) {
|
||||
color = bucket.color;
|
||||
|
||||
if (bucket.color.rgb) {
|
||||
color = bucket.color.rgb;
|
||||
}
|
||||
}
|
||||
|
||||
return color;
|
||||
};
|
||||
|
||||
function getContrastYIQ(bgColor) {
|
||||
const yiq = (bgColor.r * 299 + bgColor.g * 587 + bgColor.b * 114) / 1000;
|
||||
|
||||
return yiq >= 128 ? "black" : "white";
|
||||
}
|
||||
|
||||
export default function ProductionBoardCard(technician, card, bodyshop, cardSettings) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
let employee_body, employee_prep, employee_refinish, employee_csr;
|
||||
if (card.employee_body) {
|
||||
employee_body = bodyshop.employees.find((e) => e.id === card.employee_body);
|
||||
}
|
||||
if (card.employee_prep) {
|
||||
employee_prep = bodyshop.employees.find((e) => e.id === card.employee_prep);
|
||||
}
|
||||
if (card.employee_refinish) {
|
||||
employee_refinish = bodyshop.employees.find((e) => e.id === card.employee_refinish);
|
||||
}
|
||||
if (card.employee_csr) {
|
||||
employee_csr = bodyshop.employees.find((e) => e.id === card.employee_csr);
|
||||
}
|
||||
// if (card.employee_csr) {
|
||||
// employee_csr = bodyshop.employees.find((e) => e.id === card.employee_csr);
|
||||
// }
|
||||
|
||||
const pastDueAlert =
|
||||
!!card.scheduled_completion &&
|
||||
((dayjs().isSameOrAfter(dayjs(card.scheduled_completion), "day") && "production-completion-past") ||
|
||||
(dayjs().add(1, "day").isSame(dayjs(card.scheduled_completion), "day") && "production-completion-soon"));
|
||||
|
||||
const totalHrs = card.labhrs.aggregate.sum.mod_lb_hrs + card.larhrs.aggregate.sum.mod_lb_hrs;
|
||||
const bgColor = cardColor(bodyshop.ssbuckets, totalHrs);
|
||||
|
||||
return (
|
||||
<Card
|
||||
className="react-kanban-card imex-kanban-card"
|
||||
size="small"
|
||||
style={{
|
||||
backgroundColor:
|
||||
cardSettings && cardSettings.cardcolor && `rgba(${bgColor.r},${bgColor.g},${bgColor.b},${bgColor.a})`,
|
||||
color: cardSettings && cardSettings.cardcolor && getContrastYIQ(bgColor)
|
||||
}}
|
||||
title={
|
||||
<Space>
|
||||
<ProductionAlert record={card} key="alert" />
|
||||
{card.suspended && <PauseCircleOutlined style={{ color: "orangered" }} />}
|
||||
{card.iouparent && (
|
||||
<Tooltip title={t("jobs.labels.iou")}>
|
||||
<BranchesOutlined style={{ color: "orangered" }} />
|
||||
</Tooltip>
|
||||
)}
|
||||
<span style={{ fontWeight: "bolder" }}>
|
||||
<Link to={technician ? `/tech/joblookup?selected=${card.id}` : `/manage/jobs/${card.id}`}>
|
||||
{card.ro_number || t("general.labels.na")}
|
||||
</Link>
|
||||
</span>
|
||||
</Space>
|
||||
}
|
||||
extra={
|
||||
<Link to={{ search: `?selected=${card.id}` }}>
|
||||
<EyeFilled />
|
||||
</Link>
|
||||
}
|
||||
>
|
||||
<Row>
|
||||
{cardSettings && cardSettings.ownr_nm && (
|
||||
<Col span={24}>
|
||||
{cardSettings && cardSettings.compact ? (
|
||||
<div className="ellipses">{`${card.ownr_ln || ""} ${card.ownr_co_nm || ""}`}</div>
|
||||
) : (
|
||||
<div className="ellipses">
|
||||
<OwnerNameDisplay ownerObject={card} />
|
||||
</div>
|
||||
)}
|
||||
</Col>
|
||||
)}
|
||||
<Col span={24}>
|
||||
<div className="ellipses">{`${card.v_model_yr || ""} ${
|
||||
card.v_make_desc || ""
|
||||
} ${card.v_model_desc || ""}`}</div>
|
||||
</Col>
|
||||
{cardSettings && cardSettings.ins_co_nm && card.ins_co_nm && (
|
||||
<Col span={cardSettings && cardSettings.compact ? 24 : 12}>
|
||||
<div className="ellipses">{card.ins_co_nm || ""}</div>
|
||||
</Col>
|
||||
)}
|
||||
{cardSettings && cardSettings.clm_no && card.clm_no && (
|
||||
<Col span={cardSettings && cardSettings.compact ? 24 : 12}>
|
||||
<div className="ellipses">{card.clm_no || ""}</div>
|
||||
</Col>
|
||||
)}
|
||||
|
||||
{cardSettings && cardSettings.employeeassignments && (
|
||||
<Col span={24}>
|
||||
<Row>
|
||||
<Col span={cardSettings && cardSettings.compact ? 24 : 12}>{`B: ${
|
||||
employee_body ? `${employee_body.first_name.substr(0, 3)} ${employee_body.last_name.charAt(0)}` : ""
|
||||
} ${card.labhrs.aggregate.sum.mod_lb_hrs || "?"}h`}</Col>
|
||||
<Col span={cardSettings && cardSettings.compact ? 24 : 12}>{`P: ${
|
||||
employee_prep ? `${employee_prep.first_name.substr(0, 3)} ${employee_prep.last_name.charAt(0)}` : ""
|
||||
}`}</Col>
|
||||
<Col span={cardSettings && cardSettings.compact ? 24 : 12}>{`R: ${
|
||||
employee_refinish
|
||||
? `${employee_refinish.first_name.substr(0, 3)} ${employee_refinish.last_name.charAt(0)}`
|
||||
: ""
|
||||
} ${card.larhrs.aggregate.sum.mod_lb_hrs || "?"}h`}</Col>
|
||||
<Col span={cardSettings && cardSettings.compact ? 24 : 12}>{`C: ${
|
||||
employee_csr ? `${employee_csr.first_name} ${employee_csr.last_name}` : ""
|
||||
}`}</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
)}
|
||||
{/* {cardSettings && cardSettings.laborhrs && (
|
||||
<Col span={24}>
|
||||
<Row>
|
||||
<Col span={cardSettings && cardSettings.compact ? 24 : 12}>{`B: ${
|
||||
card.labhrs.aggregate.sum.mod_lb_hrs || "?"
|
||||
} hrs`}</Col>
|
||||
<Col span={cardSettings && cardSettings.compact ? 24 : 12}>{`R: ${
|
||||
card.larhrs.aggregate.sum.mod_lb_hrs || "?"
|
||||
} hrs`}</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
)} */}
|
||||
{cardSettings && cardSettings.actual_in && card.actual_in && (
|
||||
<Col span={cardSettings && cardSettings.compact ? 24 : 12}>
|
||||
<Space>
|
||||
<DownloadOutlined />
|
||||
<DateTimeFormatter format="MM/DD">{card.actual_in}</DateTimeFormatter>
|
||||
</Space>
|
||||
</Col>
|
||||
)}
|
||||
{cardSettings && cardSettings.scheduled_completion && card.scheduled_completion && (
|
||||
<Col span={cardSettings && cardSettings.compact ? 24 : 12}>
|
||||
<Space className={pastDueAlert}>
|
||||
<CalendarOutlined />
|
||||
<DateTimeFormatter format="MM/DD">{card.scheduled_completion}</DateTimeFormatter>
|
||||
</Space>
|
||||
</Col>
|
||||
)}
|
||||
{cardSettings && cardSettings.ats && card.alt_transport && (
|
||||
<Col span={12}>
|
||||
<div>{card.alt_transport || ""}</div>
|
||||
</Col>
|
||||
)}
|
||||
{cardSettings && cardSettings.sublets && (
|
||||
<Col span={12}>
|
||||
<ProductionSubletsManageComponent subletJobLines={card.subletLines} />
|
||||
</Col>
|
||||
)}
|
||||
{cardSettings && cardSettings.production_note && (
|
||||
<Col span={24}>
|
||||
{cardSettings && cardSettings.production_note && <ProductionListColumnProductionNote record={card} />}
|
||||
</Col>
|
||||
)}
|
||||
{cardSettings && cardSettings.partsstatus && (
|
||||
<Col span={24}>
|
||||
<JobPartsQueueCount parts={card.joblines_status} />
|
||||
</Col>
|
||||
)}
|
||||
</Row>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,437 @@
|
||||
import {
|
||||
BranchesOutlined,
|
||||
CalendarOutlined,
|
||||
DownloadOutlined,
|
||||
EyeFilled,
|
||||
PauseCircleOutlined
|
||||
} from "@ant-design/icons";
|
||||
import { Card, Col, Row, Space, Tooltip } from "antd";
|
||||
import React, { useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
||||
import Dinero from "dinero.js";
|
||||
|
||||
import ProductionAlert from "../production-list-columns/production-list-columns.alert.component";
|
||||
import ProductionListColumnProductionNote from "../production-list-columns/production-list-columns.productionnote.component";
|
||||
import ProductionSubletsManageComponent from "../production-sublets-manage/production-sublets-manage.component";
|
||||
|
||||
import dayjs from "../../utils/day";
|
||||
|
||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||
import JobPartsQueueCount from "../job-parts-queue-count/job-parts-queue-count.component";
|
||||
|
||||
const cardColor = (ssbuckets, totalHrs) => {
|
||||
const bucket = ssbuckets.find((bucket) => bucket.gte <= totalHrs && (!bucket.lt || bucket.lt > totalHrs));
|
||||
return bucket && bucket.color ? bucket.color.rgb || bucket.color : { r: 255, g: 255, b: 255 };
|
||||
};
|
||||
|
||||
const getContrastYIQ = (bgColor) =>
|
||||
(bgColor.r * 299 + bgColor.g * 587 + bgColor.b * 114) / 1000 >= 128 ? "black" : "white";
|
||||
|
||||
const findEmployeeById = (employees, id) => employees.find((e) => e.id === id);
|
||||
|
||||
const EllipsesToolTip = React.memo(({ title, children, kiosk }) => {
|
||||
if (kiosk || !title) {
|
||||
return <div className="ellipses no-select">{children}</div>;
|
||||
}
|
||||
return (
|
||||
<Tooltip title={title}>
|
||||
<div className="ellipses">{children}</div>
|
||||
</Tooltip>
|
||||
);
|
||||
});
|
||||
|
||||
const OwnerNameToolTip = ({ metadata, cardSettings }) =>
|
||||
cardSettings?.ownr_nm && (
|
||||
<Col span={24}>
|
||||
<EllipsesToolTip
|
||||
title={metadata.ownr_ln || metadata.ownr_co_nm ? <OwnerNameDisplay ownerObject={metadata} /> : null}
|
||||
kiosk={cardSettings.kiosk}
|
||||
>
|
||||
{metadata.ownr_ln || metadata.ownr_co_nm ? (
|
||||
cardSettings.compact ? (
|
||||
`${metadata.ownr_ln || ""} ${metadata.ownr_co_nm || ""}`
|
||||
) : (
|
||||
<OwnerNameDisplay ownerObject={metadata} />
|
||||
)
|
||||
) : (
|
||||
<span> </span>
|
||||
)}
|
||||
</EllipsesToolTip>
|
||||
</Col>
|
||||
);
|
||||
|
||||
const ModelInfoToolTip = ({ metadata, cardSettings }) =>
|
||||
cardSettings?.model_info && (
|
||||
<Col span={24}>
|
||||
<EllipsesToolTip
|
||||
title={
|
||||
metadata.v_model_yr || metadata.v_make_desc || metadata.v_model_desc
|
||||
? `${metadata.v_model_yr || ""} ${metadata.v_make_desc || ""} ${metadata.v_model_desc || ""}`
|
||||
: null
|
||||
}
|
||||
kiosk={cardSettings.kiosk}
|
||||
>
|
||||
{metadata.v_model_yr || metadata.v_make_desc || metadata.v_model_desc ? (
|
||||
`${metadata.v_model_yr || ""} ${metadata.v_make_desc || ""} ${metadata.v_model_desc || ""}`
|
||||
) : (
|
||||
<span> </span>
|
||||
)}
|
||||
</EllipsesToolTip>
|
||||
</Col>
|
||||
);
|
||||
|
||||
const InsuranceCompanyToolTip = ({ metadata, cardSettings }) =>
|
||||
cardSettings?.ins_co_nm && (
|
||||
<Col span={cardSettings.compact ? 24 : 12}>
|
||||
<EllipsesToolTip title={metadata.ins_co_nm || null} kiosk={cardSettings.kiosk}>
|
||||
{metadata.ins_co_nm ? metadata.ins_co_nm : <span> </span>}
|
||||
</EllipsesToolTip>
|
||||
</Col>
|
||||
);
|
||||
|
||||
const ClaimNumberToolTip = ({ metadata, cardSettings }) =>
|
||||
cardSettings?.clm_no && (
|
||||
<Col span={cardSettings.compact ? 24 : 12}>
|
||||
<EllipsesToolTip title={metadata.clm_no || null} kiosk={cardSettings.kiosk}>
|
||||
{metadata.clm_no ? metadata.clm_no : <span> </span>}
|
||||
</EllipsesToolTip>
|
||||
</Col>
|
||||
);
|
||||
|
||||
const EmployeeAssignmentsToolTip = ({
|
||||
metadata,
|
||||
cardSettings,
|
||||
employee_body,
|
||||
employee_prep,
|
||||
employee_refinish,
|
||||
employee_csr
|
||||
}) =>
|
||||
cardSettings?.employeeassignments && (
|
||||
<Col span={24}>
|
||||
<Row>
|
||||
<Col span={cardSettings.compact ? 24 : 12}>
|
||||
<EllipsesToolTip
|
||||
title={
|
||||
employee_body || metadata.labhrs.aggregate.sum.mod_lb_hrs
|
||||
? `B: ${employee_body ? `${employee_body.first_name.substring(0, 3)} ${employee_body.last_name.charAt(0)}` : ""} ${metadata.labhrs.aggregate.sum.mod_lb_hrs || "?"}h`
|
||||
: null
|
||||
}
|
||||
kiosk={cardSettings.kiosk}
|
||||
>
|
||||
{employee_body || metadata.labhrs.aggregate.sum.mod_lb_hrs ? (
|
||||
`B: ${employee_body ? `${employee_body.first_name.substring(0, 3)} ${employee_body.last_name.charAt(0)}` : ""} ${metadata.labhrs.aggregate.sum.mod_lb_hrs || "?"}h`
|
||||
) : (
|
||||
<span> </span>
|
||||
)}
|
||||
</EllipsesToolTip>
|
||||
</Col>
|
||||
<Col span={cardSettings.compact ? 24 : 12}>
|
||||
<EllipsesToolTip
|
||||
title={
|
||||
employee_prep
|
||||
? `P: ${employee_prep ? `${employee_prep.first_name.substring(0, 3)} ${employee_prep.last_name.charAt(0)}` : ""}`
|
||||
: null
|
||||
}
|
||||
kiosk={cardSettings.kiosk}
|
||||
>
|
||||
{employee_prep ? (
|
||||
`P: ${employee_prep ? `${employee_prep.first_name.substring(0, 3)} ${employee_prep.last_name.charAt(0)}` : ""}`
|
||||
) : (
|
||||
<span> </span>
|
||||
)}
|
||||
</EllipsesToolTip>
|
||||
</Col>
|
||||
<Col span={cardSettings.compact ? 24 : 12}>
|
||||
<EllipsesToolTip
|
||||
title={
|
||||
employee_refinish || metadata.larhrs.aggregate.sum.mod_lb_hrs
|
||||
? `R: ${employee_refinish ? `${employee_refinish.first_name.substring(0, 3)} ${employee_refinish.last_name.charAt(0)}` : ""} ${metadata.larhrs.aggregate.sum.mod_lb_hrs || "?"}h`
|
||||
: null
|
||||
}
|
||||
kiosk={cardSettings.kiosk}
|
||||
>
|
||||
{employee_refinish || metadata.larhrs.aggregate.sum.mod_lb_hrs ? (
|
||||
`R: ${employee_refinish ? `${employee_refinish.first_name.substring(0, 3)} ${employee_refinish.last_name.charAt(0)}` : ""} ${metadata.larhrs.aggregate.sum.mod_lb_hrs || "?"}h`
|
||||
) : (
|
||||
<span> </span>
|
||||
)}
|
||||
</EllipsesToolTip>
|
||||
</Col>
|
||||
<Col span={cardSettings.compact ? 24 : 12}>
|
||||
<EllipsesToolTip
|
||||
title={
|
||||
employee_csr ? `C: ${employee_csr ? `${employee_csr.first_name} ${employee_csr.last_name}` : ""}` : null
|
||||
}
|
||||
kiosk={cardSettings.kiosk}
|
||||
>
|
||||
{employee_csr ? (
|
||||
`C: ${employee_csr ? `${employee_csr.first_name} ${employee_csr.last_name}` : ""}`
|
||||
) : (
|
||||
<span> </span>
|
||||
)}
|
||||
</EllipsesToolTip>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
);
|
||||
|
||||
const ActualInToolTip = ({ metadata, cardSettings }) =>
|
||||
cardSettings?.actual_in && (
|
||||
<Col span={cardSettings.compact ? 24 : 12}>
|
||||
<EllipsesToolTip title={metadata.actual_in || null} kiosk={cardSettings.kiosk}>
|
||||
{metadata.actual_in ? (
|
||||
<Space>
|
||||
<DownloadOutlined />
|
||||
<DateTimeFormatter format="MM/DD">{metadata.actual_in}</DateTimeFormatter>
|
||||
</Space>
|
||||
) : (
|
||||
<span> </span>
|
||||
)}
|
||||
</EllipsesToolTip>
|
||||
</Col>
|
||||
);
|
||||
|
||||
const EstimatorToolTip = ({ metadata, cardSettings }) => {
|
||||
return (
|
||||
cardSettings?.estimator && (
|
||||
<Col span={cardSettings.compact ? 24 : 12}>
|
||||
<EllipsesToolTip
|
||||
title={metadata.est_ct_fn && metadata.est_ct_ln ? `${metadata.est_ct_fn} ${metadata.est_ct_ln}` : null}
|
||||
kiosk={cardSettings.kiosk}
|
||||
>
|
||||
{metadata.est_ct_fn && metadata.est_ct_ln ? (
|
||||
<span>E: {`${metadata.est_ct_fn} ${metadata.est_ct_ln}`}</span>
|
||||
) : (
|
||||
<span> </span>
|
||||
)}
|
||||
</EllipsesToolTip>
|
||||
</Col>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const SubtotalTooltip = ({ metadata, cardSettings, t }) => {
|
||||
const amount = metadata?.job_totals?.totals?.subtotal?.amount;
|
||||
const dineroAmount = amount ? Dinero({ amount: parseInt(amount * 100) }).toFormat("0,0.00") : null;
|
||||
|
||||
return (
|
||||
cardSettings?.subtotal && (
|
||||
<Col span={cardSettings.compact ? 24 : 12}>
|
||||
<EllipsesToolTip
|
||||
title={!!amount ? `${t("production.statistics.currency_symbol")}${dineroAmount}` : null}
|
||||
kiosk={cardSettings.kiosk}
|
||||
>
|
||||
{!!amount ? (
|
||||
<span>{`${t("production.statistics.currency_symbol")}${dineroAmount}`}</span>
|
||||
) : (
|
||||
<span> </span>
|
||||
)}
|
||||
</EllipsesToolTip>
|
||||
</Col>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const ScheduledCompletionToolTip = ({ metadata, cardSettings, pastDueAlert }) =>
|
||||
cardSettings?.scheduled_completion && (
|
||||
<Col span={cardSettings.compact ? 24 : 12}>
|
||||
<EllipsesToolTip title={metadata.scheduled_completion || null} kiosk={cardSettings.kiosk}>
|
||||
{metadata.scheduled_completion ? (
|
||||
<Space className={pastDueAlert}>
|
||||
<CalendarOutlined />
|
||||
<DateTimeFormatter format="MM/DD">{metadata.scheduled_completion}</DateTimeFormatter>
|
||||
</Space>
|
||||
) : (
|
||||
<span> </span>
|
||||
)}
|
||||
</EllipsesToolTip>
|
||||
</Col>
|
||||
);
|
||||
|
||||
const AltTransportToolTip = ({ metadata, cardSettings }) =>
|
||||
cardSettings?.ats && (
|
||||
<Col span={12}>
|
||||
<EllipsesToolTip title={metadata.alt_transport || null} kiosk={cardSettings.kiosk}>
|
||||
{metadata.alt_transport ? metadata.alt_transport : <span> </span>}
|
||||
</EllipsesToolTip>
|
||||
</Col>
|
||||
);
|
||||
|
||||
const SubletsComponent = ({ metadata, cardSettings }) =>
|
||||
cardSettings?.sublets && (
|
||||
<Col span={12}>
|
||||
{metadata.subletLines ? (
|
||||
<ProductionSubletsManageComponent subletJobLines={metadata.subletLines} />
|
||||
) : (
|
||||
<span> </span>
|
||||
)}
|
||||
</Col>
|
||||
);
|
||||
|
||||
const ProductionNoteComponent = ({ metadata, cardSettings, card }) =>
|
||||
cardSettings?.production_note && (
|
||||
<Col span={24} style={{ margin: "2px 0" }}>
|
||||
<ProductionListColumnProductionNote
|
||||
record={{
|
||||
production_vars: metadata?.production_vars,
|
||||
id: card?.id,
|
||||
refetch: card?.refetch
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
);
|
||||
|
||||
const PartsStatusComponent = ({ metadata, cardSettings }) =>
|
||||
cardSettings?.partsstatus && (
|
||||
<Col span={24} style={{ textAlign: "center" }}>
|
||||
{metadata.joblines_status ? <JobPartsQueueCount parts={metadata.joblines_status} /> : <span> </span>}
|
||||
</Col>
|
||||
);
|
||||
|
||||
const TasksToolTip = ({ metadata, cardSettings, t }) =>
|
||||
cardSettings?.tasks && (
|
||||
<Col span={12}>
|
||||
<EllipsesToolTip
|
||||
title={`${t("production.labels.tasks")}: ${metadata.tasks_aggregate?.aggregate?.count || 0}`}
|
||||
kiosk={cardSettings.kiosk}
|
||||
>
|
||||
{metadata.tasks_aggregate?.aggregate?.count ? (
|
||||
`T: ${metadata.tasks_aggregate.aggregate.count}`
|
||||
) : (
|
||||
<span>T: 0</span>
|
||||
)}
|
||||
</EllipsesToolTip>
|
||||
</Col>
|
||||
);
|
||||
|
||||
export default function ProductionBoardCard({ technician, card, bodyshop, cardSettings, clone }) {
|
||||
const { t } = useTranslation();
|
||||
const { metadata } = card;
|
||||
|
||||
const employees = useMemo(() => bodyshop.employees, [bodyshop.employees]);
|
||||
|
||||
const { employee_body, employee_prep, employee_refinish, employee_csr } = useMemo(() => {
|
||||
return {
|
||||
employee_body: metadata?.employee_body && findEmployeeById(employees, metadata.employee_body),
|
||||
employee_prep: metadata?.employee_prep && findEmployeeById(employees, metadata.employee_prep),
|
||||
employee_refinish: metadata?.employee_refinish && findEmployeeById(employees, metadata.employee_refinish),
|
||||
employee_csr: metadata?.employee_csr && findEmployeeById(employees, metadata.employee_csr)
|
||||
};
|
||||
}, [metadata, employees]);
|
||||
|
||||
const pastDueAlert = useMemo(() => {
|
||||
if (!metadata?.scheduled_completion) return null;
|
||||
const completionDate = dayjs(metadata.scheduled_completion);
|
||||
if (dayjs().isSameOrAfter(completionDate, "day")) return "production-completion-past";
|
||||
if (dayjs().add(1, "day").isSame(completionDate, "day")) return "production-completion-soon";
|
||||
return null;
|
||||
}, [metadata?.scheduled_completion]);
|
||||
|
||||
const totalHrs = useMemo(() => {
|
||||
return metadata?.labhrs && metadata?.larhrs
|
||||
? metadata.labhrs.aggregate.sum.mod_lb_hrs + metadata.larhrs.aggregate.sum.mod_lb_hrs
|
||||
: 0;
|
||||
}, [metadata?.labhrs, metadata?.larhrs]);
|
||||
|
||||
const bgColor = useMemo(() => cardColor(bodyshop.ssbuckets, totalHrs), [bodyshop.ssbuckets, totalHrs]);
|
||||
const contrastYIQ = useMemo(() => getContrastYIQ(bgColor), [bgColor]);
|
||||
|
||||
const isBodyEmpty = useMemo(() => {
|
||||
return !(
|
||||
cardSettings?.ownr_nm ||
|
||||
cardSettings?.model_info ||
|
||||
cardSettings?.ins_co_nm ||
|
||||
cardSettings?.clm_no ||
|
||||
cardSettings?.employeeassignments ||
|
||||
cardSettings?.actual_in ||
|
||||
cardSettings?.scheduled_completion ||
|
||||
cardSettings?.ats ||
|
||||
cardSettings?.sublets ||
|
||||
cardSettings?.production_note ||
|
||||
cardSettings?.partsstatus ||
|
||||
cardSettings?.estimator ||
|
||||
cardSettings?.subtotal ||
|
||||
cardSettings?.tasks
|
||||
);
|
||||
}, [cardSettings]);
|
||||
|
||||
const headerContent = (
|
||||
<div className="header-content-container">
|
||||
<div className="inner-container">
|
||||
<ProductionAlert id={card.id} productionVars={metadata?.production_vars} refetch={card?.refetch} key="alert" />
|
||||
{metadata?.suspended && <PauseCircleOutlined className="circle-outline" key="suspended" />}
|
||||
{metadata?.iouparent && (
|
||||
<EllipsesToolTip
|
||||
title={t("jobs.labels.iou")}
|
||||
key="iouparent"
|
||||
className="iouparent"
|
||||
kiosk={cardSettings.kiosk}
|
||||
>
|
||||
<BranchesOutlined className="branches-outlined" />
|
||||
</EllipsesToolTip>
|
||||
)}
|
||||
</div>
|
||||
<span className="tech-container">
|
||||
<Link to={technician ? `/tech/joblookup?selected=${card.id}` : `/manage/jobs/${card.id}`}>
|
||||
{metadata?.ro_number || t("general.labels.na")}
|
||||
</Link>
|
||||
</span>
|
||||
{isBodyEmpty && (
|
||||
<div className="body-empty-container">
|
||||
<Link to={{ search: `?selected=${card.id}` }}>
|
||||
<EyeFilled />
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
const bodyContent = (
|
||||
<Row>
|
||||
<OwnerNameToolTip metadata={metadata} cardSettings={cardSettings} />
|
||||
<ModelInfoToolTip metadata={metadata} cardSettings={cardSettings} />
|
||||
<InsuranceCompanyToolTip metadata={metadata} cardSettings={cardSettings} />
|
||||
<ClaimNumberToolTip metadata={metadata} cardSettings={cardSettings} />
|
||||
<EmployeeAssignmentsToolTip
|
||||
metadata={metadata}
|
||||
cardSettings={cardSettings}
|
||||
employee_body={employee_body}
|
||||
employee_prep={employee_prep}
|
||||
employee_refinish={employee_refinish}
|
||||
employee_csr={employee_csr}
|
||||
/>
|
||||
<EstimatorToolTip metadata={metadata} cardSettings={cardSettings} />
|
||||
<TasksToolTip metadata={metadata} cardSettings={cardSettings} t={t} />
|
||||
<SubtotalTooltip metadata={metadata} cardSettings={cardSettings} t={t} />
|
||||
<ActualInToolTip metadata={metadata} cardSettings={cardSettings} />
|
||||
<ScheduledCompletionToolTip metadata={metadata} cardSettings={cardSettings} pastDueAlert={pastDueAlert} />
|
||||
<AltTransportToolTip metadata={metadata} cardSettings={cardSettings} />
|
||||
<SubletsComponent metadata={metadata} cardSettings={cardSettings} />
|
||||
<ProductionNoteComponent metadata={metadata} cardSettings={cardSettings} card={card} />
|
||||
<PartsStatusComponent metadata={metadata} cardSettings={cardSettings} />
|
||||
</Row>
|
||||
);
|
||||
|
||||
return (
|
||||
<Card
|
||||
className={`react-trello-card ${cardSettings.kiosk ? "kiosk-mode" : ""}`}
|
||||
size="small"
|
||||
style={{
|
||||
backgroundColor: cardSettings?.cardcolor && `rgba(${bgColor.r},${bgColor.g},${bgColor.b},${bgColor.a})`,
|
||||
color: cardSettings?.cardcolor && contrastYIQ
|
||||
}}
|
||||
title={!isBodyEmpty ? headerContent : null}
|
||||
extra={
|
||||
!isBodyEmpty && (
|
||||
<Link to={{ search: `?selected=${card.id}` }}>
|
||||
<EyeFilled />
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
>
|
||||
{isBodyEmpty ? headerContent : bodyContent}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { Button, Card, Col, Form, notification, Popover, Row, Switch } from "antd";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { UPDATE_KANBAN_SETTINGS } from "../../graphql/user.queries";
|
||||
|
||||
export default function ProductionBoardKanbanCardSettings({ associationSettings }) {
|
||||
const [form] = Form.useForm();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [updateKbSettings] = useMutation(UPDATE_KANBAN_SETTINGS);
|
||||
|
||||
useEffect(() => {
|
||||
form.setFieldsValue(associationSettings && associationSettings.kanban_settings);
|
||||
}, [form, associationSettings, open]);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleFinish = async (values) => {
|
||||
setLoading(true);
|
||||
const result = await updateKbSettings({
|
||||
variables: {
|
||||
id: associationSettings && associationSettings.id,
|
||||
ks: values
|
||||
}
|
||||
});
|
||||
if (result.errors) {
|
||||
notification.open({
|
||||
type: "error",
|
||||
message: t("production.errors.settings", {
|
||||
error: JSON.stringify(result.errors)
|
||||
})
|
||||
});
|
||||
}
|
||||
setOpen(false);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const overlay = (
|
||||
<div>
|
||||
<Card>
|
||||
<Form form={form} onFinish={handleFinish} layout="vertical">
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={12}>
|
||||
<Form.Item label={t("production.labels.compact")} name="compact" valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item valuePropName="checked" label={t("production.labels.ownr_nm")} name="ownr_nm">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item valuePropName="checked" label={t("production.labels.clm_no")} name="clm_no">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item valuePropName="checked" label={t("production.labels.ins_co_nm")} name="ins_co_nm">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
{/* <Form.Item
|
||||
valuePropName="checked"
|
||||
label={t("production.labels.laborhrs")}
|
||||
name="laborhrs"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item> */}
|
||||
<Form.Item
|
||||
valuePropName="checked"
|
||||
label={t("production.labels.employeeassignments")}
|
||||
name="employeeassignments"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item valuePropName="checked" label={t("production.labels.actual_in")} name="actual_in">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item valuePropName="checked" label={t("production.labels.cardcolor")} name="cardcolor">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
valuePropName="checked"
|
||||
label={t("production.labels.scheduled_completion")}
|
||||
name="scheduled_completion"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item valuePropName="checked" label={t("production.labels.ats")} name="ats">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item valuePropName="checked" label={t("production.labels.production_note")} name="production_note">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
{/* <Form.Item
|
||||
valuePropName='checked' label={t("production.labels.alert")} name="alert">
|
||||
<Switch/>
|
||||
</Form.Item> */}
|
||||
<Form.Item valuePropName="checked" label={t("production.labels.sublets")} name="sublets">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item valuePropName="checked" label={t("production.labels.partsstatus")} name="partsstatus">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item valuePropName="checked" label={t("production.labels.stickyheader")} name="stickyheader">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
<Button
|
||||
onClick={() => {
|
||||
form.submit();
|
||||
}}
|
||||
>
|
||||
{t("general.actions.save")}
|
||||
</Button>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<Popover content={overlay} open={open} placement="topRight">
|
||||
<Button loading={loading} onClick={() => setOpen(true)}>
|
||||
{t("production.labels.cardsettings")}
|
||||
</Button>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
@@ -1,265 +1,235 @@
|
||||
import { SyncOutlined } from "@ant-design/icons";
|
||||
import { useApolloClient } from "@apollo/client";
|
||||
import Board, { moveCard } from "@asseinfo/react-kanban";
|
||||
import { Button, Grid, notification, Space, Statistic } from "antd";
|
||||
import Board from "./trello-board/index";
|
||||
import { Button, notification, Skeleton, Space } from "antd";
|
||||
import { PageHeader } from "@ant-design/pro-layout";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { Sticky, StickyContainer } from "react-sticky";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import styled from "styled-components";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { generate_UPDATE_JOB_KANBAN } from "../../graphql/jobs.queries";
|
||||
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||
import { selectTechnician } from "../../redux/tech/tech.selectors";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
import IndefiniteLoading from "../indefinite-loading/indefinite-loading.component";
|
||||
import ProductionBoardFilters from "../production-board-filters/production-board-filters.component";
|
||||
import ProductionBoardCard from "../production-board-kanban-card/production-board-kanban-card.component";
|
||||
import ProductionListDetailComponent from "../production-list-detail/production-list-detail.component";
|
||||
import ProductionBoardKanbanCardSettings from "./production-board-kanban.card-settings.component";
|
||||
//import "@asseinfo/react-kanban/dist/styles.css";
|
||||
import CardColorLegend from "../production-board-kanban-card/production-board-kanban-card-color-legend.component";
|
||||
import CardColorLegend from "./production-board-kanban-card-color-legend.component.jsx";
|
||||
import "./production-board-kanban.styles.scss";
|
||||
import { createBoardData } from "./production-board-kanban.utils.js";
|
||||
import ProductionBoardKanbanSettings from "./settings/production-board-kanban.settings.component.jsx";
|
||||
import cloneDeep from "lodash/cloneDeep";
|
||||
import isEqual from "lodash/isEqual";
|
||||
import { defaultFilters, mergeWithDefaults } from "./settings/defaultKanbanSettings.js";
|
||||
import NoteUpsertModal from "../../components/note-upsert-modal/note-upsert-modal.container";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
technician: selectTechnician
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type }))
|
||||
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||
dispatch(
|
||||
insertAuditTrail({
|
||||
jobid,
|
||||
operation,
|
||||
type
|
||||
})
|
||||
)
|
||||
});
|
||||
|
||||
export function ProductionBoardKanbanComponent({
|
||||
data,
|
||||
bodyshop,
|
||||
refetch,
|
||||
technician,
|
||||
insertAuditTrail,
|
||||
associationSettings
|
||||
}) {
|
||||
const [boardLanes, setBoardLanes] = useState({
|
||||
columns: [{ id: "Loading...", title: "Loading...", cards: [] }]
|
||||
});
|
||||
|
||||
const [filter, setFilter] = useState({ search: "", employeeId: null });
|
||||
|
||||
function ProductionBoardKanbanComponent({ data, bodyshop, refetch, insertAuditTrail, associationSettings, statuses }) {
|
||||
const [boardLanes, setBoardLanes] = useState({ lanes: [] });
|
||||
const [filter, setFilter] = useState(defaultFilters);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [isMoving, setIsMoving] = useState(false);
|
||||
const [orientation, setOrientation] = useState("vertical");
|
||||
|
||||
const { t } = useTranslation();
|
||||
useEffect(() => {
|
||||
const boardData = createBoardData(
|
||||
[...bodyshop.md_ro_statuses.production_statuses, ...(bodyshop.md_ro_statuses.additional_board_statuses || [])],
|
||||
data,
|
||||
filter
|
||||
);
|
||||
|
||||
boardData.columns = boardData.columns.map((d) => {
|
||||
return { ...d, title: `${d.title} (${d.cards.length})` };
|
||||
});
|
||||
setBoardLanes(boardData);
|
||||
setIsMoving(false);
|
||||
}, [data, setBoardLanes, setIsMoving, bodyshop.md_ro_statuses, filter]);
|
||||
|
||||
const client = useApolloClient();
|
||||
|
||||
const handleDragEnd = async (card, source, destination) => {
|
||||
logImEXEvent("kanban_drag_end");
|
||||
useEffect(() => {
|
||||
if (associationSettings) {
|
||||
setLoading(true);
|
||||
setOrientation(associationSettings?.kanban_settings?.orientation ? "vertical" : "horizontal");
|
||||
setLoading(false);
|
||||
}
|
||||
}, [associationSettings]);
|
||||
|
||||
useEffect(() => {
|
||||
setIsMoving(true);
|
||||
setBoardLanes(moveCard(boardLanes, source, destination));
|
||||
const sameColumnTransfer = source.fromColumnId === destination.toColumnId;
|
||||
const sourceColumn = boardLanes.columns.find((x) => x.id === source.fromColumnId);
|
||||
const destinationColumn = boardLanes.columns.find((x) => x.id === destination.toColumnId);
|
||||
|
||||
const movedCardWillBeFirst = destination.toPosition === 0;
|
||||
|
||||
const movedCardWillBeLast = destinationColumn.cards.length - destination.toPosition < 1;
|
||||
|
||||
const lastCardInDestinationColumn = destinationColumn.cards[destinationColumn.cards.length - 1];
|
||||
|
||||
const oldChildCard = sourceColumn.cards[source.fromPosition + 1];
|
||||
|
||||
const newChildCard = movedCardWillBeLast
|
||||
? null
|
||||
: destinationColumn.cards[
|
||||
sameColumnTransfer
|
||||
? source.fromPosition - destination.toPosition > 0
|
||||
? destination.toPosition
|
||||
: destination.toPosition + 1
|
||||
: destination.toPosition
|
||||
];
|
||||
|
||||
const oldChildCardNewParent = oldChildCard ? card.kanbanparent : null;
|
||||
|
||||
let movedCardNewKanbanParent;
|
||||
if (movedCardWillBeFirst) {
|
||||
//console.log("==> New Card is first.");
|
||||
movedCardNewKanbanParent = "-1";
|
||||
} else if (movedCardWillBeLast) {
|
||||
// console.log("==> New Card is last.");
|
||||
movedCardNewKanbanParent = lastCardInDestinationColumn.id;
|
||||
} else if (!!newChildCard) {
|
||||
// console.log("==> New Card is somewhere in the middle");
|
||||
movedCardNewKanbanParent = newChildCard.kanbanparent;
|
||||
} else {
|
||||
console.log("==> !!!!!!Couldn't find a parent.!!!! <==");
|
||||
}
|
||||
const newChildCardNewParent = newChildCard ? card.id : null;
|
||||
const update = await client.mutate({
|
||||
mutation: generate_UPDATE_JOB_KANBAN(
|
||||
oldChildCard ? oldChildCard.id : null,
|
||||
oldChildCardNewParent,
|
||||
card.id,
|
||||
movedCardNewKanbanParent,
|
||||
destination.toColumnId,
|
||||
newChildCard ? newChildCard.id : null,
|
||||
newChildCardNewParent
|
||||
)
|
||||
});
|
||||
insertAuditTrail({
|
||||
jobid: card.id,
|
||||
operation: AuditTrailMapping.jobstatuschange(destination.toColumnId),
|
||||
type: "jobstatuschange"
|
||||
const newBoardData = createBoardData({
|
||||
statuses,
|
||||
data,
|
||||
filter,
|
||||
cardSettings: associationSettings?.kanban_settings
|
||||
});
|
||||
|
||||
if (update.errors) {
|
||||
notification["error"]({
|
||||
message: t("production.errors.boardupdate", {
|
||||
message: JSON.stringify(update.errors)
|
||||
})
|
||||
});
|
||||
newBoardData.lanes = newBoardData.lanes.map((lane) => ({
|
||||
...lane,
|
||||
title: `${lane.title} (${lane.cards.length})`
|
||||
}));
|
||||
|
||||
setBoardLanes((prevBoardLanes) => {
|
||||
const deepClonedData = cloneDeep(newBoardData);
|
||||
if (!isEqual(prevBoardLanes, deepClonedData)) {
|
||||
return deepClonedData;
|
||||
}
|
||||
return prevBoardLanes;
|
||||
});
|
||||
setIsMoving(false);
|
||||
}, [data, bodyshop.md_ro_statuses, filter, statuses, associationSettings?.kanban_settings]);
|
||||
|
||||
const getCardByID = useCallback((data, cardId) => {
|
||||
for (const lane of data.lanes) {
|
||||
for (const card of lane.cards) {
|
||||
if (card.id === cardId) {
|
||||
return card;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}, []);
|
||||
|
||||
const onDragEnd = useCallback(
|
||||
async ({ type, source, destination, draggableId }) => {
|
||||
logImEXEvent("kanban_drag_end");
|
||||
|
||||
if (!type || type !== "lane" || !source || !destination || isMoving) return;
|
||||
|
||||
setIsMoving(true);
|
||||
|
||||
const targetLane = boardLanes.lanes.find((lane) => lane.id === destination.droppableId);
|
||||
const sourceLane = boardLanes.lanes.find((lane) => lane.id === source.droppableId);
|
||||
|
||||
if (!targetLane || !sourceLane) {
|
||||
setIsMoving(false);
|
||||
console.error("Invalid source or destination lane");
|
||||
return;
|
||||
}
|
||||
|
||||
const sameColumnTransfer = source.droppableId === destination.droppableId;
|
||||
const sourceCard = getCardByID(boardLanes, draggableId);
|
||||
|
||||
const movedCardWillBeFirst = destination.index === 0;
|
||||
const movedCardWillBeLast = destination.index >= targetLane.cards.length - 1;
|
||||
|
||||
const lastCardInTargetLane = targetLane.cards[targetLane.cards.length - 1];
|
||||
const oldChildCard = sourceLane.cards[source.index + 1];
|
||||
|
||||
const newChildCard = movedCardWillBeLast
|
||||
? null
|
||||
: targetLane.cards[
|
||||
sameColumnTransfer
|
||||
? source.index < destination.index
|
||||
? destination.index + 1
|
||||
: destination.index
|
||||
: destination.index
|
||||
];
|
||||
|
||||
const oldChildCardNewParent = oldChildCard ? sourceCard.metadata.kanbanparent : null;
|
||||
|
||||
let movedCardNewKanbanParent;
|
||||
if (movedCardWillBeFirst) {
|
||||
movedCardNewKanbanParent = "-1";
|
||||
} else if (movedCardWillBeLast) {
|
||||
movedCardNewKanbanParent = lastCardInTargetLane.id;
|
||||
} else if (newChildCard) {
|
||||
movedCardNewKanbanParent = newChildCard.metadata.kanbanparent;
|
||||
} else {
|
||||
console.error("==> !!!!!!Couldn't find a parent.!!!! <==");
|
||||
}
|
||||
|
||||
const newChildCardNewParent = newChildCard ? draggableId : null;
|
||||
|
||||
try {
|
||||
const update = await client.mutate({
|
||||
mutation: generate_UPDATE_JOB_KANBAN(
|
||||
oldChildCard ? oldChildCard.id : null,
|
||||
oldChildCardNewParent,
|
||||
draggableId,
|
||||
movedCardNewKanbanParent,
|
||||
targetLane.id,
|
||||
newChildCard ? newChildCard.id : null,
|
||||
newChildCardNewParent
|
||||
)
|
||||
});
|
||||
|
||||
insertAuditTrail({
|
||||
jobid: draggableId,
|
||||
operation: AuditTrailMapping.jobstatuschange(targetLane.id),
|
||||
type: "jobstatuschange"
|
||||
});
|
||||
|
||||
if (update.errors) {
|
||||
notification["error"]({
|
||||
message: t("production.errors.boardupdate", {
|
||||
message: JSON.stringify(update.errors)
|
||||
})
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
notification["error"]({
|
||||
message: t("production.errors.boardupdate", {
|
||||
message: error.message
|
||||
})
|
||||
});
|
||||
} finally {
|
||||
setIsMoving(false);
|
||||
}
|
||||
},
|
||||
[boardLanes, client, getCardByID, isMoving, t, insertAuditTrail]
|
||||
);
|
||||
|
||||
const cardSettings = useMemo(() => {
|
||||
const kanbanSettings = associationSettings?.kanban_settings;
|
||||
return mergeWithDefaults(kanbanSettings);
|
||||
}, [associationSettings]);
|
||||
|
||||
const handleSettingsChange = () => {
|
||||
setFilter(defaultFilters);
|
||||
};
|
||||
|
||||
const totalHrs = data
|
||||
.reduce(
|
||||
(acc, val) => acc + (val.labhrs?.aggregate?.sum?.mod_lb_hrs || 0) + (val.larhrs?.aggregate?.sum?.mod_lb_hrs || 0),
|
||||
0
|
||||
)
|
||||
.toFixed(1);
|
||||
const totalLAB = data.reduce((acc, val) => acc + (val.labhrs?.aggregate?.sum?.mod_lb_hrs || 0), 0).toFixed(1);
|
||||
const totalLAR = data.reduce((acc, val) => acc + (val.larhrs?.aggregate?.sum?.mod_lb_hrs || 0), 0).toFixed(1);
|
||||
const selectedBreakpoint = Object.entries(Grid.useBreakpoint())
|
||||
.filter((screen) => !!screen[1])
|
||||
.slice(-1)[0];
|
||||
|
||||
const standardSizes = {
|
||||
xs: "250",
|
||||
sm: "250",
|
||||
md: "250",
|
||||
lg: "250",
|
||||
xl: "250",
|
||||
xxl: "250"
|
||||
};
|
||||
const compactSizes = {
|
||||
xs: "150",
|
||||
sm: "150",
|
||||
md: "150",
|
||||
lg: "150",
|
||||
xl: "155",
|
||||
xxl: "155"
|
||||
};
|
||||
|
||||
const width = selectedBreakpoint
|
||||
? associationSettings && associationSettings.kanban_settings && associationSettings.kanban_settings.compact
|
||||
? compactSizes[selectedBreakpoint[0]]
|
||||
: standardSizes[selectedBreakpoint[0]]
|
||||
: "250";
|
||||
|
||||
const stickyHeader = {
|
||||
renderColumnHeader: ({ title }) => (
|
||||
<Sticky>
|
||||
{({
|
||||
style,
|
||||
|
||||
// the following are also available but unused in this example
|
||||
isSticky,
|
||||
wasSticky,
|
||||
distanceFromTop,
|
||||
distanceFromBottom,
|
||||
calculatedHeight
|
||||
}) => (
|
||||
<div className="react-kanban-column-header" style={{ ...style, zIndex: "99", backgroundColor: "#ddd" }}>
|
||||
{title}
|
||||
</div>
|
||||
)}
|
||||
</Sticky>
|
||||
)
|
||||
};
|
||||
|
||||
const cardSettings =
|
||||
associationSettings &&
|
||||
associationSettings.kanban_settings &&
|
||||
Object.keys(associationSettings.kanban_settings).length > 0
|
||||
? associationSettings.kanban_settings
|
||||
: {
|
||||
ats: true,
|
||||
clm_no: true,
|
||||
compact: false,
|
||||
ownr_nm: true,
|
||||
sublets: true,
|
||||
ins_co_nm: true,
|
||||
production_note: true,
|
||||
employeeassignments: true,
|
||||
scheduled_completion: true,
|
||||
stickyheader: false,
|
||||
cardcolor: false
|
||||
};
|
||||
if (loading) {
|
||||
return <Skeleton active />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Container width={width}>
|
||||
<div>
|
||||
<IndefiniteLoading loading={isMoving} />
|
||||
|
||||
<PageHeader
|
||||
title={
|
||||
<Space>
|
||||
<Statistic title={t("dashboard.titles.productionhours")} value={totalHrs} />
|
||||
<Statistic title={t("dashboard.titles.labhours")} value={totalLAB} />
|
||||
<Statistic title={t("dashboard.titles.larhours")} value={totalLAR} />
|
||||
<Statistic title={t("appointments.labels.inproduction")} value={data && data.length} />
|
||||
</Space>
|
||||
}
|
||||
title={cardSettings.cardcolor && <CardColorLegend cardSettings={cardSettings} bodyshop={bodyshop} />}
|
||||
style={{ paddingInline: 0, paddingBlock: 0 }}
|
||||
extra={
|
||||
<Space wrap>
|
||||
<Button onClick={() => refetch && refetch()}>
|
||||
<SyncOutlined />
|
||||
</Button>
|
||||
<ProductionBoardFilters filter={filter} setFilter={setFilter} loading={isMoving} />
|
||||
<ProductionBoardKanbanCardSettings associationSettings={associationSettings} />
|
||||
<ProductionBoardKanbanSettings
|
||||
parentLoading={setLoading}
|
||||
associationSettings={associationSettings}
|
||||
onSettingsChange={handleSettingsChange}
|
||||
bodyshop={bodyshop}
|
||||
data={data}
|
||||
/>
|
||||
</Space>
|
||||
}
|
||||
/>
|
||||
|
||||
{cardSettings.cardcolor && <CardColorLegend cardSettings={cardSettings} bodyshop={bodyshop} />}
|
||||
|
||||
<NoteUpsertModal />
|
||||
<ProductionListDetailComponent jobs={data} />
|
||||
<StickyContainer>
|
||||
<Board
|
||||
style={{ height: "100%" }}
|
||||
children={boardLanes}
|
||||
disableCardDrag={isMoving}
|
||||
{...(cardSettings.stickyheader && stickyHeader)}
|
||||
renderCard={(card) => ProductionBoardCard(technician, card, bodyshop, cardSettings)}
|
||||
onCardDragEnd={handleDragEnd}
|
||||
/>
|
||||
</StickyContainer>
|
||||
</Container>
|
||||
|
||||
<Board
|
||||
queryData={data}
|
||||
data={boardLanes}
|
||||
onDragEnd={onDragEnd}
|
||||
orientation={orientation}
|
||||
cardSettings={cardSettings}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ProductionBoardKanbanComponent);
|
||||
|
||||
const Container = styled.div`
|
||||
.react-kanban-card-skeleton,
|
||||
.react-kanban-card,
|
||||
.react-kanban-card-adder-form {
|
||||
box-sizing: border-box;
|
||||
max-width: ${(props) => props.width}px;
|
||||
min-width: ${(props) => props.width}px;
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -1,14 +1,8 @@
|
||||
import { useApolloClient, useQuery, useSubscription } from "@apollo/client";
|
||||
import _ from "lodash";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React, { useEffect, useMemo } from "react";
|
||||
import { useQuery, useSubscription } from "@apollo/client";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import {
|
||||
QUERY_EXACT_JOB_IN_PRODUCTION,
|
||||
QUERY_EXACT_JOBS_IN_PRODUCTION,
|
||||
QUERY_JOBS_IN_PRODUCTION,
|
||||
SUBSCRIPTION_JOBS_IN_PRODUCTION
|
||||
} from "../../graphql/jobs.queries";
|
||||
import { QUERY_JOBS_IN_PRODUCTION, SUBSCRIPTION_JOBS_IN_PRODUCTION } from "../../graphql/jobs.queries";
|
||||
import { QUERY_KANBAN_SETTINGS } from "../../graphql/user.queries";
|
||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||
import ProductionBoardKanbanComponent from "./production-board-kanban.component";
|
||||
@@ -18,76 +12,53 @@ const mapStateToProps = createStructuredSelector({
|
||||
currentUser: selectCurrentUser
|
||||
});
|
||||
|
||||
export function ProductionBoardKanbanContainer({ bodyshop, currentUser }) {
|
||||
function ProductionBoardKanbanContainer({ bodyshop, currentUser }) {
|
||||
const combinedStatuses = useMemo(
|
||||
() => [
|
||||
...bodyshop.md_ro_statuses.production_statuses,
|
||||
...(bodyshop.md_ro_statuses.additional_board_statuses || [])
|
||||
],
|
||||
[bodyshop.md_ro_statuses.production_statuses, bodyshop.md_ro_statuses.additional_board_statuses]
|
||||
);
|
||||
|
||||
const { refetch, loading, data } = useQuery(QUERY_JOBS_IN_PRODUCTION, {
|
||||
pollInterval: 3600000,
|
||||
fetchPolicy: "network-only",
|
||||
nextFetchPolicy: "network-only"
|
||||
nextFetchPolicy: "network-only",
|
||||
onError: (error) => console.error(`Error fetching jobs in production: ${error.message}`)
|
||||
});
|
||||
const client = useApolloClient();
|
||||
const [joblist, setJoblist] = useState([]);
|
||||
const { data: updatedJobs } = useSubscription(SUBSCRIPTION_JOBS_IN_PRODUCTION);
|
||||
|
||||
useEffect(() => {
|
||||
if (!(data && data.jobs)) return;
|
||||
setJoblist(
|
||||
data.jobs.map((j) => {
|
||||
return { id: j.id, updated_at: j.updated_at };
|
||||
})
|
||||
);
|
||||
}, [data]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!updatedJobs || joblist.length === 0) return;
|
||||
|
||||
const jobDiff = _.differenceWith(
|
||||
joblist,
|
||||
updatedJobs.jobs,
|
||||
(a, b) => a.id === b.id && a.updated_at === b.updated_at
|
||||
);
|
||||
|
||||
jobDiff.forEach((job) => {
|
||||
getUpdatedJobData(job.id);
|
||||
});
|
||||
if (jobDiff.length > 1) {
|
||||
getUpdatedJobsData(jobDiff.map((j) => j.id));
|
||||
} else if (jobDiff.length === 1) {
|
||||
jobDiff.forEach((job) => {
|
||||
getUpdatedJobData(job.id);
|
||||
});
|
||||
}
|
||||
|
||||
setJoblist(updatedJobs.jobs);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [updatedJobs]);
|
||||
|
||||
const getUpdatedJobData = async (jobId) => {
|
||||
client.query({
|
||||
query: QUERY_EXACT_JOB_IN_PRODUCTION,
|
||||
variables: { id: jobId }
|
||||
});
|
||||
};
|
||||
const getUpdatedJobsData = async (jobIds) => {
|
||||
client.query({
|
||||
query: QUERY_EXACT_JOBS_IN_PRODUCTION,
|
||||
variables: { ids: jobIds }
|
||||
});
|
||||
};
|
||||
const { data: updatedJobs } = useSubscription(SUBSCRIPTION_JOBS_IN_PRODUCTION, {
|
||||
onError: (error) => console.error(`Error subscribing to jobs in production: ${error.message}`)
|
||||
});
|
||||
|
||||
const { loading: associationSettingsLoading, data: associationSettings } = useQuery(QUERY_KANBAN_SETTINGS, {
|
||||
variables: { email: currentUser.email }
|
||||
variables: { email: currentUser.email },
|
||||
onError: (error) => console.error(`Error fetching Kanban settings: ${error.message}`)
|
||||
});
|
||||
|
||||
// const currentReducerData = useSelector((state) => (state.trello.lanes ? state.trello : {}));
|
||||
|
||||
useEffect(() => {
|
||||
if (updatedJobs && data) {
|
||||
refetch().catch((err) => console.error(`Error re-fetching jobs in production: ${err.message}`));
|
||||
}
|
||||
}, [updatedJobs, data, refetch]);
|
||||
|
||||
const filteredAssociationSettings = useMemo(() => {
|
||||
return associationSettings?.associations[0] || null;
|
||||
}, [associationSettings]);
|
||||
|
||||
return (
|
||||
<ProductionBoardKanbanComponent
|
||||
loading={loading || associationSettingsLoading}
|
||||
data={data ? data.jobs : []}
|
||||
refetch={refetch}
|
||||
associationSettings={
|
||||
associationSettings && associationSettings.associations[0] ? associationSettings.associations[0] : null
|
||||
}
|
||||
associationSettings={filteredAssociationSettings}
|
||||
bodyshop={bodyshop}
|
||||
statuses={combinedStatuses}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, null)(ProductionBoardKanbanContainer);
|
||||
export default connect(mapStateToProps)(ProductionBoardKanbanContainer);
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
import React, { useMemo } from "react";
|
||||
import { Card, Statistic } from "antd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import PropTypes from "prop-types";
|
||||
import { defaultKanbanSettings, statisticsItems } from "./settings/defaultKanbanSettings.js";
|
||||
|
||||
export const StatisticType = {
|
||||
HOURS: "hours",
|
||||
AMOUNT: "amount",
|
||||
JOBS: "jobs",
|
||||
TASKS: "tasks"
|
||||
};
|
||||
|
||||
const mergeStatistics = (items, values) => {
|
||||
const valuesMap = values.reduce((acc, value) => {
|
||||
acc[value.id] = value;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
return items.map((item) => ({
|
||||
...item,
|
||||
value: valuesMap[item.id]?.value,
|
||||
type: valuesMap[item.id]?.type
|
||||
}));
|
||||
};
|
||||
|
||||
const ProductionStatistics = ({ data, cardSettings, reducerData }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const calculateTotal = (items, key, subKey) => {
|
||||
return items.reduce((acc, item) => acc + (item[key]?.aggregate?.sum?.[subKey] || 0), 0);
|
||||
};
|
||||
|
||||
const calculateTotalAmount = (items, key) => {
|
||||
return items.reduce((acc, item) => acc + (item[key]?.totals?.subtotal?.amount || 0), 0);
|
||||
};
|
||||
|
||||
const calculateReducerTotal = (lanes, key, subKey) => {
|
||||
return lanes.reduce((acc, lane) => {
|
||||
return (
|
||||
acc + lane.cards.reduce((laneAcc, card) => laneAcc + (card.metadata[key]?.aggregate?.sum?.[subKey] || 0), 0)
|
||||
);
|
||||
}, 0);
|
||||
};
|
||||
|
||||
const calculateReducerTotalAmount = (lanes, key) => {
|
||||
return lanes.reduce((acc, lane) => {
|
||||
return (
|
||||
acc + lane.cards.reduce((laneAcc, card) => laneAcc + (card.metadata[key]?.totals?.subtotal?.amount || 0), 0)
|
||||
);
|
||||
}, 0);
|
||||
};
|
||||
|
||||
const formatValue = (value, type) => {
|
||||
if (type === StatisticType.JOBS) {
|
||||
return value.toFixed(0);
|
||||
}
|
||||
if (type === StatisticType.HOURS) {
|
||||
return value.toFixed(2);
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
const totalHrs = useMemo(() => {
|
||||
if (!cardSettings.totalHrs) return null;
|
||||
const total = calculateTotal(data, "labhrs", "mod_lb_hrs") + calculateTotal(data, "larhrs", "mod_lb_hrs");
|
||||
return parseFloat(total.toFixed(2));
|
||||
}, [data, cardSettings.totalHrs]);
|
||||
|
||||
const totalLAB = useMemo(() => {
|
||||
if (!cardSettings.totalLAB) return null;
|
||||
const total = calculateTotal(data, "labhrs", "mod_lb_hrs");
|
||||
return parseFloat(total.toFixed(2));
|
||||
}, [data, cardSettings.totalLAB]);
|
||||
|
||||
const totalLAR = useMemo(() => {
|
||||
if (!cardSettings.totalLAR) return null;
|
||||
const total = calculateTotal(data, "larhrs", "mod_lb_hrs");
|
||||
return parseFloat(total.toFixed(2));
|
||||
}, [data, cardSettings.totalLAR]);
|
||||
|
||||
const jobsInProduction = useMemo(
|
||||
() => (cardSettings.jobsInProduction ? data.length : null),
|
||||
[data, cardSettings.jobsInProduction]
|
||||
);
|
||||
|
||||
const totalAmountInProduction = useMemo(() => {
|
||||
if (!cardSettings.totalAmountInProduction) return null;
|
||||
const total = calculateTotalAmount(data, "job_totals");
|
||||
return parseFloat(total.toFixed(2));
|
||||
}, [data, cardSettings.totalAmountInProduction]);
|
||||
|
||||
const totalHrsOnBoard = useMemo(() => {
|
||||
if (!reducerData || !cardSettings.totalHrsOnBoard) return null;
|
||||
const total =
|
||||
calculateReducerTotal(reducerData.lanes, "labhrs", "mod_lb_hrs") +
|
||||
calculateReducerTotal(reducerData.lanes, "larhrs", "mod_lb_hrs");
|
||||
return parseFloat(total.toFixed(2));
|
||||
}, [reducerData, cardSettings.totalHrsOnBoard]);
|
||||
|
||||
const totalLABOnBoard = useMemo(() => {
|
||||
if (!reducerData || !cardSettings.totalLABOnBoard) return null;
|
||||
const total = calculateReducerTotal(reducerData.lanes, "labhrs", "mod_lb_hrs");
|
||||
return parseFloat(total.toFixed(2));
|
||||
}, [reducerData, cardSettings.totalLABOnBoard]);
|
||||
|
||||
const totalLAROnBoard = useMemo(() => {
|
||||
if (!reducerData || !cardSettings.totalLAROnBoard) return null;
|
||||
const total = calculateReducerTotal(reducerData.lanes, "larhrs", "mod_lb_hrs");
|
||||
return parseFloat(total.toFixed(2));
|
||||
}, [reducerData, cardSettings.totalLAROnBoard]);
|
||||
|
||||
const jobsOnBoard = useMemo(
|
||||
() =>
|
||||
reducerData && cardSettings.jobsOnBoard
|
||||
? reducerData.lanes.reduce((acc, lane) => acc + lane.cards.length, 0)
|
||||
: null,
|
||||
[reducerData, cardSettings.jobsOnBoard]
|
||||
);
|
||||
|
||||
const totalAmountOnBoard = useMemo(() => {
|
||||
if (!reducerData || !cardSettings.totalAmountOnBoard) return null;
|
||||
const total = calculateReducerTotalAmount(reducerData.lanes, "job_totals");
|
||||
return parseFloat(total.toFixed(2));
|
||||
}, [reducerData, cardSettings.totalAmountOnBoard]);
|
||||
|
||||
const tasksInProduction = useMemo(() => {
|
||||
if (!data || !cardSettings.tasksInProduction) return null;
|
||||
return data.reduce((acc, item) => acc + (item.tasks_aggregate?.aggregate?.count || 0), 0);
|
||||
}, [data, cardSettings.tasksInProduction]);
|
||||
|
||||
const tasksOnBoard = useMemo(() => {
|
||||
if (!reducerData || !cardSettings.tasksOnBoard) return null;
|
||||
return reducerData.lanes.reduce((acc, lane) => {
|
||||
return (
|
||||
acc + lane.cards.reduce((laneAcc, card) => laneAcc + (card.metadata.tasks_aggregate?.aggregate?.count || 0), 0)
|
||||
);
|
||||
}, 0);
|
||||
}, [reducerData, cardSettings.tasksOnBoard]);
|
||||
|
||||
const statistics = useMemo(
|
||||
() =>
|
||||
mergeStatistics(statisticsItems, [
|
||||
{ id: 0, value: totalHrs, type: StatisticType.HOURS },
|
||||
{ id: 1, value: totalAmountInProduction, type: StatisticType.AMOUNT },
|
||||
{ id: 2, value: totalLAB, type: StatisticType.HOURS },
|
||||
{ id: 3, value: totalLAR, type: StatisticType.HOURS },
|
||||
{ id: 4, value: jobsInProduction, type: StatisticType.JOBS },
|
||||
{ id: 5, value: totalHrsOnBoard, type: StatisticType.HOURS },
|
||||
{ id: 6, value: totalAmountOnBoard, type: StatisticType.AMOUNT },
|
||||
{ id: 7, value: totalLABOnBoard, type: StatisticType.HOURS },
|
||||
{ id: 8, value: totalLAROnBoard, type: StatisticType.HOURS },
|
||||
{ id: 9, value: jobsOnBoard, type: StatisticType.JOBS },
|
||||
{ id: 10, value: tasksOnBoard, type: StatisticType.TASKS },
|
||||
{ id: 11, value: tasksInProduction, type: StatisticType.TASKS }
|
||||
]),
|
||||
[
|
||||
totalHrs,
|
||||
totalAmountInProduction,
|
||||
totalLAB,
|
||||
totalLAR,
|
||||
jobsInProduction,
|
||||
totalHrsOnBoard,
|
||||
totalAmountOnBoard,
|
||||
totalLABOnBoard,
|
||||
totalLAROnBoard,
|
||||
jobsOnBoard,
|
||||
tasksOnBoard,
|
||||
tasksInProduction
|
||||
]
|
||||
);
|
||||
|
||||
const sortedStatistics = useMemo(() => {
|
||||
const statisticsMap = new Map(statistics.map((stat) => [stat.id, stat]));
|
||||
|
||||
return (
|
||||
cardSettings?.statisticsOrder ? cardSettings.statisticsOrder : defaultKanbanSettings.statisticsOrder
|
||||
).reduce((sorted, orderId) => {
|
||||
const value = statisticsMap.get(orderId);
|
||||
if (value && value.value !== null) {
|
||||
sorted.push(value);
|
||||
}
|
||||
return sorted;
|
||||
}, []);
|
||||
}, [statistics, cardSettings.statisticsOrder]);
|
||||
|
||||
return (
|
||||
<div style={{ display: "flex", gap: "5px", flexWrap: "wrap", marginBottom: "5px" }}>
|
||||
{sortedStatistics.map((stat) => (
|
||||
<Card styles={{ body: { padding: "8px" } }} key={stat.id}>
|
||||
<Statistic
|
||||
title={t(`production.statistics.${stat.label}`)}
|
||||
value={formatValue(stat.value, stat.type)}
|
||||
prefix={stat.type === StatisticType.AMOUNT ? t("production.statistics.currency_symbol") : undefined}
|
||||
suffix={
|
||||
stat.type === StatisticType.HOURS
|
||||
? t("production.statistics.hours")
|
||||
: stat.type === StatisticType.JOBS
|
||||
? t("production.statistics.jobs")
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ProductionStatistics.propTypes = {
|
||||
data: PropTypes.array.isRequired,
|
||||
cardSettings: PropTypes.object.isRequired,
|
||||
reducerData: PropTypes.object
|
||||
};
|
||||
|
||||
export default ProductionStatistics;
|
||||
@@ -1,145 +1,72 @@
|
||||
.react-kanban-board {
|
||||
.react-trello-board {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.react-kanban-card {
|
||||
border-radius: 3px;
|
||||
background-color: #fff;
|
||||
padding: 4px;
|
||||
margin-bottom: 7px;
|
||||
.height-preserving-container:empty {
|
||||
min-height: calc(var(--child-height));
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
// .react-kanban-card-skeleton,
|
||||
// .react-kanban-card,
|
||||
// .react-kanban-card-adder-form {
|
||||
// box-sizing: border-box;
|
||||
// max-width: 145px;
|
||||
// min-width: 145px;
|
||||
// }
|
||||
|
||||
.react-kanban-card--dragging {
|
||||
box-shadow: 2px 2px grey;
|
||||
.height-preserving-container {
|
||||
}
|
||||
|
||||
.react-kanban-card__description {
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.react-kanban-card__title {
|
||||
border-bottom: 1px solid #eee;
|
||||
padding-bottom: 5px;
|
||||
.react-trello-column-header {
|
||||
font-weight: bold;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.react-kanban-column {
|
||||
padding: 10px;
|
||||
border-radius: 2px;
|
||||
background-color: #eee;
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.react-kanban-column input:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.react-kanban-card-adder-form {
|
||||
border-radius: 3px;
|
||||
background-color: #fff;
|
||||
padding: 10px;
|
||||
margin-bottom: 7px;
|
||||
}
|
||||
|
||||
.react-kanban-card-adder-form input {
|
||||
border: 0px;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
.react-kanban-card-adder-button {
|
||||
width: 100%;
|
||||
margin-top: 5px;
|
||||
background-color: transparent;
|
||||
cursor: pointer;
|
||||
border: 1px solid #ccc;
|
||||
transition: 0.3s;
|
||||
border-radius: 3px;
|
||||
font-size: 20px;
|
||||
margin-bottom: 10px;
|
||||
font-weight: bold;
|
||||
background-color: #d0d0d0;
|
||||
border-radius: 5px 5px 0 0;
|
||||
}
|
||||
|
||||
.react-kanban-card-adder-button:hover {
|
||||
background-color: #ccc;
|
||||
}
|
||||
|
||||
.react-kanban-card-adder-form__title {
|
||||
font-weight: bold;
|
||||
border-bottom: 1px solid #eee;
|
||||
padding-bottom: 5px;
|
||||
font-weight: bold;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.react-kanban-card-adder-form__title:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.react-kanban-card-adder-form__description {
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.react-kanban-card-adder-form__description:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.react-kanban-card-adder-form__button {
|
||||
background-color: #eee;
|
||||
.production-alert {
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 5px;
|
||||
width: 45%;
|
||||
margin-top: 5px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.react-trello-footer {
|
||||
background-color: #d0d0d0;
|
||||
border-radius: 0 0 5px 5px;
|
||||
}
|
||||
|
||||
.react-kanban-card-adder-form__button:hover {
|
||||
transition: 0.3s;
|
||||
cursor: pointer;
|
||||
background-color: #ccc;
|
||||
.grid-item {
|
||||
margin: 1px; // TODO: (Note) THis is where we set the margin for vertical
|
||||
}
|
||||
|
||||
.react-kanban-column-header {
|
||||
padding-bottom: 10px;
|
||||
font-weight: bold;
|
||||
.lane-title {
|
||||
vertical-align: middle;
|
||||
|
||||
.icon {
|
||||
margin-right: 8px; /* Adjust the spacing as needed */
|
||||
}
|
||||
}
|
||||
|
||||
.react-kanban-column-header input:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.react-kanban-column-header__button {
|
||||
color: #333333;
|
||||
background-color: #ffffff;
|
||||
border-color: #cccccc;
|
||||
}
|
||||
|
||||
.react-kanban-column-header__button:hover,
|
||||
.react-kanban-column-header__button:focus,
|
||||
.react-kanban-column-header__button:active {
|
||||
background-color: #e6e6e6;
|
||||
}
|
||||
|
||||
.react-kanban-column-adder-button {
|
||||
border: 2px dashed #eee;
|
||||
height: 132px;
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.react-kanban-column-adder-button:hover {
|
||||
cursor: pointer;
|
||||
.header-content-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
.body-empty-container {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
}
|
||||
.tech-container {
|
||||
font-weight: bolder;
|
||||
text-align: center;
|
||||
flex: 1;
|
||||
.branches-outlined {
|
||||
color: orangered;
|
||||
}
|
||||
}
|
||||
.inner-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
.circle-outline {
|
||||
color: orangered;
|
||||
margin-left: 8px;
|
||||
}
|
||||
.iou-parent {
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,121 +1,107 @@
|
||||
// Function to sort an array of objects by parentId
|
||||
import { groupBy } from "lodash";
|
||||
|
||||
const sortByParentId = (arr) => {
|
||||
// return arr.reduce((accumulator, currentValue) => {
|
||||
// //Find the parent item.
|
||||
// let item = accumulator.find((x) => x.id === currentValue.kanbanparent);
|
||||
// //Get index of parent item
|
||||
// let index = accumulator.indexOf(item);
|
||||
|
||||
// index = index !== -1 ? index + 1 : 0;
|
||||
// accumulator.splice(index, 0, currentValue);
|
||||
// return accumulator;
|
||||
// }, []);
|
||||
|
||||
let parentId = "-1";
|
||||
const sortedList = [];
|
||||
const byParentsIdsList = groupBy(arr, "kanbanparent"); // Create a new array with objects indexed by parentId
|
||||
//console.log("sortByParentId -> byParentsIdsList", byParentsIdsList);
|
||||
const byParentsIdsList = groupBy(arr, "kanbanparent");
|
||||
|
||||
while (byParentsIdsList[parentId]) {
|
||||
sortedList.push(...byParentsIdsList[parentId]); //Spread in the whole list in case several items have the same parents.
|
||||
parentId = byParentsIdsList[parentId][byParentsIdsList[parentId].length -1].id; //Grab the ID from the last one.
|
||||
sortedList.push(...byParentsIdsList[parentId]);
|
||||
parentId = byParentsIdsList[parentId][byParentsIdsList[parentId].length - 1].id;
|
||||
}
|
||||
|
||||
if (byParentsIdsList["null"]) byParentsIdsList["null"].map((i) => sortedList.push(i));
|
||||
if (byParentsIdsList["null"]) {
|
||||
sortedList.push(...byParentsIdsList["null"]);
|
||||
}
|
||||
|
||||
//Validate that the 2 arrays are of the same length and no children are missing.
|
||||
// Ensure all items are included in the sorted list
|
||||
if (arr.length !== sortedList.length) {
|
||||
arr.map((origItem) => {
|
||||
if (!!!sortedList.find((s) => s.id === origItem.id)) {
|
||||
arr.forEach((origItem) => {
|
||||
if (!sortedList.some((s) => s.id === origItem.id)) {
|
||||
sortedList.push(origItem);
|
||||
console.log("DATA CONSISTENCY ERROR: ", origItem.ro_number);
|
||||
}
|
||||
return 1;
|
||||
});
|
||||
}
|
||||
|
||||
return sortedList;
|
||||
};
|
||||
|
||||
export const createBoardData = (AllStatuses, Jobs, filter) => {
|
||||
const { search, employeeId } = filter;
|
||||
const boardLanes = {
|
||||
columns: AllStatuses.map((s) => {
|
||||
return {
|
||||
id: s,
|
||||
title: s,
|
||||
cards: []
|
||||
};
|
||||
})
|
||||
};
|
||||
// Function to create board data based on statuses and jobs, with optional filtering
|
||||
export const createBoardData = ({ statuses, data, filter, cardSettings }) => {
|
||||
const { search, employeeId, alert } = filter;
|
||||
|
||||
const filteredJobs =
|
||||
(search === "" || !search) && !employeeId
|
||||
? Jobs
|
||||
: Jobs.filter((j) => {
|
||||
let include = false;
|
||||
if (search && search !== "") {
|
||||
include = CheckSearch(search, j);
|
||||
}
|
||||
const lanes = statuses.map((status) => ({
|
||||
id: status,
|
||||
title: status,
|
||||
cards: []
|
||||
}));
|
||||
|
||||
if (!!employeeId) {
|
||||
include =
|
||||
include ||
|
||||
j.employee_body === employeeId ||
|
||||
j.employee_prep === employeeId ||
|
||||
j.employee_csr === employeeId ||
|
||||
j.employee_refinish === employeeId;
|
||||
}
|
||||
let filteredJobs =
|
||||
(search === "" || !search) && !employeeId ? data : data.filter((job) => checkFilter(search, employeeId, job));
|
||||
|
||||
return include;
|
||||
});
|
||||
// Filter jobs by selectedMdInsCos if it has values
|
||||
if (cardSettings?.selectedMdInsCos?.length > 0) {
|
||||
filteredJobs = filteredJobs.filter((job) => cardSettings.selectedMdInsCos.includes(job.ins_co_nm));
|
||||
}
|
||||
|
||||
const DataGroupedByStatus = groupBy(filteredJobs, (d) => d.status);
|
||||
// Filter jobs by selectedEstimators if it has values
|
||||
if (cardSettings?.selectedEstimators?.length > 0) {
|
||||
filteredJobs = filteredJobs.filter((job) =>
|
||||
cardSettings.selectedEstimators.includes(`${job.est_ct_fn} ${job.est_ct_ln}`)
|
||||
);
|
||||
}
|
||||
|
||||
Object.keys(DataGroupedByStatus).map((statusGroupKey) => {
|
||||
// Filter jobs by alert if alert filter is true
|
||||
if (alert) {
|
||||
filteredJobs = filteredJobs.filter((job) => job.production_vars?.alert);
|
||||
}
|
||||
|
||||
const DataGroupedByStatus = groupBy(filteredJobs, "status");
|
||||
|
||||
Object.keys(DataGroupedByStatus).forEach((statusGroupKey) => {
|
||||
try {
|
||||
const needle = boardLanes.columns.find((l) => l.id === statusGroupKey);
|
||||
if (!needle?.cards) return null;
|
||||
needle.cards = sortByParentId(DataGroupedByStatus[statusGroupKey]);
|
||||
const lane = lanes.find((l) => l.id === statusGroupKey);
|
||||
if (!lane) return;
|
||||
|
||||
lane.cards = sortByParentId(DataGroupedByStatus[statusGroupKey]).map((job) => {
|
||||
const { id, title, description, due_date, ...metadata } = job;
|
||||
return {
|
||||
id,
|
||||
title,
|
||||
description,
|
||||
label: due_date || "",
|
||||
metadata
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
console.log("Error while creating board card", error);
|
||||
console.error("Error while creating board card", error);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
return boardLanes;
|
||||
return { lanes };
|
||||
};
|
||||
|
||||
const CheckSearch = (search, job) => {
|
||||
return (
|
||||
(job.ro_number || "").toLowerCase().includes(search.toLowerCase()) ||
|
||||
(job.ownr_fn || "").toLowerCase().includes(search.toLowerCase()) ||
|
||||
(job.ownr_co_nm || "").toLowerCase().includes(search.toLowerCase()) ||
|
||||
(job.ownr_ln || "").toLowerCase().includes(search.toLowerCase()) ||
|
||||
(job.status || "").toLowerCase().includes(search.toLowerCase()) ||
|
||||
(job.v_make_desc || "").toLowerCase().includes(search.toLowerCase()) ||
|
||||
(job.v_model_desc || "").toLowerCase().includes(search.toLowerCase()) ||
|
||||
(job.clm_no || "").toLowerCase().includes(search.toLowerCase()) ||
|
||||
(job.plate_no || "").toLowerCase().includes(search.toLowerCase())
|
||||
);
|
||||
// Function to check if a job matches the search and/or employeeId filter
|
||||
const checkFilter = (search, employeeId, job) => {
|
||||
const lowerSearch = search?.toLowerCase() ?? "";
|
||||
|
||||
const matchesSearch =
|
||||
lowerSearch &&
|
||||
[
|
||||
job.ro_number,
|
||||
job.ownr_fn,
|
||||
job.ownr_co_nm,
|
||||
job.ownr_ln,
|
||||
job.status,
|
||||
job.v_make_desc,
|
||||
job.v_model_desc,
|
||||
job.clm_no,
|
||||
job.plate_no
|
||||
].some((field) => field?.toLowerCase().includes(lowerSearch));
|
||||
|
||||
const matchesEmployeeId =
|
||||
employeeId && [job.employee_body, job.employee_prep, job.employee_csr, job.employee_refinish].includes(employeeId);
|
||||
|
||||
return matchesSearch || matchesEmployeeId;
|
||||
};
|
||||
|
||||
// export const updateBoardOnMove = (board, card, source, destination) => {
|
||||
// //Slice from source
|
||||
|
||||
// const sourceCardList = board.columns.find((x) => x.id === source.fromColumnId)
|
||||
// .cards;
|
||||
// sourceCardList.slice(source.fromPosition, 0);
|
||||
|
||||
// //Splice into destination.
|
||||
// const destCardList = board.columns.find(
|
||||
// (x) => x.id === destination.toColumnId
|
||||
// ).cards;
|
||||
// console.log("updateBoardOnMove -> destCardList", destCardList);
|
||||
|
||||
// destCardList.splice(destination.toPosition, 0, card);
|
||||
// console.log("updateBoardOnMove -> destCardList", destCardList);
|
||||
// console.log("board", board);
|
||||
// return board;
|
||||
// };
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import React from "react";
|
||||
import { Card, Form, Select } from "antd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const FilterSettings = ({
|
||||
selectedMdInsCos,
|
||||
setSelectedMdInsCos,
|
||||
selectedEstimators,
|
||||
setSelectedEstimators,
|
||||
setHasChanges,
|
||||
bodyshop,
|
||||
data
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const extractNames = (source, firstNameKey, lastNameKey) =>
|
||||
source.map((item) => ({
|
||||
firstName: item[firstNameKey],
|
||||
lastName: item[lastNameKey]
|
||||
}));
|
||||
|
||||
const bodyshopNames = extractNames(bodyshop.md_estimators, "est_ct_fn", "est_ct_ln");
|
||||
const dataNames = extractNames(data, "est_ct_fn", "est_ct_ln");
|
||||
|
||||
const combinedNames = [...bodyshopNames, ...dataNames];
|
||||
|
||||
const uniqueNames = Array.from(
|
||||
new Map(combinedNames.map((item) => [`${item.firstName} ${item.lastName}`, item])).values()
|
||||
);
|
||||
|
||||
return (
|
||||
<Card title={t("production.settings.filters_title")}>
|
||||
<Form.Item label={t("production.settings.filters.md_ins_cos")}>
|
||||
<Select
|
||||
mode="multiple"
|
||||
placeholder={t("production.settings.filters.md_ins_cos")}
|
||||
value={selectedMdInsCos}
|
||||
onChange={(value) => {
|
||||
setSelectedMdInsCos(value);
|
||||
setHasChanges(true);
|
||||
}}
|
||||
options={bodyshop.md_ins_cos.map((item) => ({
|
||||
value: item.name,
|
||||
label: item.name
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label={t("production.settings.filters.md_estimators")}>
|
||||
<Select
|
||||
mode="multiple"
|
||||
placeholder={t("production.settings.filters.md_estimators")}
|
||||
value={selectedEstimators}
|
||||
onChange={(value) => {
|
||||
setSelectedEstimators(value);
|
||||
setHasChanges(true);
|
||||
}}
|
||||
options={uniqueNames.map((item) => {
|
||||
const name = `${item.firstName} ${item.lastName}`.trim();
|
||||
return {
|
||||
value: name,
|
||||
label: name
|
||||
};
|
||||
})}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
FilterSettings.propTypes = {
|
||||
selectedMdInsCos: PropTypes.array.isRequired,
|
||||
setSelectedMdInsCos: PropTypes.func.isRequired,
|
||||
setHasChanges: PropTypes.func.isRequired,
|
||||
selectedEstimators: PropTypes.array.isRequired,
|
||||
setSelectedEstimators: PropTypes.func,
|
||||
bodyshop: PropTypes.object.isRequired,
|
||||
data: PropTypes.arrayOf(PropTypes.object).isRequired
|
||||
};
|
||||
|
||||
export default FilterSettings;
|
||||
@@ -0,0 +1,38 @@
|
||||
import { Card, Checkbox, Col, Form, Row } from "antd";
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const InformationSettings = ({ t }) => (
|
||||
<Card title={t("production.settings.information")}>
|
||||
<Row gutter={[16, 16]}>
|
||||
{[
|
||||
"model_info",
|
||||
"ownr_nm",
|
||||
"clm_no",
|
||||
"ins_co_nm",
|
||||
"employeeassignments",
|
||||
"actual_in",
|
||||
"scheduled_completion",
|
||||
"ats",
|
||||
"production_note",
|
||||
"sublets",
|
||||
"partsstatus",
|
||||
"estimator",
|
||||
"subtotal",
|
||||
"tasks"
|
||||
].map((item) => (
|
||||
<Col span={4} key={item}>
|
||||
<Form.Item name={item} valuePropName="checked">
|
||||
<Checkbox>{t(`production.labels.${item}`)}</Checkbox>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
</Card>
|
||||
);
|
||||
|
||||
InformationSettings.propTypes = {
|
||||
t: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default InformationSettings;
|
||||
@@ -0,0 +1,71 @@
|
||||
import { Card, Col, Form, Radio, Row } from "antd";
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const LayoutSettings = ({ t }) => (
|
||||
<Card title={t("production.settings.layout")}>
|
||||
<Row gutter={[16, 16]}>
|
||||
{[
|
||||
{
|
||||
name: "orientation",
|
||||
label: t("production.labels.orientation"),
|
||||
options: [
|
||||
{ value: true, label: t("production.labels.vertical") },
|
||||
{ value: false, label: t("production.labels.horizontal") }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "cardSize",
|
||||
label: t("production.labels.card_size"),
|
||||
options: [
|
||||
{ value: "small", label: t("production.options.small") },
|
||||
{ value: "medium", label: t("production.options.medium") },
|
||||
{ value: "large", label: t("production.options.large") }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "compact",
|
||||
label: t("production.labels.compact"),
|
||||
options: [
|
||||
{ value: true, label: t("production.labels.tall") },
|
||||
{ value: false, label: t("production.labels.wide") }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "cardcolor",
|
||||
label: t("production.labels.cardcolor"),
|
||||
options: [
|
||||
{ value: true, label: t("production.labels.on") },
|
||||
{ value: false, label: t("production.labels.off") }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "kiosk",
|
||||
label: t("production.labels.kiosk_mode"),
|
||||
options: [
|
||||
{ value: true, label: t("production.labels.on") },
|
||||
{ value: false, label: t("production.labels.off") }
|
||||
]
|
||||
}
|
||||
].map(({ name, label, options }) => (
|
||||
<Col span={4} key={name}>
|
||||
<Form.Item name={name} label={label}>
|
||||
<Radio.Group>
|
||||
{options.map((option) => (
|
||||
<Radio.Button key={option.value.toString()} value={option.value}>
|
||||
{option.label}
|
||||
</Radio.Button>
|
||||
))}
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
</Card>
|
||||
);
|
||||
|
||||
LayoutSettings.propTypes = {
|
||||
t: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default LayoutSettings;
|
||||
@@ -0,0 +1,59 @@
|
||||
import { DragDropContext, Draggable, Droppable } from "../trello-board/dnd/lib/index.js";
|
||||
import { statisticsItems } from "./defaultKanbanSettings.js";
|
||||
import { Card, Checkbox, Form } from "antd";
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const StatisticsSettings = ({ t, statisticsOrder, setStatisticsOrder, setHasChanges }) => {
|
||||
const onDragEnd = (result) => {
|
||||
if (!result.destination) return;
|
||||
const newOrder = Array.from(statisticsOrder);
|
||||
const [movedItem] = newOrder.splice(result.source.index, 1);
|
||||
newOrder.splice(result.destination.index, 0, movedItem);
|
||||
setStatisticsOrder(newOrder);
|
||||
setHasChanges(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card title={t("production.settings.statistics_title")}>
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
<Droppable direction="grid" droppableId="statistics">
|
||||
{(provided) => (
|
||||
<div
|
||||
{...provided.droppableProps}
|
||||
ref={provided.innerRef}
|
||||
style={{ display: "flex", flexWrap: "wrap", gap: "8px" }}
|
||||
>
|
||||
{statisticsOrder.map((itemId, index) => {
|
||||
const item = statisticsItems.find((stat) => stat.id === itemId);
|
||||
return (
|
||||
<Draggable key={itemId} draggableId={itemId.toString()} index={index}>
|
||||
{(provided) => (
|
||||
<div ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}>
|
||||
<Card styles={{ body: { padding: "5px" } }} style={{ marginBottom: 8, flex: "0 1 auto" }}>
|
||||
<Form.Item style={{ marginBottom: 0 }} name={item.name} valuePropName="checked">
|
||||
<Checkbox>{t(`production.settings.statistics.${item.label}`)}</Checkbox>
|
||||
</Form.Item>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
);
|
||||
})}
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
</DragDropContext>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
StatisticsSettings.propTypes = {
|
||||
t: PropTypes.func.isRequired,
|
||||
statisticsOrder: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
setStatisticsOrder: PropTypes.func.isRequired,
|
||||
setHasChanges: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default StatisticsSettings;
|
||||
@@ -0,0 +1,69 @@
|
||||
const statisticsItems = [
|
||||
{ id: 0, name: "totalHrs", label: "total_hours_in_production" },
|
||||
{ id: 1, name: "totalAmountInProduction", label: "total_amount_in_production" },
|
||||
{ id: 2, name: "totalLAB", label: "total_lab_in_production" },
|
||||
{ id: 3, name: "totalLAR", label: "total_lar_in_production" },
|
||||
{ id: 4, name: "jobsInProduction", label: "jobs_in_production" },
|
||||
{ id: 5, name: "totalHrsOnBoard", label: "total_hours_on_board" },
|
||||
{ id: 6, name: "totalAmountOnBoard", label: "total_amount_on_board" },
|
||||
{ id: 7, name: "totalLABOnBoard", label: "total_lab_on_board" },
|
||||
{ id: 8, name: "totalLAROnBoard", label: "total_lar_on_board" },
|
||||
{ id: 9, name: "jobsOnBoard", label: "total_jobs_on_board" },
|
||||
{ id: 10, name: "tasksOnBoard", label: "tasks_on_board" },
|
||||
{ id: 11, name: "tasksInProduction", label: "tasks_in_production" }
|
||||
];
|
||||
|
||||
const defaultKanbanSettings = {
|
||||
ats: true,
|
||||
clm_no: true,
|
||||
compact: false,
|
||||
ownr_nm: true,
|
||||
sublets: true,
|
||||
ins_co_nm: true,
|
||||
production_note: true,
|
||||
employeeassignments: true,
|
||||
scheduled_completion: true,
|
||||
cardcolor: false,
|
||||
orientation: false,
|
||||
tasks: false,
|
||||
cardSize: "small",
|
||||
model_info: true,
|
||||
kiosk: false,
|
||||
totalHrs: true,
|
||||
totalAmountInProduction: false,
|
||||
totalLAB: true,
|
||||
totalLAR: true,
|
||||
jobsInProduction: true,
|
||||
totalHrsOnBoard: false,
|
||||
totalLABOnBoard: false,
|
||||
totalLAROnBoard: false,
|
||||
jobsOnBoard: false,
|
||||
tasksOnBoard: false,
|
||||
tasksInProduction: false,
|
||||
totalAmountOnBoard: true,
|
||||
estimator: false,
|
||||
subtotal: false,
|
||||
statisticsOrder: statisticsItems.map((item) => item.id),
|
||||
selectedMdInsCos: [],
|
||||
selectedEstimators: []
|
||||
};
|
||||
|
||||
const defaultFilters = { search: "", employeeId: null, alert: false };
|
||||
|
||||
const mergeWithDefaults = (settings) => {
|
||||
// Create a new object that starts with the default settings
|
||||
const mergedSettings = { ...defaultKanbanSettings };
|
||||
|
||||
// Override with the provided settings, if any
|
||||
if (settings) {
|
||||
for (const key in settings) {
|
||||
if (settings.hasOwnProperty(key)) {
|
||||
mergedSettings[key] = settings[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return mergedSettings;
|
||||
};
|
||||
|
||||
export { defaultKanbanSettings, statisticsItems, mergeWithDefaults, defaultFilters };
|
||||
@@ -0,0 +1,169 @@
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { Button, Card, Col, Form, notification, Popover, Row, Tabs } from "antd";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { UPDATE_KANBAN_SETTINGS } from "../../../graphql/user.queries.js";
|
||||
import { defaultKanbanSettings, mergeWithDefaults } from "./defaultKanbanSettings.js";
|
||||
import LayoutSettings from "./LayoutSettings.jsx";
|
||||
import InformationSettings from "./InformationSettings.jsx";
|
||||
import StatisticsSettings from "./StatisticsSettings.jsx";
|
||||
import FilterSettings from "./FilterSettings.jsx";
|
||||
import PropTypes from "prop-types";
|
||||
import { isFunction } from "lodash";
|
||||
|
||||
function ProductionBoardKanbanSettings({ associationSettings, parentLoading, bodyshop, data, onSettingsChange }) {
|
||||
const [form] = Form.useForm();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [hasChanges, setHasChanges] = useState(false);
|
||||
const [statisticsOrder, setStatisticsOrder] = useState(defaultKanbanSettings.statisticsOrder);
|
||||
const [selectedMdInsCos, setSelectedMdInsCos] = useState(defaultKanbanSettings.selectedMdInsCos);
|
||||
const [selectedEstimators, setSelectedEstimators] = useState(defaultKanbanSettings.selectedEstimators);
|
||||
|
||||
const [updateKbSettings] = useMutation(UPDATE_KANBAN_SETTINGS);
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
if (associationSettings?.kanban_settings) {
|
||||
const finalSettings = mergeWithDefaults(associationSettings.kanban_settings);
|
||||
form.setFieldsValue(finalSettings);
|
||||
setStatisticsOrder(finalSettings.statisticsOrder);
|
||||
setSelectedMdInsCos(finalSettings.selectedMdInsCos);
|
||||
setSelectedEstimators(finalSettings.selectedEstimators);
|
||||
}
|
||||
}, [form, associationSettings]);
|
||||
|
||||
const handleFinish = async (values) => {
|
||||
setLoading(true);
|
||||
parentLoading(true);
|
||||
|
||||
const result = await updateKbSettings({
|
||||
variables: {
|
||||
id: associationSettings?.id,
|
||||
ks: {
|
||||
...associationSettings.kanban_settings,
|
||||
...values,
|
||||
statisticsOrder,
|
||||
selectedMdInsCos,
|
||||
selectedEstimators
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (result.errors) {
|
||||
notification.open({
|
||||
type: "error",
|
||||
message: t("production.errors.settings", {
|
||||
error: JSON.stringify(result.errors)
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
setOpen(false);
|
||||
setLoading(false);
|
||||
parentLoading(false);
|
||||
|
||||
if (onSettingsChange && isFunction(onSettingsChange)) {
|
||||
onSettingsChange(values);
|
||||
}
|
||||
|
||||
setHasChanges(false);
|
||||
};
|
||||
|
||||
const handleValuesChange = () => setHasChanges(true);
|
||||
|
||||
const handleRestoreDefaults = () => {
|
||||
form.setFieldsValue({
|
||||
...defaultKanbanSettings,
|
||||
statisticsOrder: defaultKanbanSettings.statisticsOrder
|
||||
});
|
||||
setStatisticsOrder(defaultKanbanSettings.statisticsOrder);
|
||||
setSelectedMdInsCos(defaultKanbanSettings.selectedMdInsCos);
|
||||
setSelectedEstimators(defaultKanbanSettings.selectedEstimators);
|
||||
setHasChanges(true);
|
||||
};
|
||||
|
||||
const overlay = (
|
||||
<Card style={{ minWidth: "80vw" }}>
|
||||
<Form form={form} onFinish={handleFinish} layout="vertical" onValuesChange={handleValuesChange}>
|
||||
<Tabs
|
||||
defaultActiveKey="1"
|
||||
items={[
|
||||
{
|
||||
key: "1",
|
||||
label: t("production.settings.layout"),
|
||||
children: <LayoutSettings t={t} />
|
||||
},
|
||||
{
|
||||
key: "2",
|
||||
label: t("production.settings.information"),
|
||||
children: <InformationSettings t={t} />
|
||||
},
|
||||
{
|
||||
key: "3",
|
||||
label: t("production.settings.statistics_title"),
|
||||
children: (
|
||||
<StatisticsSettings
|
||||
t={t}
|
||||
statisticsOrder={statisticsOrder}
|
||||
setStatisticsOrder={setStatisticsOrder}
|
||||
setHasChanges={setHasChanges}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
key: "4",
|
||||
label: t("production.settings.filters_title"),
|
||||
children: (
|
||||
<FilterSettings
|
||||
selectedMdInsCos={selectedMdInsCos}
|
||||
setSelectedMdInsCos={setSelectedMdInsCos}
|
||||
selectedEstimators={selectedEstimators}
|
||||
setSelectedEstimators={setSelectedEstimators}
|
||||
setHasChanges={setHasChanges}
|
||||
bodyshop={bodyshop}
|
||||
data={data}
|
||||
/>
|
||||
)
|
||||
}
|
||||
]}
|
||||
/>
|
||||
<Row justify="center" style={{ marginTop: 15 }} gutter={16}>
|
||||
<Col span={8}>
|
||||
<Button block onClick={() => setOpen(false)}>
|
||||
{t("general.actions.cancel")}
|
||||
</Button>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Button block onClick={handleRestoreDefaults}>
|
||||
{t("general.actions.defaults")}
|
||||
</Button>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Button block onClick={form.submit} loading={loading} type="primary" disabled={!hasChanges}>
|
||||
{t("general.actions.save")}
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
</Card>
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover content={overlay} open={open} placement="topRight">
|
||||
<Button loading={loading} onClick={() => setOpen(!open)}>
|
||||
{t("production.settings.board_settings")}
|
||||
</Button>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
ProductionBoardKanbanSettings.propTypes = {
|
||||
associationSettings: PropTypes.object,
|
||||
parentLoading: PropTypes.func.isRequired,
|
||||
bodyshop: PropTypes.object.isRequired,
|
||||
onSettingsChange: PropTypes.func,
|
||||
data: PropTypes.array
|
||||
};
|
||||
|
||||
export default ProductionBoardKanbanSettings;
|
||||
@@ -0,0 +1,83 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
/**
|
||||
* Height Memory Wrapper
|
||||
* @param children
|
||||
* @param maxHeight
|
||||
* @param setMaxHeight
|
||||
* @param override - Override the minHeight style from being set
|
||||
* @param itemKey - Unique key to preserve height for items with the same key
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
const HeightMemoryWrapper = ({ children, maxHeight, setMaxHeight, override, itemKey }) => {
|
||||
const ref = useRef(null);
|
||||
const heightMapRef = useRef(new Map());
|
||||
const [localMaxHeight, setLocalMaxHeight] = useState(maxHeight);
|
||||
const [devicePixelRatio, setDevicePixelRatio] = useState(window.devicePixelRatio);
|
||||
|
||||
useEffect(() => {
|
||||
const currentRef = ref.current;
|
||||
const updateHeight = () => {
|
||||
const currentHeight = currentRef?.firstChild?.clientHeight || 0;
|
||||
if (itemKey) {
|
||||
const keyHeight = heightMapRef.current.get(itemKey) || 0;
|
||||
const newHeight = Math.max(keyHeight, currentHeight);
|
||||
heightMapRef.current.set(itemKey, newHeight);
|
||||
setLocalMaxHeight(newHeight);
|
||||
} else {
|
||||
setLocalMaxHeight((prevHeight) => Math.max(prevHeight, currentHeight));
|
||||
}
|
||||
setMaxHeight((prevHeight) => Math.max(prevHeight, currentHeight));
|
||||
};
|
||||
|
||||
const resizeObserver = new ResizeObserver(updateHeight);
|
||||
|
||||
if (currentRef?.firstChild) {
|
||||
resizeObserver.observe(currentRef.firstChild);
|
||||
}
|
||||
|
||||
const resizeHandler = () => {
|
||||
if (Math.abs(window.devicePixelRatio - devicePixelRatio) > 0.1) {
|
||||
// Threshold to detect significant zoom level change
|
||||
heightMapRef.current.clear(); // Clearing the height memory as zoom level has changed significantly
|
||||
setLocalMaxHeight(0); // Reset local max height
|
||||
setDevicePixelRatio(window.devicePixelRatio); // Update the recorded device pixel ratio
|
||||
}
|
||||
updateHeight();
|
||||
};
|
||||
|
||||
window.addEventListener("resize", resizeHandler);
|
||||
|
||||
return () => {
|
||||
if (currentRef?.firstChild) {
|
||||
resizeObserver.unobserve(currentRef.firstChild);
|
||||
}
|
||||
window.removeEventListener("resize", resizeHandler);
|
||||
};
|
||||
}, [itemKey, setMaxHeight, devicePixelRatio]);
|
||||
|
||||
useEffect(() => {
|
||||
if (itemKey && heightMapRef.current.has(itemKey)) {
|
||||
setLocalMaxHeight(heightMapRef.current.get(itemKey));
|
||||
}
|
||||
}, [itemKey]);
|
||||
|
||||
const style = override ? {} : { minHeight: localMaxHeight };
|
||||
|
||||
return (
|
||||
<div ref={ref} style={style}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
HeightMemoryWrapper.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
maxHeight: PropTypes.number.isRequired,
|
||||
setMaxHeight: PropTypes.func.isRequired,
|
||||
override: PropTypes.bool,
|
||||
itemKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
|
||||
};
|
||||
|
||||
export default HeightMemoryWrapper;
|
||||
@@ -0,0 +1,24 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
const HeightPreservingItem = ({ children, ...props }) => {
|
||||
const [size, setSize] = useState(0);
|
||||
const knownSize = props["data-known-size"];
|
||||
useEffect(() => {
|
||||
setSize((prevSize) => {
|
||||
return knownSize === 0 ? prevSize : knownSize;
|
||||
});
|
||||
}, [setSize, knownSize]);
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
className="height-preserving-container"
|
||||
style={{
|
||||
"--child-height": `${size}px`
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default HeightPreservingItem;
|
||||
@@ -0,0 +1,9 @@
|
||||
import React from "react";
|
||||
|
||||
const ItemComponent = ({ children, maxCardHeight, maxCardWidth, ...props }) => (
|
||||
<div style={{ minWidth: maxCardWidth, minHeight: maxCardHeight }} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
export default ItemComponent;
|
||||
@@ -0,0 +1,9 @@
|
||||
import React from "react";
|
||||
|
||||
const ItemWrapper = React.memo(({ children, ...props }) => (
|
||||
<div {...props} className="item-wrapper">
|
||||
{children}
|
||||
</div>
|
||||
));
|
||||
|
||||
export default ItemWrapper;
|
||||
@@ -0,0 +1,11 @@
|
||||
import React from "react";
|
||||
import { LaneFooter } from "../styles/Base.js";
|
||||
import { CollapseBtn, ExpandBtn } from "../styles/Elements.js";
|
||||
|
||||
const LaneFooterComponent = ({ onClick, collapsed }) => (
|
||||
<LaneFooter className="react-trello-footer" onClick={onClick}>
|
||||
{collapsed ? <ExpandBtn /> : <CollapseBtn />}
|
||||
</LaneFooter>
|
||||
);
|
||||
|
||||
export default LaneFooterComponent;
|
||||
@@ -0,0 +1,9 @@
|
||||
import React, { forwardRef } from "react";
|
||||
|
||||
const ListComponent = forwardRef(({ style, children, ...props }, ref) => (
|
||||
<div ref={ref} {...props} style={{ ...style }}>
|
||||
{children}
|
||||
</div>
|
||||
));
|
||||
|
||||
export default ListComponent;
|
||||
@@ -0,0 +1,55 @@
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const SizeMemoryWrapper = ({ children, maxHeight, setMaxHeight, maxWidth, setMaxWidth }) => {
|
||||
const ref = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
const currentRef = ref.current;
|
||||
|
||||
const updateSize = () => {
|
||||
const currentHeight = currentRef?.firstChild?.clientHeight || 0;
|
||||
const currentWidth = currentRef?.firstChild?.clientWidth || 0;
|
||||
setMaxHeight((prevHeight) => Math.max(prevHeight, currentHeight));
|
||||
setMaxWidth((prevWidth) => Math.max(prevWidth, currentWidth));
|
||||
};
|
||||
|
||||
const resizeObserver = new ResizeObserver(updateSize);
|
||||
|
||||
if (currentRef?.firstChild) {
|
||||
resizeObserver.observe(currentRef.firstChild);
|
||||
}
|
||||
|
||||
const handleLoad = () => {
|
||||
if (window.devicePixelRatio < 1) {
|
||||
return; // Do not update width and height
|
||||
}
|
||||
updateSize();
|
||||
};
|
||||
|
||||
window.addEventListener("load", handleLoad);
|
||||
|
||||
return () => {
|
||||
if (currentRef?.firstChild) {
|
||||
resizeObserver.unobserve(currentRef.firstChild);
|
||||
}
|
||||
window.removeEventListener("load", handleLoad);
|
||||
};
|
||||
}, [setMaxHeight, setMaxWidth]);
|
||||
|
||||
return (
|
||||
<div ref={ref} className="size-memory-wrapper" style={{ minHeight: maxHeight, minWidth: maxWidth }}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
SizeMemoryWrapper.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
maxHeight: PropTypes.number.isRequired,
|
||||
setMaxHeight: PropTypes.func.isRequired,
|
||||
maxWidth: PropTypes.number.isRequired,
|
||||
setMaxWidth: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default SizeMemoryWrapper;
|
||||
@@ -0,0 +1,39 @@
|
||||
import { BoardContainer } from "../index";
|
||||
import { useMemo } from "react";
|
||||
import { StyleHorizontal, StyleVertical } from "../styles/Base.js";
|
||||
import { cardSizesVertical } from "../styles/Globals.js";
|
||||
|
||||
const Board = ({ id, className, orientation, cardSettings, ...additionalProps }) => {
|
||||
const OrientationStyle = useMemo(
|
||||
() => (orientation === "horizontal" ? StyleHorizontal : StyleVertical),
|
||||
[orientation]
|
||||
);
|
||||
|
||||
const gridItemWidth = useMemo(() => {
|
||||
switch (cardSettings?.cardSize) {
|
||||
case "small":
|
||||
return cardSizesVertical.small;
|
||||
case "large":
|
||||
return cardSizesVertical.large;
|
||||
case "medium":
|
||||
return cardSizesVertical.medium;
|
||||
default:
|
||||
return cardSizesVertical.small;
|
||||
}
|
||||
}, [cardSettings]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<OrientationStyle {...{ gridItemWidth }}>
|
||||
<BoardContainer
|
||||
orientation={orientation}
|
||||
cardSettings={cardSettings}
|
||||
{...additionalProps}
|
||||
className="react-trello-board"
|
||||
/>
|
||||
</OrientationStyle>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Board;
|
||||
@@ -0,0 +1,173 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { DragDropContext } from "../dnd/lib";
|
||||
import PropTypes from "prop-types";
|
||||
import isEqual from "lodash/isEqual";
|
||||
import Lane from "./Lane";
|
||||
import { PopoverWrapper } from "react-popopo";
|
||||
import * as actions from "../../../../redux/trello/trello.actions.js";
|
||||
import { BoardWrapper } from "../styles/Base.js";
|
||||
import ProductionStatistics from "../../production-board-kanban.statistics.jsx";
|
||||
|
||||
const useDragMap = () => {
|
||||
const dragMapRef = useRef(new Map());
|
||||
|
||||
const setDragTime = (laneId) => {
|
||||
dragMapRef.current.set(laneId, Date.now());
|
||||
};
|
||||
|
||||
const getLastDragTime = (laneId) => {
|
||||
return dragMapRef.current.get(laneId);
|
||||
};
|
||||
|
||||
return { setDragTime, getLastDragTime };
|
||||
};
|
||||
|
||||
const BoardContainer = ({
|
||||
data,
|
||||
onDataChange = () => {},
|
||||
onDragEnd = () => {},
|
||||
laneSortFunction = () => {},
|
||||
orientation = "horizontal",
|
||||
cardSettings = {},
|
||||
eventBusHandle,
|
||||
reducerData,
|
||||
queryData
|
||||
}) => {
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
const [maxLaneHeight, setMaxLaneHeight] = useState(0);
|
||||
const [maxCardHeight, setMaxCardHeight] = useState(0);
|
||||
const [maxCardWidth, setMaxCardWidth] = useState(0);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const currentReducerData = useSelector((state) => (state.trello.lanes ? state.trello : {}));
|
||||
const { setDragTime, getLastDragTime } = useDragMap();
|
||||
|
||||
const wireEventBus = useCallback(() => {
|
||||
const eventBus = {
|
||||
publish: (event) => {
|
||||
switch (event.type) {
|
||||
// case "ADD_CARD":
|
||||
// return dispatch(actions.addCard({ laneId: event.laneId, card: event.card }));
|
||||
// case "REMOVE_CARD":
|
||||
// return dispatch(actions.removeCard({ laneId: event.laneId, cardId: event.cardId }));
|
||||
// case "REFRESH_BOARD":
|
||||
// return dispatch(actions.loadBoard(event.data));
|
||||
// case "UPDATE_CARDS":
|
||||
// return dispatch(actions.updateCards({ laneId: event.laneId, cards: event.cards }));
|
||||
// case "UPDATE_CARD":
|
||||
// return dispatch(actions.updateCard({ laneId: event.laneId, updatedCard: event.card }));
|
||||
// case "UPDATE_LANES":
|
||||
// return dispatch(actions.updateLanes(event.lanes));
|
||||
// case "UPDATE_LANE":
|
||||
// return dispatch(actions.updateLane(event.lane));
|
||||
case "MOVE_CARD":
|
||||
return dispatch(
|
||||
actions.moveCardAcrossLanes({
|
||||
fromLaneId: event.fromLaneId,
|
||||
toLaneId: event.toLaneId,
|
||||
cardId: event.cardId,
|
||||
index: event.index,
|
||||
event
|
||||
})
|
||||
);
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
eventBusHandle(eventBus);
|
||||
}, [dispatch, eventBusHandle]);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(actions.loadBoard(data));
|
||||
if (eventBusHandle) {
|
||||
wireEventBus();
|
||||
}
|
||||
}, [data, eventBusHandle, dispatch, wireEventBus]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isEqual(currentReducerData, reducerData)) {
|
||||
onDataChange(currentReducerData);
|
||||
}
|
||||
}, [currentReducerData, reducerData, onDataChange]);
|
||||
|
||||
const onDragStart = useCallback(() => {
|
||||
setIsDragging(true);
|
||||
}, []);
|
||||
|
||||
const onLaneDrag = useCallback(
|
||||
async ({ draggableId, type, source, reason, mode, destination, combine }) => {
|
||||
setIsDragging(false);
|
||||
setDragTime(source.droppableId);
|
||||
if (!type || type !== "lane" || !source || !destination || isEqual(source, destination)) return;
|
||||
|
||||
setIsProcessing(true);
|
||||
|
||||
dispatch(
|
||||
actions.moveCardAcrossLanes({
|
||||
fromLaneId: source.droppableId,
|
||||
toLaneId: destination.droppableId,
|
||||
cardId: draggableId,
|
||||
index: destination.index
|
||||
})
|
||||
);
|
||||
|
||||
try {
|
||||
await onDragEnd({ draggableId, type, source, reason, mode, destination, combine });
|
||||
} catch (err) {
|
||||
console.error("Error in onLaneDrag", err);
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
}
|
||||
},
|
||||
[dispatch, onDragEnd, setDragTime]
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ProductionStatistics data={queryData} reducerData={currentReducerData} cardSettings={cardSettings} />
|
||||
<PopoverWrapper>
|
||||
<BoardWrapper orientation={orientation}>
|
||||
<DragDropContext onDragEnd={onLaneDrag} onDragStart={onDragStart} contextId="production-board">
|
||||
{currentReducerData.lanes.map((lane, index) => (
|
||||
<Lane
|
||||
key={lane.id}
|
||||
id={lane.id}
|
||||
title={lane.title}
|
||||
index={index}
|
||||
laneSortFunction={laneSortFunction}
|
||||
orientation={orientation}
|
||||
cards={lane.cards}
|
||||
isDragging={isDragging}
|
||||
isProcessing={isProcessing}
|
||||
cardSettings={cardSettings}
|
||||
maxLaneHeight={maxLaneHeight}
|
||||
setMaxLaneHeight={setMaxLaneHeight}
|
||||
maxCardHeight={maxCardHeight}
|
||||
setMaxCardHeight={setMaxCardHeight}
|
||||
maxCardWidth={maxCardWidth}
|
||||
setMaxCardWidth={setMaxCardWidth}
|
||||
lastDrag={getLastDragTime(lane.id)}
|
||||
/>
|
||||
))}
|
||||
</DragDropContext>
|
||||
</BoardWrapper>
|
||||
</PopoverWrapper>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
BoardContainer.propTypes = {
|
||||
id: PropTypes.string,
|
||||
data: PropTypes.object.isRequired,
|
||||
reducerData: PropTypes.object,
|
||||
onDataChange: PropTypes.func,
|
||||
eventBusHandle: PropTypes.func,
|
||||
laneSortFunction: PropTypes.func,
|
||||
handleDragEnd: PropTypes.func,
|
||||
orientation: PropTypes.string
|
||||
};
|
||||
|
||||
export default BoardContainer;
|
||||
@@ -0,0 +1,291 @@
|
||||
import React, { useCallback, useMemo, useRef, useState } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { bindActionCreators } from "redux";
|
||||
import { connect } from "react-redux";
|
||||
import * as actions from "../../../../redux/trello/trello.actions.js";
|
||||
import { Draggable, Droppable } from "../dnd/lib";
|
||||
import { Virtuoso, VirtuosoGrid } from "react-virtuoso";
|
||||
import HeightPreservingItem from "../components/HeightPreservingItem.jsx";
|
||||
import { Section } from "../styles/Base.js";
|
||||
import LaneFooter from "../components/LaneFooter.jsx";
|
||||
import { EyeInvisibleOutlined, EyeOutlined } from "@ant-design/icons";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../../../redux/user/user.selectors.js";
|
||||
import { selectTechnician } from "../../../../redux/tech/tech.selectors.js";
|
||||
import ProductionBoardCard from "../../production-board-kanban-card.component.jsx";
|
||||
import HeightMemoryWrapper from "../components/HeightMemoryWrapper.jsx";
|
||||
import SizeMemoryWrapper from "../components/SizeMemoryWrapper.jsx";
|
||||
import ListComponent from "../components/ListComponent.jsx";
|
||||
import ItemComponent from "../components/ItemComponent.jsx";
|
||||
import ItemWrapper from "../components/ItemWrapper.jsx";
|
||||
import objectHash from "object-hash";
|
||||
|
||||
/**
|
||||
* Lane is a React component that represents a lane in a Trello-like board.
|
||||
* @param id
|
||||
* @param title
|
||||
* @param index
|
||||
* @param isProcessing
|
||||
* @param laneSortFunction
|
||||
* @param cards
|
||||
* @param cardSettings
|
||||
* @param orientation
|
||||
* @param maxLaneHeight
|
||||
* @param setMaxLaneHeight
|
||||
* @param maxCardHeight
|
||||
* @param setMaxCardHeight
|
||||
* @param maxCardWidth
|
||||
* @param setMaxCardWidth
|
||||
* @param lastDrag
|
||||
* @param technician -- connected to redux
|
||||
* @param bodyshop -- connected to redux
|
||||
* @returns {Element}
|
||||
* @constructor
|
||||
*/
|
||||
const Lane = ({
|
||||
id,
|
||||
title,
|
||||
index,
|
||||
isProcessing,
|
||||
laneSortFunction,
|
||||
cards,
|
||||
cardSettings = {},
|
||||
orientation = "vertical",
|
||||
maxLaneHeight,
|
||||
setMaxLaneHeight,
|
||||
maxCardHeight,
|
||||
setMaxCardHeight,
|
||||
maxCardWidth,
|
||||
setMaxCardWidth,
|
||||
lastDrag,
|
||||
technician,
|
||||
bodyshop
|
||||
}) => {
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
const laneRef = useRef(null);
|
||||
|
||||
const sortedCards = useMemo(() => {
|
||||
if (!cards) return [];
|
||||
if (!laneSortFunction) return cards;
|
||||
return [...cards].sort(laneSortFunction);
|
||||
}, [cards, laneSortFunction]);
|
||||
|
||||
const toggleLaneCollapsed = useCallback(() => {
|
||||
setCollapsed((prevCollapsed) => !prevCollapsed);
|
||||
}, []);
|
||||
|
||||
const renderDraggable = useCallback(
|
||||
(index, card) => {
|
||||
if (!card) {
|
||||
console.log("null card");
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Draggable draggableId={card.id} index={index} key={card.id} isDragDisabled={isProcessing}>
|
||||
{(provided, snapshot) => (
|
||||
<div
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
ref={provided.innerRef}
|
||||
style={provided.draggableProps.style}
|
||||
className={`item ${snapshot.isDragging ? "is-dragging" : ""}`}
|
||||
key={card.id}
|
||||
>
|
||||
<SizeMemoryWrapper
|
||||
maxHeight={maxCardHeight}
|
||||
setMaxHeight={setMaxCardHeight}
|
||||
maxWidth={maxCardWidth}
|
||||
setMaxWidth={setMaxCardWidth}
|
||||
>
|
||||
<ProductionBoardCard
|
||||
technician={technician}
|
||||
bodyshop={bodyshop}
|
||||
cardSettings={cardSettings}
|
||||
key={card.id}
|
||||
card={card}
|
||||
style={{ minHeight: maxCardHeight, minWidth: maxCardWidth }}
|
||||
className="react-trello-card"
|
||||
/>
|
||||
</SizeMemoryWrapper>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
);
|
||||
},
|
||||
[isProcessing, technician, bodyshop, cardSettings, maxCardHeight, setMaxCardHeight, maxCardWidth, setMaxCardWidth]
|
||||
);
|
||||
|
||||
const renderDroppable = useCallback(
|
||||
(provided, renderedCards) => {
|
||||
const Component = orientation === "vertical" ? VirtuosoGrid : Virtuoso;
|
||||
const FinalComponent = collapsed ? "div" : Component;
|
||||
const commonProps = {
|
||||
useWindowScroll: true,
|
||||
data: renderedCards
|
||||
};
|
||||
|
||||
const verticalProps = {
|
||||
...commonProps,
|
||||
listClassName: "grid-container",
|
||||
itemClassName: "grid-item",
|
||||
customScrollParent: laneRef.current,
|
||||
components: {
|
||||
List: ListComponent,
|
||||
Item: ItemComponent
|
||||
},
|
||||
itemContent: (index, item) => <ItemWrapper>{renderDraggable(index, item)}</ItemWrapper>,
|
||||
overscan: { main: 10, reverse: 10 }
|
||||
};
|
||||
|
||||
const horizontalProps = {
|
||||
...commonProps,
|
||||
components: { Item: HeightPreservingItem },
|
||||
overscan: { main: 3, reverse: 3 },
|
||||
itemContent: (index, item) => renderDraggable(index, item),
|
||||
scrollerRef: provided.innerRef,
|
||||
style: {
|
||||
minWidth: maxCardWidth,
|
||||
minHeight: maxLaneHeight
|
||||
}
|
||||
};
|
||||
|
||||
const componentProps = orientation === "vertical" ? verticalProps : horizontalProps;
|
||||
|
||||
// If the lane is collapsed, we want to render a div instead of the virtualized list, and we want to set the height to the max height of the lane so that
|
||||
// the lane doesn't shrink when collapsed (in horizontal mode)
|
||||
const finalComponentProps = collapsed
|
||||
? orientation === "horizontal"
|
||||
? {
|
||||
style: {
|
||||
height: maxLaneHeight
|
||||
}
|
||||
}
|
||||
: {}
|
||||
: componentProps;
|
||||
|
||||
// If the lane is horizontal and collapsed, we want to render a placeholder so that the lane doesn't shrink to 0 height and grows when
|
||||
// a card is dragged over it
|
||||
const shouldRenderPlaceholder = orientation !== "horizontal" && (collapsed || renderedCards.length === 0);
|
||||
|
||||
return (
|
||||
<HeightMemoryWrapper
|
||||
itemKey={objectHash({
|
||||
id,
|
||||
orientation,
|
||||
cardSettings,
|
||||
cardLength: renderedCards?.length
|
||||
})}
|
||||
maxHeight={maxLaneHeight}
|
||||
setMaxHeight={setMaxLaneHeight}
|
||||
override={orientation !== "horizontal" && (collapsed || !renderedCards.length)}
|
||||
>
|
||||
<div
|
||||
{...provided.droppableProps}
|
||||
ref={provided.innerRef}
|
||||
className={`react-trello-lane ${collapsed ? "lane-collapsed" : ""}`}
|
||||
style={{ ...provided.droppableProps.style }}
|
||||
>
|
||||
<FinalComponent {...finalComponentProps} />
|
||||
{shouldRenderPlaceholder && provided.placeholder}
|
||||
</div>
|
||||
</HeightMemoryWrapper>
|
||||
);
|
||||
},
|
||||
[orientation, collapsed, renderDraggable, maxLaneHeight, setMaxLaneHeight, maxCardWidth, id, cardSettings]
|
||||
);
|
||||
|
||||
const renderDragContainer = useCallback(
|
||||
() => (
|
||||
<Droppable
|
||||
droppableId={id}
|
||||
index={index}
|
||||
type="lane"
|
||||
direction={orientation === "horizontal" ? "vertical" : "grid"}
|
||||
mode="virtual"
|
||||
renderClone={(provided, snapshot, rubric) => {
|
||||
const card = sortedCards[rubric.source.index];
|
||||
return (
|
||||
<div
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
ref={provided.innerRef}
|
||||
style={{
|
||||
...provided.draggableProps.style,
|
||||
minHeight: maxCardHeight,
|
||||
minWidth: maxCardWidth
|
||||
}}
|
||||
className={`clone ${snapshot.isDragging ? "is-dragging" : ""}`}
|
||||
key={card.id}
|
||||
>
|
||||
<ProductionBoardCard
|
||||
technician={technician}
|
||||
bodyshop={bodyshop}
|
||||
cardSettings={cardSettings}
|
||||
key={card.id}
|
||||
className="react-trello-card"
|
||||
card={card}
|
||||
clone={false}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
>
|
||||
{(provided) => renderDroppable(provided, sortedCards)}
|
||||
</Droppable>
|
||||
),
|
||||
[
|
||||
id,
|
||||
index,
|
||||
orientation,
|
||||
renderDroppable,
|
||||
sortedCards,
|
||||
technician,
|
||||
bodyshop,
|
||||
cardSettings,
|
||||
maxCardHeight,
|
||||
maxCardWidth
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
<Section key={`lane-${id}-${lastDrag}`} orientation={orientation} cardSettings={cardSettings}>
|
||||
<div onDoubleClick={toggleLaneCollapsed} className="react-trello-column-header">
|
||||
<span className="lane-title">
|
||||
{collapsed ? <EyeInvisibleOutlined className="icon" /> : <EyeOutlined className="icon" />}
|
||||
{title}
|
||||
</span>
|
||||
</div>
|
||||
{renderDragContainer()}
|
||||
<LaneFooter onClick={toggleLaneCollapsed} collapsed={collapsed} />
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
Lane.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
title: PropTypes.node.isRequired,
|
||||
index: PropTypes.number.isRequired,
|
||||
laneSortFunction: PropTypes.func,
|
||||
cards: PropTypes.array.isRequired,
|
||||
orientation: PropTypes.string.isRequired,
|
||||
isProcessing: PropTypes.bool.isRequired,
|
||||
cardSettings: PropTypes.object.isRequired,
|
||||
maxLaneHeight: PropTypes.number.isRequired,
|
||||
setMaxLaneHeight: PropTypes.func.isRequired,
|
||||
maxCardHeight: PropTypes.number.isRequired,
|
||||
setMaxCardHeight: PropTypes.func.isRequired,
|
||||
maxCardWidth: PropTypes.number.isRequired,
|
||||
setMaxCardWidth: PropTypes.func.isRequired,
|
||||
lastDrag: PropTypes.number
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
actions: bindActionCreators(actions, dispatch)
|
||||
});
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
technician: selectTechnician
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(Lane);
|
||||
@@ -0,0 +1,67 @@
|
||||
import { isEqual, origin } from "./state/position";
|
||||
|
||||
export const curves = {
|
||||
outOfTheWay: "cubic-bezier(0.2, 0, 0, 1)",
|
||||
drop: "cubic-bezier(.2,1,.1,1)"
|
||||
};
|
||||
export const combine = {
|
||||
opacity: {
|
||||
// while dropping: fade out totally
|
||||
drop: 0,
|
||||
// while dragging: fade out partially
|
||||
combining: 0.7
|
||||
},
|
||||
scale: {
|
||||
drop: 0.75
|
||||
}
|
||||
};
|
||||
export const timings = {
|
||||
outOfTheWay: 0.2,
|
||||
// greater than the out of the way time
|
||||
// so that when the drop ends everything will
|
||||
// have to be out of the way
|
||||
minDropTime: 0.33,
|
||||
maxDropTime: 0.55
|
||||
};
|
||||
|
||||
// slow timings
|
||||
// uncomment to use
|
||||
// export const timings = {
|
||||
// outOfTheWay: 2,
|
||||
// // greater than the out of the way time
|
||||
// // so that when the drop ends everything will
|
||||
// // have to be out of the way
|
||||
// minDropTime: 3,
|
||||
// maxDropTime: 4,
|
||||
// };
|
||||
|
||||
const outOfTheWayTiming = `${timings.outOfTheWay}s ${curves.outOfTheWay}`;
|
||||
export const placeholderTransitionDelayTime = 0.1;
|
||||
export const transitions = {
|
||||
fluid: `opacity ${outOfTheWayTiming}`,
|
||||
snap: `transform ${outOfTheWayTiming}, opacity ${outOfTheWayTiming}`,
|
||||
drop: (duration) => {
|
||||
const timing = `${duration}s ${curves.drop}`;
|
||||
return `transform ${timing}, opacity ${timing}`;
|
||||
},
|
||||
outOfTheWay: `transform ${outOfTheWayTiming}`,
|
||||
placeholder: `height ${outOfTheWayTiming}, width ${outOfTheWayTiming}, margin ${outOfTheWayTiming}`
|
||||
};
|
||||
const moveTo = (offset) => (isEqual(offset, origin) ? null : `translate(${offset.x}px, ${offset.y}px)`);
|
||||
export const transforms = {
|
||||
moveTo,
|
||||
drop: (offset, isCombining) => {
|
||||
const translate = moveTo(offset);
|
||||
if (!translate) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// only transforming the translate
|
||||
if (!isCombining) {
|
||||
return translate;
|
||||
}
|
||||
|
||||
// when dropping while combining we also update the scale
|
||||
return `${translate} scale(${combine.scale.drop})`;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
const average = (values) => {
|
||||
const sum = values.reduce((previous, current) => previous + current, 0);
|
||||
return sum / values.length;
|
||||
};
|
||||
export default (groupSize) => {
|
||||
console.log("Starting average action timer middleware");
|
||||
console.log(`Will take an average every ${groupSize} actions`);
|
||||
const bucket = {};
|
||||
return () => (next) => (action) => {
|
||||
const start = performance.now();
|
||||
const result = next(action);
|
||||
const end = performance.now();
|
||||
const duration = end - start;
|
||||
if (!bucket[action.type]) {
|
||||
bucket[action.type] = [duration];
|
||||
return result;
|
||||
}
|
||||
bucket[action.type].push(duration);
|
||||
if (bucket[action.type].length < groupSize) {
|
||||
return result;
|
||||
}
|
||||
console.warn(`Average time for ${action.type}`, average(bucket[action.type]));
|
||||
|
||||
// reset
|
||||
bucket[action.type] = [];
|
||||
return result;
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
import * as timings from "../timings";
|
||||
|
||||
export default () => (next) => (action) => {
|
||||
timings.forceEnable();
|
||||
const key = `redux action: ${action.type}`;
|
||||
timings.start(key);
|
||||
const result = next(action);
|
||||
timings.finish(key);
|
||||
return result;
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
export default (mode = "verbose") =>
|
||||
(store) =>
|
||||
(next) =>
|
||||
(action) => {
|
||||
if (mode === "light") {
|
||||
console.log("🏃 Action:", action.type);
|
||||
return next(action);
|
||||
}
|
||||
console.group(`action: ${action.type}`);
|
||||
console.log("action payload", action.payload);
|
||||
console.log("state before", store.getState());
|
||||
const result = next(action);
|
||||
console.log("state after", store.getState());
|
||||
console.groupEnd();
|
||||
return result;
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
export default () => (next) => (action) => {
|
||||
const title = `👾 redux (action): ${action.type}`;
|
||||
const startMark = `${action.type}:start`;
|
||||
const endMark = `${action.type}:end`;
|
||||
performance.mark(startMark);
|
||||
const result = next(action);
|
||||
performance.mark(endMark);
|
||||
performance.measure(title, startMark, endMark);
|
||||
return result;
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
const records = {};
|
||||
let isEnabled = false;
|
||||
const isTimingsEnabled = () => isEnabled;
|
||||
export const forceEnable = () => {
|
||||
isEnabled = true;
|
||||
};
|
||||
|
||||
// Debug: uncomment to enable
|
||||
// forceEnable();
|
||||
|
||||
export const start = (key) => {
|
||||
// we want to strip all the code out for production builds
|
||||
// draw back: can only do timings in dev env (which seems to be fine for now)
|
||||
if (import.meta.env.DEV) {
|
||||
if (!isTimingsEnabled()) {
|
||||
return;
|
||||
}
|
||||
const now = performance.now();
|
||||
records[key] = now;
|
||||
}
|
||||
};
|
||||
export const finish = (key) => {
|
||||
if (import.meta.env.DEV) {
|
||||
if (!isTimingsEnabled()) {
|
||||
return;
|
||||
}
|
||||
const now = performance.now();
|
||||
const previous = records[key];
|
||||
if (!previous) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn("cannot finish timing as no previous time found", key);
|
||||
return;
|
||||
}
|
||||
const result = now - previous;
|
||||
const rounded = result.toFixed(2);
|
||||
const style = (() => {
|
||||
if (result < 12) {
|
||||
return {
|
||||
textColor: "green",
|
||||
symbol: "✅"
|
||||
};
|
||||
}
|
||||
if (result < 40) {
|
||||
return {
|
||||
textColor: "orange",
|
||||
symbol: "⚠️"
|
||||
};
|
||||
}
|
||||
return {
|
||||
textColor: "red",
|
||||
symbol: "❌"
|
||||
};
|
||||
})();
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(
|
||||
`${style.symbol} %cTiming %c${rounded} %cms %c${key}`,
|
||||
// title
|
||||
"color: blue; font-weight: bold;",
|
||||
// result
|
||||
`color: ${style.textColor}; font-size: 1.1em;`,
|
||||
// ms
|
||||
"color: grey;",
|
||||
// key
|
||||
"color: purple; font-weight: bold;"
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
const isProduction = import.meta.env.PROD;
|
||||
|
||||
// not replacing newlines (which \s does)
|
||||
const spacesAndTabs = /[ \t]{2,}/g;
|
||||
const lineStartWithSpaces = /^[ \t]*/gm;
|
||||
|
||||
// using .trim() to clear the any newlines before the first text and after last text
|
||||
const clean = (value) => value.replace(spacesAndTabs, " ").replace(lineStartWithSpaces, "").trim();
|
||||
const getDevMessage = (message) =>
|
||||
clean(`
|
||||
%creact-beautiful-dnd
|
||||
|
||||
%c${clean(message)}
|
||||
|
||||
%c👷 This is a development only message. It will be removed in production builds.
|
||||
`);
|
||||
export const getFormattedMessage = (message) => [
|
||||
getDevMessage(message),
|
||||
// title (green400)
|
||||
"color: #00C584; font-size: 1.2em; font-weight: bold;",
|
||||
// message
|
||||
"line-height: 1.5",
|
||||
// footer (purple300)
|
||||
"color: #723874;"
|
||||
];
|
||||
const isDisabledFlag = "__react-beautiful-dnd-disable-dev-warnings";
|
||||
|
||||
export function log(type, message) {
|
||||
// no warnings in production
|
||||
if (isProduction) {
|
||||
return;
|
||||
}
|
||||
|
||||
// manual opt out of warnings
|
||||
if (typeof window !== "undefined" && window[isDisabledFlag]) {
|
||||
return;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console[type](...getFormattedMessage(message));
|
||||
}
|
||||
|
||||
export const warning = log.bind(null, "warn");
|
||||
export const error = log.bind(null, "error");
|
||||
@@ -0,0 +1,5 @@
|
||||
export function noop() {}
|
||||
|
||||
export function identity(value) {
|
||||
return value;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// Components
|
||||
export { default as DragDropContext } from "./view/drag-drop-context";
|
||||
export { default as Droppable } from "./view/droppable";
|
||||
export { default as Draggable } from "./view/draggable";
|
||||
|
||||
// Default sensors
|
||||
|
||||
export { useMouseSensor, useTouchSensor, useKeyboardSensor } from "./view/use-sensor-marshal";
|
||||
|
||||
// Utils
|
||||
|
||||
export { resetServerContext } from "./view/drag-drop-context";
|
||||
|
||||
// Public flow types
|
||||
|
||||
// Droppable types
|
||||
|
||||
// Draggable types
|
||||
@@ -0,0 +1,32 @@
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
const isProduction = import.meta.env.PROD;
|
||||
const prefix = "Invariant failed";
|
||||
|
||||
// Want to use this:
|
||||
// export class RbdInvariant extends Error { }
|
||||
// But it causes babel to bring in a lot of code
|
||||
|
||||
export function RbdInvariant(message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
// $FlowFixMe
|
||||
RbdInvariant.prototype.toString = function toString() {
|
||||
return this.message;
|
||||
};
|
||||
|
||||
// A copy-paste of tiny-invariant but with a custom error type
|
||||
// Throw an error if the condition fails
|
||||
export function invariant(condition, message) {
|
||||
if (condition) {
|
||||
return;
|
||||
}
|
||||
if (isProduction) {
|
||||
// In production we strip the message but still throw
|
||||
throw new RbdInvariant(prefix);
|
||||
} else {
|
||||
// When not in production we allow the message to pass through
|
||||
// *This block will be removed in production builds*
|
||||
throw new RbdInvariant(`${prefix}: ${message || ""}`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/* eslint-disable no-restricted-globals */
|
||||
export function isInteger(value) {
|
||||
if (Number.isInteger) {
|
||||
return Number.isInteger(value);
|
||||
}
|
||||
return typeof value === "number" && isFinite(value) && Math.floor(value) === value;
|
||||
}
|
||||
|
||||
// Using this helper to ensure there are correct flow types
|
||||
// https://github.com/facebook/flow/issues/2221
|
||||
export function values(map) {
|
||||
if (Object.values) {
|
||||
// $FlowFixMe - Object.values currently does not have good flow support
|
||||
return Object.values(map);
|
||||
}
|
||||
return Object.keys(map).map((key) => map[key]);
|
||||
}
|
||||
|
||||
// Could also extend to pass index and list
|
||||
|
||||
// TODO: swap order
|
||||
export function findIndex(list, predicate) {
|
||||
if (list.findIndex) {
|
||||
return list.findIndex(predicate);
|
||||
}
|
||||
|
||||
// Using a for loop so that we can exit early
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
if (predicate(list[i])) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
// Array.prototype.find returns -1 when nothing is found
|
||||
return -1;
|
||||
}
|
||||
|
||||
export function find(list, predicate) {
|
||||
if (list.find) {
|
||||
return list.find(predicate);
|
||||
}
|
||||
const index = findIndex(list, predicate);
|
||||
if (index !== -1) {
|
||||
return list[index];
|
||||
}
|
||||
// Array.prototype.find returns undefined when nothing is found
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Using this rather than Array.from as Array.from adds 2kb to the gzip
|
||||
// document.querySelector actually returns Element[], but flow thinks it is HTMLElement[]
|
||||
// So we downcast the result to Element[]
|
||||
export function toArray(list) {
|
||||
return Array.prototype.slice.call(list);
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
const dragHandleUsageInstructions = `
|
||||
Press space bar to start a drag.
|
||||
When dragging you can use the arrow keys to move the item around and escape to cancel.
|
||||
Some screen readers may require you to be in focus mode or to use your pass through key
|
||||
`;
|
||||
const position = (index) => index + 1;
|
||||
|
||||
// We cannot list what index the Droppable is in automatically as we are not sure how
|
||||
// the Droppable's have been configured
|
||||
const onDragStart = (start) => `
|
||||
You have lifted an item in position ${position(start.source.index)}
|
||||
`;
|
||||
const withLocation = (source, destination) => {
|
||||
const isInHomeList = source.droppableId === destination.droppableId;
|
||||
const startPosition = position(source.index);
|
||||
const endPosition = position(destination.index);
|
||||
if (isInHomeList) {
|
||||
return `
|
||||
You have moved the item from position ${startPosition}
|
||||
to position ${endPosition}
|
||||
`;
|
||||
}
|
||||
return `
|
||||
You have moved the item from position ${startPosition}
|
||||
in list ${source.droppableId}
|
||||
to list ${destination.droppableId}
|
||||
in position ${endPosition}
|
||||
`;
|
||||
};
|
||||
const withCombine = (id, source, combine) => {
|
||||
const inHomeList = source.droppableId === combine.droppableId;
|
||||
if (inHomeList) {
|
||||
return `
|
||||
The item ${id}
|
||||
has been combined with ${combine.draggableId}`;
|
||||
}
|
||||
return `
|
||||
The item ${id}
|
||||
in list ${source.droppableId}
|
||||
has been combined with ${combine.draggableId}
|
||||
in list ${combine.droppableId}
|
||||
`;
|
||||
};
|
||||
const onDragUpdate = (update) => {
|
||||
const location = update.destination;
|
||||
if (location) {
|
||||
return withLocation(update.source, location);
|
||||
}
|
||||
const combine = update.combine;
|
||||
if (combine) {
|
||||
return withCombine(update.draggableId, update.source, combine);
|
||||
}
|
||||
return "You are over an area that cannot be dropped on";
|
||||
};
|
||||
const returnedToStart = (source) => `
|
||||
The item has returned to its starting position
|
||||
of ${position(source.index)}
|
||||
`;
|
||||
const onDragEnd = (result) => {
|
||||
if (result.reason === "CANCEL") {
|
||||
return `
|
||||
Movement cancelled.
|
||||
${returnedToStart(result.source)}
|
||||
`;
|
||||
}
|
||||
const location = result.destination;
|
||||
const combine = result.combine;
|
||||
if (location) {
|
||||
return `
|
||||
You have dropped the item.
|
||||
${withLocation(result.source, location)}
|
||||
`;
|
||||
}
|
||||
if (combine) {
|
||||
return `
|
||||
You have dropped the item.
|
||||
${withCombine(result.draggableId, result.source, combine)}
|
||||
`;
|
||||
}
|
||||
return `
|
||||
The item has been dropped while not over a drop area.
|
||||
${returnedToStart(result.source)}
|
||||
`;
|
||||
};
|
||||
const preset = {
|
||||
dragHandleUsageInstructions,
|
||||
onDragStart,
|
||||
onDragUpdate,
|
||||
onDragEnd
|
||||
};
|
||||
export default preset;
|
||||
@@ -0,0 +1,88 @@
|
||||
export const beforeInitialCapture = (args) => ({
|
||||
type: "BEFORE_INITIAL_CAPTURE",
|
||||
payload: args
|
||||
});
|
||||
export const lift = (args) => ({
|
||||
type: "LIFT",
|
||||
payload: args
|
||||
});
|
||||
export const initialPublish = (args) => ({
|
||||
type: "INITIAL_PUBLISH",
|
||||
payload: args
|
||||
});
|
||||
export const publishWhileDragging = (args) => ({
|
||||
type: "PUBLISH_WHILE_DRAGGING",
|
||||
payload: args
|
||||
});
|
||||
export const collectionStarting = () => ({
|
||||
type: "COLLECTION_STARTING",
|
||||
payload: null
|
||||
});
|
||||
export const updateDroppableScroll = (args) => ({
|
||||
type: "UPDATE_DROPPABLE_SCROLL",
|
||||
payload: args
|
||||
});
|
||||
export const updateDroppableIsEnabled = (args) => ({
|
||||
type: "UPDATE_DROPPABLE_IS_ENABLED",
|
||||
payload: args
|
||||
});
|
||||
export const updateDroppableIsCombineEnabled = (args) => ({
|
||||
type: "UPDATE_DROPPABLE_IS_COMBINE_ENABLED",
|
||||
payload: args
|
||||
});
|
||||
export const move = (args) => ({
|
||||
type: "MOVE",
|
||||
payload: args
|
||||
});
|
||||
export const moveByWindowScroll = (args) => ({
|
||||
type: "MOVE_BY_WINDOW_SCROLL",
|
||||
payload: args
|
||||
});
|
||||
export const updateViewportMaxScroll = (args) => ({
|
||||
type: "UPDATE_VIEWPORT_MAX_SCROLL",
|
||||
payload: args
|
||||
});
|
||||
export const moveUp = () => ({
|
||||
type: "MOVE_UP",
|
||||
payload: null
|
||||
});
|
||||
export const moveDown = () => ({
|
||||
type: "MOVE_DOWN",
|
||||
payload: null
|
||||
});
|
||||
export const moveRight = () => ({
|
||||
type: "MOVE_RIGHT",
|
||||
payload: null
|
||||
});
|
||||
export const moveLeft = () => ({
|
||||
type: "MOVE_LEFT",
|
||||
payload: null
|
||||
});
|
||||
export const flush = () => ({
|
||||
type: "FLUSH",
|
||||
payload: null
|
||||
});
|
||||
export const animateDrop = (args) => ({
|
||||
type: "DROP_ANIMATE",
|
||||
payload: args
|
||||
});
|
||||
export const completeDrop = (args) => ({
|
||||
type: "DROP_COMPLETE",
|
||||
payload: args
|
||||
});
|
||||
export const drop = (args) => ({
|
||||
type: "DROP",
|
||||
payload: args
|
||||
});
|
||||
export const cancel = () =>
|
||||
drop({
|
||||
reason: "CANCEL"
|
||||
});
|
||||
export const dropPending = (args) => ({
|
||||
type: "DROP_PENDING",
|
||||
payload: args
|
||||
});
|
||||
export const dropAnimationFinished = () => ({
|
||||
type: "DROP_ANIMATION_FINISHED",
|
||||
payload: null
|
||||
});
|
||||
@@ -0,0 +1,111 @@
|
||||
import { add, apply, isEqual, origin } from "../position";
|
||||
|
||||
const smallestSigned = apply((value) => {
|
||||
if (value === 0) {
|
||||
return 0;
|
||||
}
|
||||
return value > 0 ? 1 : -1;
|
||||
});
|
||||
// We need to figure out how much of the movement
|
||||
// cannot be done with a scroll
|
||||
export const getOverlap = (() => {
|
||||
const getRemainder = (target, max) => {
|
||||
if (target < 0) {
|
||||
return target;
|
||||
}
|
||||
if (target > max) {
|
||||
return target - max;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
return ({ current, max, change }) => {
|
||||
const targetScroll = add(current, change);
|
||||
const overlap = {
|
||||
x: getRemainder(targetScroll.x, max.x),
|
||||
y: getRemainder(targetScroll.y, max.y)
|
||||
};
|
||||
if (isEqual(overlap, origin)) {
|
||||
return null;
|
||||
}
|
||||
return overlap;
|
||||
};
|
||||
})();
|
||||
export const canPartiallyScroll = ({ max: rawMax, current, change }) => {
|
||||
// It is possible for the max scroll to be greater than the current scroll
|
||||
// when there are scrollbars on the cross axis. We adjust for this by
|
||||
// increasing the max scroll point if needed
|
||||
// This will allow movements backwards even if the current scroll is greater than the max scroll
|
||||
const max = {
|
||||
x: Math.max(current.x, rawMax.x),
|
||||
y: Math.max(current.y, rawMax.y)
|
||||
};
|
||||
|
||||
// Only need to be able to move the smallest amount in the desired direction
|
||||
const smallestChange = smallestSigned(change);
|
||||
const overlap = getOverlap({
|
||||
max,
|
||||
current,
|
||||
change: smallestChange
|
||||
});
|
||||
|
||||
// no overlap at all - we can move there!
|
||||
if (!overlap) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// if there was an x value, but there is no x overlap - then we can scroll on the x!
|
||||
if (smallestChange.x !== 0 && overlap.x === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// if there was an y value, but there is no y overlap - then we can scroll on the y!
|
||||
if (smallestChange.y !== 0 && overlap.y === 0) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
export const canScrollWindow = (viewport, change) =>
|
||||
canPartiallyScroll({
|
||||
current: viewport.scroll.current,
|
||||
max: viewport.scroll.max,
|
||||
change
|
||||
});
|
||||
export const getWindowOverlap = (viewport, change) => {
|
||||
if (!canScrollWindow(viewport, change)) {
|
||||
return null;
|
||||
}
|
||||
const max = viewport.scroll.max;
|
||||
const current = viewport.scroll.current;
|
||||
return getOverlap({
|
||||
current,
|
||||
max,
|
||||
change
|
||||
});
|
||||
};
|
||||
export const canScrollDroppable = (droppable, change) => {
|
||||
const frame = droppable.frame;
|
||||
|
||||
// Cannot scroll when there is no scrollable
|
||||
if (!frame) {
|
||||
return false;
|
||||
}
|
||||
return canPartiallyScroll({
|
||||
current: frame.scroll.current,
|
||||
max: frame.scroll.max,
|
||||
change
|
||||
});
|
||||
};
|
||||
export const getDroppableOverlap = (droppable, change) => {
|
||||
const frame = droppable.frame;
|
||||
if (!frame) {
|
||||
return null;
|
||||
}
|
||||
if (!canScrollDroppable(droppable, change)) {
|
||||
return null;
|
||||
}
|
||||
return getOverlap({
|
||||
current: frame.scroll.current,
|
||||
max: frame.scroll.max,
|
||||
change
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
// Values used to control how the fluid auto scroll feels
|
||||
const config = {
|
||||
// percentage distance from edge of container:
|
||||
startFromPercentage: 0.25,
|
||||
maxScrollAtPercentage: 0.05,
|
||||
// pixels per frame
|
||||
maxPixelScroll: 28,
|
||||
// A function used to ease a percentage value
|
||||
// A simple linear function would be: (percentage) => percentage;
|
||||
// percentage is between 0 and 1
|
||||
// result must be between 0 and 1
|
||||
ease: (percentage) => Math.pow(percentage, 2),
|
||||
durationDampening: {
|
||||
// ms: how long to dampen the speed of an auto scroll from the start of a drag
|
||||
stopDampeningAt: 1200,
|
||||
// ms: when to start accelerating the reduction of duration dampening
|
||||
accelerateAt: 360
|
||||
}
|
||||
};
|
||||
export default config;
|
||||
@@ -0,0 +1,45 @@
|
||||
import memoizeOne from "memoize-one";
|
||||
import { invariant } from "../../../invariant";
|
||||
import isPositionInFrame from "../../visibility/is-position-in-frame";
|
||||
import { toDroppableList } from "../../dimension-structures";
|
||||
import { find } from "../../../native-with-fallback";
|
||||
|
||||
const getScrollableDroppables = memoizeOne((droppables) =>
|
||||
toDroppableList(droppables).filter((droppable) => {
|
||||
// exclude disabled droppables
|
||||
if (!droppable.isEnabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// only want droppables that are scrollable
|
||||
if (!droppable.frame) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
);
|
||||
const getScrollableDroppableOver = (target, droppables) => {
|
||||
const maybe = find(getScrollableDroppables(droppables), (droppable) => {
|
||||
invariant(droppable.frame, "Invalid result");
|
||||
return isPositionInFrame(droppable.frame.pageMarginBox)(target);
|
||||
});
|
||||
return maybe;
|
||||
};
|
||||
const getBestScrollableDroppable = ({ center, destination, droppables }) => {
|
||||
// We need to scroll the best droppable frame we can so that the
|
||||
// placeholder buffer logic works correctly
|
||||
|
||||
if (destination) {
|
||||
const dimension = droppables[destination];
|
||||
if (!dimension.frame) {
|
||||
return null;
|
||||
}
|
||||
return dimension;
|
||||
}
|
||||
|
||||
// 2. If we are not over a droppable - are we over a droppable frame?
|
||||
const dimension = getScrollableDroppableOver(center, droppables);
|
||||
return dimension;
|
||||
};
|
||||
|
||||
export default getBestScrollableDroppable;
|
||||
@@ -0,0 +1,22 @@
|
||||
import getScroll from "./get-scroll";
|
||||
import { canScrollDroppable } from "../can-scroll";
|
||||
|
||||
const getDroppableScrollChange = ({ droppable, subject, center, dragStartTime, shouldUseTimeDampening }) => {
|
||||
// We know this has a closestScrollable
|
||||
const frame = droppable.frame;
|
||||
|
||||
// this should never happen - just being safe
|
||||
if (!frame) {
|
||||
return null;
|
||||
}
|
||||
const scroll = getScroll({
|
||||
dragStartTime,
|
||||
container: frame.pageMarginBox,
|
||||
subject,
|
||||
center,
|
||||
shouldUseTimeDampening
|
||||
});
|
||||
return scroll && canScrollDroppable(droppable, scroll) ? scroll : null;
|
||||
};
|
||||
|
||||
export default getDroppableScrollChange;
|
||||
@@ -0,0 +1,18 @@
|
||||
import { warning } from "../../../dev-warning";
|
||||
|
||||
const getPercentage = ({ startOfRange, endOfRange, current }) => {
|
||||
const range = endOfRange - startOfRange;
|
||||
if (range === 0) {
|
||||
warning(`
|
||||
Detected distance range of 0 in the fluid auto scroller
|
||||
This is unexpected and would cause a divide by 0 issue.
|
||||
Not allowing an auto scroll
|
||||
`);
|
||||
return 0;
|
||||
}
|
||||
const currentInRange = current - startOfRange;
|
||||
|
||||
return currentInRange / range;
|
||||
};
|
||||
|
||||
export default getPercentage;
|
||||
@@ -0,0 +1,23 @@
|
||||
const adjustForSizeLimits = ({ container, subject, proposedScroll }) => {
|
||||
const isTooBigVertically = subject.height > container.height;
|
||||
const isTooBigHorizontally = subject.width > container.width;
|
||||
|
||||
// not too big on any axis
|
||||
if (!isTooBigHorizontally && !isTooBigVertically) {
|
||||
return proposedScroll;
|
||||
}
|
||||
|
||||
// too big on both axis
|
||||
if (isTooBigHorizontally && isTooBigVertically) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Only too big on one axis
|
||||
// Exclude the axis that we cannot scroll on
|
||||
return {
|
||||
x: isTooBigHorizontally ? 0 : proposedScroll.x,
|
||||
y: isTooBigVertically ? 0 : proposedScroll.y
|
||||
};
|
||||
};
|
||||
|
||||
export default adjustForSizeLimits;
|
||||
@@ -0,0 +1,34 @@
|
||||
import getPercentage from "../../get-percentage";
|
||||
import config from "../../config";
|
||||
import minScroll from "./min-scroll";
|
||||
|
||||
const accelerateAt = config.durationDampening.accelerateAt;
|
||||
const stopAt = config.durationDampening.stopDampeningAt;
|
||||
|
||||
const dampenValueByTime = (proposedScroll, dragStartTime) => {
|
||||
const startOfRange = dragStartTime;
|
||||
const endOfRange = stopAt;
|
||||
const now = Date.now();
|
||||
const runTime = now - startOfRange;
|
||||
|
||||
// we have finished the time dampening period
|
||||
if (runTime >= stopAt) {
|
||||
return proposedScroll;
|
||||
}
|
||||
|
||||
// Up to this point we know there is a proposed scroll
|
||||
// but we have not reached our accelerate point
|
||||
// Return the minimum amount of scroll
|
||||
if (runTime < accelerateAt) {
|
||||
return minScroll;
|
||||
}
|
||||
const betweenAccelerateAtAndStopAtPercentage = getPercentage({
|
||||
startOfRange: accelerateAt,
|
||||
endOfRange,
|
||||
current: runTime
|
||||
});
|
||||
const scroll = proposedScroll * config.ease(betweenAccelerateAtAndStopAtPercentage);
|
||||
return Math.ceil(scroll);
|
||||
};
|
||||
|
||||
export default dampenValueByTime;
|
||||
@@ -0,0 +1,15 @@
|
||||
import config from "../../config";
|
||||
|
||||
const getDistanceThresholds = (container, axis) => {
|
||||
const startScrollingFrom = container[axis.size] * config.startFromPercentage;
|
||||
const maxScrollValueAt = container[axis.size] * config.maxScrollAtPercentage;
|
||||
return {
|
||||
startScrollingFrom,
|
||||
maxScrollValueAt
|
||||
};
|
||||
};
|
||||
|
||||
// all in pixels
|
||||
|
||||
// converts the percentages in the config into actual pixel values
|
||||
export default getDistanceThresholds;
|
||||
@@ -0,0 +1,54 @@
|
||||
import getPercentage from "../../get-percentage";
|
||||
import config from "../../config";
|
||||
import minScroll from "./min-scroll";
|
||||
|
||||
const getValueFromDistance = (distanceToEdge, thresholds) => {
|
||||
/*
|
||||
// This function only looks at the distance to one edge
|
||||
// Example: looking at bottom edge
|
||||
|----------------------------------|
|
||||
| |
|
||||
| |
|
||||
| |
|
||||
| |
|
||||
| | => no scroll in this range
|
||||
| |
|
||||
| |
|
||||
| startScrollingFrom (eg 100px) |
|
||||
| |
|
||||
| | => increased scroll value the closer to maxScrollValueAt
|
||||
| maxScrollValueAt (eg 10px) |
|
||||
| | => max scroll value in this range
|
||||
|----------------------------------|
|
||||
*/
|
||||
|
||||
// too far away to auto scroll
|
||||
if (distanceToEdge > thresholds.startScrollingFrom) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// use max speed when on or over boundary
|
||||
if (distanceToEdge <= thresholds.maxScrollValueAt) {
|
||||
return config.maxPixelScroll;
|
||||
}
|
||||
|
||||
// when just going on the boundary return the minimum integer
|
||||
if (distanceToEdge === thresholds.startScrollingFrom) {
|
||||
return minScroll;
|
||||
}
|
||||
|
||||
// to get the % past startScrollingFrom we will calculate
|
||||
// the % the value is from maxScrollValueAt and then invert it
|
||||
const percentageFromMaxScrollValueAt = getPercentage({
|
||||
startOfRange: thresholds.maxScrollValueAt,
|
||||
endOfRange: thresholds.startScrollingFrom,
|
||||
current: distanceToEdge
|
||||
});
|
||||
const percentageFromStartScrollingFrom = 1 - percentageFromMaxScrollValueAt;
|
||||
const scroll = config.maxPixelScroll * config.ease(percentageFromStartScrollingFrom);
|
||||
|
||||
// scroll will always be a positive integer
|
||||
return Math.ceil(scroll);
|
||||
};
|
||||
|
||||
export default getValueFromDistance;
|
||||
@@ -0,0 +1,27 @@
|
||||
import getValueFromDistance from "./get-value-from-distance";
|
||||
import dampenValueByTime from "./dampen-value-by-time";
|
||||
import minScroll from "./min-scroll";
|
||||
|
||||
const getValue = ({ distanceToEdge, thresholds, dragStartTime, shouldUseTimeDampening }) => {
|
||||
const scroll = getValueFromDistance(distanceToEdge, thresholds);
|
||||
|
||||
// not enough distance to trigger a minimum scroll
|
||||
// we can bail here
|
||||
if (scroll === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Dampen an auto scroll speed based on duration of drag
|
||||
|
||||
if (!shouldUseTimeDampening) {
|
||||
return scroll;
|
||||
}
|
||||
|
||||
// Once we know an auto scroll should occur based on distance,
|
||||
// we must let at least 1px through to trigger a scroll event an
|
||||
// another auto scroll call
|
||||
|
||||
return Math.max(dampenValueByTime(scroll, dragStartTime), minScroll);
|
||||
};
|
||||
|
||||
export default getValue;
|
||||
@@ -0,0 +1,26 @@
|
||||
import getDistanceThresholds from "./get-distance-thresholds";
|
||||
import getValue from "./get-value";
|
||||
|
||||
const getScrollOnAxis = ({ container, distanceToEdges, dragStartTime, axis, shouldUseTimeDampening }) => {
|
||||
const thresholds = getDistanceThresholds(container, axis);
|
||||
const isCloserToEnd = distanceToEdges[axis.end] < distanceToEdges[axis.start];
|
||||
if (isCloserToEnd) {
|
||||
return getValue({
|
||||
distanceToEdge: distanceToEdges[axis.end],
|
||||
thresholds,
|
||||
dragStartTime,
|
||||
shouldUseTimeDampening
|
||||
});
|
||||
}
|
||||
return (
|
||||
-1 *
|
||||
getValue({
|
||||
distanceToEdge: distanceToEdges[axis.start],
|
||||
thresholds,
|
||||
dragStartTime,
|
||||
shouldUseTimeDampening
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
export default getScrollOnAxis;
|
||||
@@ -0,0 +1,4 @@
|
||||
// A scroll event will only be triggered when there is a value of at least 1px change
|
||||
const minScroll = 1;
|
||||
|
||||
export default minScroll;
|
||||
@@ -0,0 +1,62 @@
|
||||
import { apply, isEqual, origin } from "../../../position";
|
||||
import getScrollOnAxis from "./get-scroll-on-axis";
|
||||
import adjustForSizeLimits from "./adjust-for-size-limits";
|
||||
import { horizontal, vertical } from "../../../axis";
|
||||
|
||||
// will replace -0 and replace with +0
|
||||
const clean = apply((value) => (value === 0 ? 0 : value));
|
||||
|
||||
const getScroll = ({ dragStartTime, container, subject, center, shouldUseTimeDampening }) => {
|
||||
// get distance to each edge
|
||||
const distanceToEdges = {
|
||||
top: center.y - container.top,
|
||||
right: container.right - center.x,
|
||||
bottom: container.bottom - center.y,
|
||||
left: center.x - container.left
|
||||
};
|
||||
|
||||
// 1. Figure out which x,y values are the best target
|
||||
// 2. Can the container scroll in that direction at all?
|
||||
// If no for both directions, then return null
|
||||
// 3. Is the center close enough to a edge to start a drag?
|
||||
// 4. Based on the distance, calculate the speed at which a scroll should occur
|
||||
// The lower distance value the faster the scroll should be.
|
||||
// Maximum speed value should be hit before the distance is 0
|
||||
// Negative values to not continue to increase the speed
|
||||
const y = getScrollOnAxis({
|
||||
container,
|
||||
distanceToEdges,
|
||||
dragStartTime,
|
||||
axis: vertical,
|
||||
shouldUseTimeDampening
|
||||
});
|
||||
const x = getScrollOnAxis({
|
||||
container,
|
||||
distanceToEdges,
|
||||
dragStartTime,
|
||||
axis: horizontal,
|
||||
shouldUseTimeDampening
|
||||
});
|
||||
const required = clean({
|
||||
x,
|
||||
y
|
||||
});
|
||||
|
||||
// nothing required
|
||||
if (isEqual(required, origin)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// need to not scroll in a direction that we are too big to scroll in
|
||||
const limited = adjustForSizeLimits({
|
||||
container,
|
||||
subject,
|
||||
proposedScroll: required
|
||||
});
|
||||
if (!limited) {
|
||||
return null;
|
||||
}
|
||||
return isEqual(limited, origin) ? null : limited;
|
||||
};
|
||||
|
||||
export default getScroll;
|
||||
@@ -0,0 +1,15 @@
|
||||
import getScroll from "./get-scroll";
|
||||
import { canScrollWindow } from "../can-scroll";
|
||||
|
||||
const getWindowScrollChange = ({ viewport, subject, center, dragStartTime, shouldUseTimeDampening }) => {
|
||||
const scroll = getScroll({
|
||||
dragStartTime,
|
||||
container: viewport.frame,
|
||||
subject,
|
||||
center,
|
||||
shouldUseTimeDampening
|
||||
});
|
||||
return scroll && canScrollWindow(viewport, scroll) ? scroll : null;
|
||||
};
|
||||
|
||||
export default getWindowScrollChange;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user