Compare commits

..

91 Commits

Author SHA1 Message Date
Dave Richer
97693fbcff - Add Prop Types to ProductFruitsWrapper.jsx
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-07-19 16:01:40 -04:00
Dave Richer
5e94b1a71e - Improve product fruits wrapper for extra checks
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-07-19 15:50:09 -04:00
Dave Richer
3dd154de79 - Improve product fruits wrapper for extra checks
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-07-19 13:23:43 -04:00
Dave Richer
72a2366abe - Improve product fruits wrapper for extra checks
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-07-19 13:18:10 -04:00
Allan Carr
36f517e8e1 Merged in feature/IO-2845-Payment-by-Date-Grouped-by-Payment-Type (pull request #1525)
IO-2845 Payments Grouped by Payment Type

Approved-by: Dave Richer
2024-07-19 17:16:26 +00:00
Allan Carr
981fb57d36 Merged in feature/IO-2847-Employee-Rate-Filter (pull request #1526)
IO-2847 Employee Rate Filter

Approved-by: Dave Richer
2024-07-19 17:15:04 +00:00
Allan Carr
a059c2b5a8 IO-2847 Employee Rate Filter
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-07-19 08:41:12 -07:00
Allan Carr
207bb39672 IO-2845 Payments Grouped by Payment Type
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-07-17 14:15:38 -07:00
Dave Richer
16e34e4ed9 Merged in bugfix/productfruits (pull request #1523)
- misc updates / clear stage
2024-07-17 16:30:53 +00:00
Dave Richer
cd04f2b2b2 Merged in bugfix/productfruits (pull request #1524)
- misc updates / clear stage
2024-07-17 16:30:40 +00:00
Dave Richer
26e164b4d1 - misc updates / clear stage
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-07-17 12:22:31 -04:00
Allan Carr
ae6b2fe4f5 Merged in feature/IO-2843-QBO_USA-Switch-for-IO (pull request #1521)
IO-2843 State Tax for QBO_USA and Region CA_
2024-07-15 23:53:59 +00:00
Allan Carr
4ca686126a IO-2843 State Tax for QBO_USA and Region CA_
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-07-15 16:53:48 -07:00
Allan Carr
c254a8abfe Merged in release/2024-07-12 (pull request #1520)
Release/2024 07 12
2024-07-12 20:15:53 +00:00
Allan Carr
f864e40a90 Merged in feature/IO-2836-Charts-Route (pull request #1518)
IO-2836 Charts Route

Approved-by: Dave Richer
2024-07-11 22:17:01 +00:00
Allan Carr
802f70dde8 IO-2836 Charts Route
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-07-11 11:25:27 -07:00
Allan Carr
6cb4fd6b93 Merged in feature/IO-2839-DMS-Allocation-Labels (pull request #1514)
IO-2839 DMS Allocation Labels

Approved-by: Dave Richer
2024-07-11 17:10:49 +00:00
Allan Carr
70ca6edcb2 Merged in feature/IO-2840-Area-of-Damage-Correction (pull request #1515)
IO-2840 Correct for Area of Damage that is only 1 Charater intead of 2

Approved-by: Dave Richer
2024-07-11 17:09:41 +00:00
Allan Carr
9dcc861740 Merged in IO-2841-Non-Parts-in-jobline_status (pull request #1516)
IO-2841 Non-Parts listed in Jobline_Status

Approved-by: Dave Richer
2024-07-11 17:09:04 +00:00
Allan Carr
fecb7fb24b Merged in feature/IO-2837-Job-Card-Add-to-Scoreboard (pull request #1517)
IO-2837 Add to Scoreboard from Job Drawer

Approved-by: Dave Richer
2024-07-11 17:06:37 +00:00
Allan Carr
c5b94db8af Merged in feature/IO-2520-Kaizen-Data-Pump (pull request #1513)
IO-2520 Kaizen Data Pump

Approved-by: Dave Richer
2024-07-11 17:06:23 +00:00
Allan Carr
6690c9c692 IO-2841 Non-Parts listed in Jobline_Status
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-07-11 09:15:18 -07:00
Allan Carr
506edcb3c6 IO-2837 Add to Scoreboard from Job Drawer
Query restricted necessary lines for proper labour calculations

Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-07-10 16:45:35 -07:00
Allan Carr
c45e53e38b IO-2840 Correct for Area of Damage that is only 1 Charater intead of 2
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-07-10 16:00:47 -07:00
Allan Carr
d8d8a4701e IO-2839 DMS Allocation Labels
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-07-10 14:54:04 -07:00
Allan Carr
970c4ee9e1 IO-2520 Kaizen Data Pump
Additional fields requested

Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-07-10 10:33:53 -07:00
Allan Carr
c2386f43ef Merge branch 'master-AIO' into feature/IO-2520-Kaizen-Data-Pump
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-07-10 10:32:21 -07:00
Allan Carr
0fd3e75862 Merged in feature/IO-2835-CDK-Calculate-Allocations-Route (pull request #1511)
IO-2835 CDK Calculate Allocation Route

Approved-by: Dave Richer
2024-07-09 15:54:21 +00:00
Allan Carr
fcd3234fd9 Merged in feature/IO-2838-JobCloseRoGuardSublet (pull request #1512)
IO-2838 JobCloseRoGuardSublet

Approved-by: Dave Richer
2024-07-09 15:52:45 +00:00
Allan Carr
2afa810e6c IO-2838 JobCloseRoGuardSublet
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-07-08 18:15:47 -07:00
Allan Carr
ff33b924b2 IO-2835 CDK Calculate Allocation Route
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-07-08 18:07:33 -07:00
Dave Richer
84bb25985b Merged in release/2024-06-28 (pull request #1510)
Release - 2024/06/28

Approved-by: Allan Carr
2024-06-28 23:02:23 +00:00
Allan Carr
e38a58550f Merged in feature/IO-2832-Purchases-by-RO-Date (pull request #1508)
IO-2832 Purchases by RO - Invoice Date bound

Approved-by: Dave Richer
2024-06-28 15:33:02 +00:00
Allan Carr
25ef4c6228 IO-2832 Purchases by RO - Invoice Date bound
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-06-27 18:57:43 -07:00
Allan Carr
277fbeebc8 Merged in release/2024-06-28 (pull request #1506)
Release/2024 06 28

Approved-by: Dave Richer
2024-06-26 18:09:45 +00:00
Allan Carr
399df78957 Merged in feature/IO-2793-QBO-State-Tax-Null (pull request #1504)
IO-2793 Insure Part Tax Type Exists
2024-06-26 16:31:40 +00:00
Allan Carr
294325343b IO-2793 Insure Part Tax Type Exists
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-06-26 09:32:04 -07:00
Allan Carr
ed17eec948 Merged in feature/IO-2829-Multiple-Towing-Lines (pull request #1500)
IO-2829 Multiple Towing Lines

Approved-by: Dave Richer
2024-06-26 00:51:52 +00:00
Allan Carr
f87c95079c Merged in feature/IO-2830-Bill-Line-Select-Search-Component (pull request #1501)
IO-2830 Bill Line Select Search Component

Approved-by: Dave Richer
2024-06-26 00:50:58 +00:00
Allan Carr
327149ffc9 IO-2830 Bill Line Select Search Component
remove unused variable

Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-06-25 13:32:01 -07:00
Allan Carr
f81b21b933 IO-2829 Multiple Towing Lines
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-06-25 13:26:56 -07:00
Allan Carr
a559b56983 Merged in feature/IO-2520-Kaizen-Data-Pump (pull request #1499)
IO-2520 Kaizen Data Pump

Approved-by: Dave Richer
2024-06-25 19:22:55 +00:00
Allan Carr
6a9030b653 IO-2520 Kaizen Data Pump
Add in Repair Line Details, Time Ticket Details, Void Date

Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-06-25 11:57:38 -07:00
Dave Richer
fc7da187f4 Merged in release/2024-06-21 (pull request #1498)
Release/2024 06 21
2024-06-21 22:47:20 +00:00
Allan Carr
f593f83ec1 Merged in feature/IO-2820-Adjust-to-Bottom-Line-State-Tax (pull request #1496)
IO-2820 Adjustment to Bottom Line
2024-06-21 22:32:03 +00:00
Allan Carr
5752f123ac IO-2820 Adjustment to Bottom Line
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-06-21 15:32:55 -07:00
Allan Carr
6396d68584 Merged in feature/IO-2828-Deliver-Checklist-Delete (pull request #1494)
IO-2828 Add InstanceManager and correct delete button for delivery checklist
2024-06-21 18:14:38 +00:00
Allan Carr
bfd29f25dd IO-2828 Add InstanceManager and correct delete button for delivery checklist
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-06-21 11:12:10 -07:00
Allan Carr
e75e35e4ee Merged in feature/IO-2793-State-Tax-Null-QBO (pull request #1493)
Feature/IO-2793 State Tax Null QBO
2024-06-20 21:39:24 +00:00
Allan Carr
d7ddbf7e8d IO-2793 Change Additional Costs tax item
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-06-20 14:15:10 -07:00
Allan Carr
1b0198af63 Merged in feature/IO-2793-State-Tax-Null-QBO (pull request #1491)
IO-2793 Change Additional Costs tax item
2024-06-20 21:14:35 +00:00
Allan Carr
ace16ba873 IO-2793 Adjustment for parts
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-06-20 13:46:54 -07:00
Allan Carr
f57a4bd948 Merged in feature/IO-2793-State-Tax-Null-QBO (pull request #1489)
IO-2793 Adjustment for parts
2024-06-20 20:45:44 +00:00
Allan Carr
fef4393f9c Merged in feature/IO-2795-Delivery-Checklist-Option (pull request #1487)
IO-2795 Delivery Date required on Delivery Checklist

Approved-by: Dave Richer
2024-06-20 18:43:20 +00:00
Allan Carr
47adb6d40a IO-2795 Delivery Date required on Delivery Checklist
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-06-20 11:26:05 -07:00
Allan Carr
4940b10910 IO-2793 Adjustmnet for OP14
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-06-20 08:30:58 -07:00
Allan Carr
9c8e241ef7 Merged in feature/IO-2793-State-Tax-Null-QBO (pull request #1486)
IO-2793 Adjustmnet for OP14
2024-06-20 15:30:14 +00:00
Allan Carr
c3d6b98c89 Merged in feature/IO-2820-Adjust-to-Bottom-Line-State-Tax (pull request #1483)
IO-2820 State Tax & Adjustment to Bottom Line

Approved-by: Dave Richer
2024-06-18 20:39:40 +00:00
Allan Carr
4b49654083 Merged in feature/IO-2814-Job-Costing-Correction (pull request #1484)
IO-2814 Job Costing Correction for OP14 Duplication

Approved-by: Dave Richer
2024-06-18 20:39:16 +00:00
Allan Carr
78223078f4 Merged in feature/IO-2816-Unsaved-Changes-Manual-Creation-of-Job (pull request #1485)
IO-2816 Unsaved Changes on Manual Job Creation

Approved-by: Dave Richer
2024-06-18 20:39:03 +00:00
Allan Carr
ca4b78d44c IO-2816 Unsaved Changes on Manual Job Creation
Correct Hardcoded button labels

Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-06-18 13:16:58 -07:00
Allan Carr
d7bfc789e2 IO-2814 Job Costing Correction for OP14 Duplication
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-06-18 08:44:35 -07:00
Allan Carr
b54d0ed62a Merge branch 'master-AIO' into feature/IO-2814-Job-Costing-Correction
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-06-18 08:43:51 -07:00
Allan Carr
8386443cb0 IO-2820 State Tax & Adjustment to Bottom Line
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-06-17 10:13:36 -07:00
Dave Richer
7959dc67ce Merged in release/AIO/2024-06-07 (pull request #1482)
Release/AIO/2024 06 07
2024-06-11 00:38:31 +00:00
Allan Carr
0d2cdec75c IO-2793 Correction for Sublet Part Tax
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-06-10 14:42:21 -07:00
Allan Carr
a4116b6c28 Merged in feature/IO-2793-State-Tax-Null-QBO (pull request #1480)
IO-2793 Correction for Sublet Part Tax
2024-06-10 21:41:17 +00:00
Allan Carr
269ef25ece IO-2793 Better tax handling
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-06-10 14:14:15 -07:00
Allan Carr
575fbd5357 Merged in feature/IO-2793-State-Tax-Null-QBO (pull request #1478)
IO-2793 Better tax handling
2024-06-10 21:13:20 +00:00
Allan Carr
2ad887fb82 Merged in feature/IO-2793-State-Tax-Null-QBO (pull request #1476)
IO-2793 Correct passed Variable
2024-06-10 18:25:27 +00:00
Allan Carr
40a1a86f72 IO-2793 Correct passed Variable
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-06-10 11:25:06 -07:00
Allan Carr
95d43d936c Merged in feature/IO-2793-State-Tax-Null-QBO (pull request #1474)
IO-2793 Correct variables
2024-06-10 18:03:12 +00:00
Allan Carr
9f56568680 IO-2793 Correct variables
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-06-10 10:58:01 -07:00
Allan Carr
3428940c72 Merged in feature/IO-2793-State-Tax-Null-QBO (pull request #1472)
IO-2793 Change to function for better clarity
2024-06-10 17:49:06 +00:00
Allan Carr
ea604a5e64 IO-2793 Change to function for better clarity
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-06-10 09:59:45 -07:00
Allan Carr
5b76473cbc IO-2793 Correct Parts Side for Taxes
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-06-07 15:59:51 -07:00
Allan Carr
db5359e086 Merged in feature/IO-2793-State-Tax-Null-QBO (pull request #1470)
Feature/IO-2793 State Tax Null QBO
2024-06-07 22:58:58 +00:00
Allan Carr
35046f11c2 IO-2793 Add Comment to see output for testing
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-06-07 14:31:33 -07:00
Allan Carr
f4c4005a2a IO-2814 Remove Comment as api.test.romeonline.io doesn't update w/ CICD
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-06-07 12:16:59 -07:00
Allan Carr
69d8d27ad3 Merged in feature/IO-2814-Job-Costing-Correction (pull request #1467)
IO-2814 Remove Comment as api.test.romeonline.io doesn't update w/ CICD
2024-06-07 19:16:01 +00:00
Allan Carr
755acd24f0 IO-2814 Correct Parts Price and add Console Log
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-06-07 11:40:39 -07:00
Allan Carr
079d6cfee6 Merged in feature/IO-2814-Job-Costing-Correction (pull request #1465)
IO-2814 Correct Parts Price and add Console Log
2024-06-07 18:39:43 +00:00
Allan Carr
3e3b3c269a Merged in feature/IO-2814-Job-Costing-Correction (pull request #1462)
IO-2814 Job Cost Correction
2024-06-07 03:18:49 +00:00
Allan Carr
39f1af7d4b Merged in feature/IO-2793-State-Tax-Null-QBO (pull request #1463)
IO-2793 State Tax Null QBO
2024-06-07 03:18:40 +00:00
Allan Carr
1ea4d616d7 IO-2793 State Tax Null QBO
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-06-06 15:00:58 -07:00
Allan Carr
fdf0ecf6f6 IO-2814 Job Cost Correction
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-06-06 14:51:44 -07:00
Allan Carr
e5f7285253 Merge branch 'feature/IO-2793-State-Tax-Null-QBO' into release/AIO/2024-06-07
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>

# Conflicts:
#	server/accounting/qb-receivables-lines.js
2024-06-03 17:28:18 -07:00
Allan Carr
e46c304f7c IO-2793 State Tax to QBO refactor
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-06-03 17:23:33 -07:00
Dave Richer
1aa570db90 Merged in hotfix/revert-tax-import-code (pull request #1459)
revert previous change due to problems with tax import.
2024-06-03 22:04:31 +00:00
Allan Carr
e1417b03f9 Merged in feature/IO-2801-os-loader-bills-data (pull request #1456)
IO-2801 os-loader bills data

Approved-by: Dave Richer
2024-06-03 18:59:08 +00:00
Allan Carr
7b99de8046 IO-2801 os-loader bills data
add in job object to bills in opensearch for ro_number to be searchable again

Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-06-03 11:50:44 -07:00
108 changed files with 20479 additions and 173673 deletions

View File

@@ -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.jsx --exclude build templates
npx deadfile ./src/index.js --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

View File

@@ -4,7 +4,7 @@ Clone Repository for:
{
"name": "node-webhook-scripts",
"version": "1.0.0",
"main": "index.jsx",
"main": "index.js",
"dependencies": {
"express": "^4.16.4"
},

View File

@@ -11,7 +11,7 @@ module.exports = {
{
name: "Bitbucket Webhook",
script: "./webhook/index.jsx",
script: "./webhook/index.js",
env: {
NODE_ENV: "production"
}

View File

@@ -1,7 +1,7 @@
VITE_APP_GRAPHQL_ENDPOINT=https://db.dev.bodyshop.app/v1/graphql
VITE_APP_GRAPHQL_ENDPOINT_WS=wss://db.dev.bodyshop.app/v1/graphql
VITE_APP_GA_CODE=231099835
VITE_APP_FIREBASE_CONFIG={ "apiKey": "AIzaSyAuLQR9SV5LsVxjU8wh9hvFLdhcAHU6cxE", "authDomain": "rome-prod-1.firebaseapp.com", "projectId": "rome-prod-1", "storageBucket": "rome-prod-1.appspot.com", "messagingSenderId": "147786367145", "appId": "1:147786367145:web:9d4cba68071c3f29a8a9b8", "measurementId": "G-G8Z9DRHTZS"}
VITE_APP_FIREBASE_CONFIG={"apiKey":"AIzaSyDPLT8GiDHDR1R4nI66Qi0BY1aYviDPioc","authDomain":"imex-dev.firebaseapp.com","databaseURL":"https://imex-dev.firebaseio.com","projectId":"imex-dev","storageBucket":"imex-dev.appspot.com","messagingSenderId":"759548147434","appId":"1:759548147434:web:e8239868a48ceb36700993","measurementId":"G-K5XRBVVB4S"}
VITE_APP_CLOUDINARY_ENDPOINT_API=https://api.cloudinary.com/v1_1/io-test
VITE_APP_CLOUDINARY_ENDPOINT=https://res.cloudinary.com/io-test
VITE_APP_CLOUDINARY_API_KEY=957865933348715

53
client/craco.config.js Normal file
View File

@@ -0,0 +1,53 @@
// 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"
};

View File

@@ -1,6 +1,6 @@
/// <reference types="cypress" />
// ***********************************************************
// This example plugins/index.jsx can be used to load plugins
// This example plugins/index.js 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.

View File

@@ -1,5 +1,5 @@
// ***********************************************************
// This example support/index.jsx is processed and
// This example support/index.js is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and

22220
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,86 +2,90 @@
"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/pro-layout": "^7.19.7",
"@ant-design/compatible": "^5.1.2",
"@ant-design/pro-layout": "^7.17.16",
"@apollo/client": "^3.8.10",
"@emotion/is-prop-valid": "^1.2.2",
"@fingerprintjs/fingerprintjs": "^4.3.0",
"@asseinfo/react-kanban": "^2.2.0",
"@fingerprintjs/fingerprintjs": "^4.2.2",
"@jsreport/browser-client": "^3.1.0",
"@reduxjs/toolkit": "^2.2.5",
"@sentry/cli": "^2.31.2",
"@sentry/react": "^7.114.0",
"@splitsoftware/splitio-react": "^1.12.0",
"@reduxjs/toolkit": "^2.2.1",
"@sentry/cli": "^2.28.6",
"@sentry/react": "^7.104.0",
"@splitsoftware/splitio-react": "^1.11.0",
"@tanem/react-nprogress": "^5.0.51",
"@vitejs/plugin-react": "^4.2.1",
"antd": "^5.17.4",
"antd": "^5.15.3",
"apollo-link-logger": "^2.0.1",
"apollo-link-sentry": "^3.3.0",
"autosize": "^6.0.1",
"axios": "^1.6.8",
"classnames": "^2.5.1",
"dayjs": "^1.11.11",
"axios": "^1.6.7",
"dayjs": "^1.11.10",
"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.12.2",
"firebase": "^10.8.1",
"graphql": "^16.6.0",
"i18next": "^23.11.5",
"i18next-browser-languagedetector": "^7.2.1",
"immutability-helper": "^3.1.1",
"libphonenumber-js": "^1.11.2",
"logrocket": "^8.1.0",
"markerjs2": "^2.32.1",
"normalize-url": "^8.0.1",
"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",
"prop-types": "^15.8.1",
"query-string": "^9.0.0",
"react": "^18.3.1",
"react-big-calendar": "^1.12.2",
"react": "^18.2.0",
"react-big-calendar": "^1.11.0",
"react-color": "^2.19.3",
"react-cookie": "^7.1.4",
"react-dom": "^18.3.1",
"react-cookie": "^7.1.0",
"react-dom": "^18.2.0",
"react-drag-listview": "^2.0.0",
"react-grid-gallery": "^1.0.1",
"react-grid-gallery": "^1.0.0",
"react-grid-layout": "1.3.4",
"react-i18next": "^14.1.2",
"react-icons": "^5.2.1",
"react-i18next": "^14.0.5",
"react-icons": "^5.0.1",
"react-image-lightbox": "^5.1.4",
"react-joyride": "^2.8.2",
"react-joyride": "^2.7.4",
"react-markdown": "^9.0.1",
"react-number-format": "^5.3.4",
"react-popopo": "^2.1.9",
"react-number-format": "^5.3.3",
"react-product-fruits": "^2.2.6",
"react-redux": "^9.1.2",
"react-redux": "^9.1.0",
"react-resizable": "^3.0.5",
"react-router-dom": "^6.23.1",
"react-router-dom": "^6.22.2",
"react-scripts": "^5.0.1",
"react-sticky": "^6.0.3",
"react-virtualized": "^9.22.5",
"recharts": "^2.12.7",
"recharts": "^2.12.2",
"redux": "^5.0.1",
"redux-actions": "^3.0.0",
"redux-persist": "^6.0.0",
"redux-saga": "^1.3.0",
"redux-state-sync": "^3.1.4",
"reselect": "^5.1.0",
"sass": "^1.77.2",
"socket.io-client": "^4.7.5",
"styled-components": "^6.1.11",
"sass": "^1.71.1",
"socket.io-client": "^4.7.4",
"styled-components": "^6.1.8",
"subscriptions-transport-ws": "^0.11.0",
"terser-webpack-plugin": "^5.3.10",
"userpilot": "^1.3.1",
"vite-plugin-ejs": "^1.7.0",
"web-vitals": "^3.5.2"
"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"
},
"scripts": {
"analyze": "source-map-explorer 'build/static/js/*.js'",
"start": "vite",
"build": "dotenvx run --env-file=.env.development.imex -- vite build",
"build": "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",
@@ -124,30 +128,32 @@
},
"devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@babel/preset-react": "^7.24.6",
"@dotenvx/dotenvx": "^0.44.1",
"@babel/preset-react": "^7.23.3",
"@dotenvx/dotenvx": "^0.15.4",
"@emotion/babel-plugin": "^11.11.0",
"@emotion/react": "^11.11.4",
"@sentry/webpack-plugin": "^2.16.1",
"@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",
"browserslist-to-esbuild": "^2.1.1",
"cross-env": "^7.0.3",
"cypress": "^13.9.0",
"cypress": "^13.6.6",
"eslint": "^8.57.0",
"eslint-config-react-app": "^7.0.1",
"eslint-plugin-cypress": "^2.15.1",
"memfs": "^4.9.2",
"memfs": "^4.6.0",
"os-browserify": "^0.3.0",
"react-error-overlay": "6.0.11",
"redux-logger": "^3.0.6",
"source-map-explorer": "^2.5.3",
"vite": "^5.2.11",
"vite": "^5.0.11",
"vite-plugin-babel": "^1.2.0",
"vite-plugin-eslint": "^1.8.1",
"vite-plugin-legacy": "^2.1.0",
"vite-plugin-node-polyfills": "^0.22.0",
"vite-plugin-pwa": "^0.20.0",
"vite-plugin-node-polyfills": "^0.19.0",
"vite-plugin-pwa": "^0.19.0",
"vite-plugin-style-import": "^2.0.0"
}
}

View File

@@ -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.jsx')
var mylib = require('./lib/index.js')
var stubs = require('stubs')
// make it a noop

View File

@@ -16567,7 +16567,7 @@ even more slower.
## Benchmarks
```bash
$ node benchmarks/index.jsx
$ node benchmarks/index.js
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)

View File

@@ -7,9 +7,7 @@ import { connect } from "react-redux";
import { Route, Routes } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import DocumentEditorContainer from "../components/document-editor/document-editor.container";
import ErrorBoundary from "../components/error-boundary/error-boundary.component";
//Component Imports
import ErrorBoundary from "../components/error-boundary/error-boundary.component"; // Component Imports
import LoadingSpinner from "../components/loading-spinner/loading-spinner.component";
import DisclaimerPage from "../pages/disclaimer/disclaimer.page";
import LandingPage from "../pages/landing/landing.page";
@@ -23,20 +21,21 @@ import "./App.styles.scss";
import handleBeta from "../utils/betaHandler";
import Eula from "../components/eula/eula.component";
import InstanceRenderMgr from "../utils/instanceRenderMgr";
import { ProductFruits } from "react-product-fruits";
import ProductFruitsWrapper from "./ProductFruitsWrapper.jsx";
const ResetPassword = lazy(() => import("../pages/reset-password/reset-password.component"));
const ManagePage = lazy(() => import("../pages/manage/manage.page.container"));
const SignInPage = lazy(() => import("../pages/sign-in/sign-in.page"));
const CsiPage = lazy(() => import("../pages/csi/csi.container.page"));
const MobilePaymentContainer = lazy(() => import("../pages/mobile-payment/mobile-payment.container"));
const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser,
online: selectOnline,
bodyshop: selectBodyshop,
currentEula: selectCurrentEula
});
const mapDispatchToProps = (dispatch) => ({
checkUserSession: () => dispatch(checkUserSession()),
setOnline: (isOnline) => dispatch(setOnline(isOnline))
@@ -60,11 +59,11 @@ export function App({ bodyshop, checkUserSession, currentUser, online, setOnline
// Associate event listeners, memoize to prevent multiple listeners being added
useEffect(() => {
const offlineListener = (e) => {
const offlineListener = () => {
setOnline(false);
};
const onlineListener = (e) => {
const onlineListener = () => {
setOnline(true);
};
@@ -98,7 +97,7 @@ export function App({ bodyshop, checkUserSession, currentUser, online, setOnline
InstanceRenderMgr({
imex: "gvfvfw/bodyshopapp",
rome: "rome-online/rome-online",
promanager: "" //TODO:AIO Add in log rocket for promanager instances.
promanager: "" // TODO: AIO Add in log rocket for promanager instances.
})
);
}
@@ -111,24 +110,20 @@ export function App({ bodyshop, checkUserSession, currentUser, online, setOnline
handleBeta();
if (!online)
if (!online) {
return (
<Result
status="warning"
title={t("general.labels.nointernet")}
subTitle={t("general.labels.nointernet_sub")}
extra={
<Button
type="primary"
onClick={() => {
window.location.reload();
}}
>
<Button type="primary" onClick={() => window.location.reload()}>
{t("general.actions.refresh")}
</Button>
}
/>
);
}
if (currentEula && !currentUser.eulaIsAccepted) {
return <Eula />;
@@ -147,18 +142,13 @@ export function App({ bodyshop, checkUserSession, currentUser, online, setOnline
/>
}
>
<ProductFruits
<ProductFruitsWrapper
currentUser={currentUser}
workspaceCode={InstanceRenderMgr({
imex: null,
rome: "9BkbEseqNqxw8jUH",
promanager: "aoJoEifvezYI0Z0P"
})}
debug
language="en"
user={{
email: currentUser.email,
username: currentUser.email
}}
/>
<Routes>

View File

@@ -0,0 +1,32 @@
import React from "react";
import { ProductFruits } from "react-product-fruits";
import PropTypes from "prop-types";
const ProductFruitsWrapper = React.memo(({ currentUser, workspaceCode }) => {
return (
workspaceCode &&
currentUser?.authorized === true &&
currentUser?.email && (
<ProductFruits
lifeCycle="unmount"
workspaceCode={workspaceCode}
debug
language="en"
user={{
email: currentUser.email,
username: currentUser.email
}}
/>
)
);
});
export default ProductFruitsWrapper;
ProductFruitsWrapper.propTypes = {
currentUser: PropTypes.shape({
authorized: PropTypes.bool,
email: PropTypes.string
}).isRequired,
workspaceCode: PropTypes.string.isRequired
};

View File

@@ -4,7 +4,6 @@ import { useTranslation } from "react-i18next";
import InstanceRenderMgr from "../../utils/instanceRenderMgr";
//To be used as a form element only.
const { Option } = Select;
const BillLineSearchSelect = ({ options, disabled, allowRemoved, ...restProps }, ref) => {
const { t } = useTranslation();

View File

@@ -9,6 +9,7 @@ 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);
@@ -142,7 +143,7 @@ export default function JobLifecycleDashboardComponent({ data, bodyshop, ...card
>
<div>
{lifecycleData.summations.map((key) => (
<Tag key={key.status} color={key.color} style={{ width: "13vh", padding: "4px", margin: "4px" }}>
<Tag 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}`}
@@ -164,7 +165,6 @@ 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>

View File

@@ -89,6 +89,8 @@ 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 }}>

View File

@@ -14,7 +14,6 @@ import {
Typography
} from "antd";
import Dinero from "dinero.js";
import dayjs from "../../utils/day";
import React from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
@@ -22,6 +21,7 @@ import { createStructuredSelector } from "reselect";
import { determineDmsType } from "../../pages/dms/dms.container";
import { selectBodyshop } from "../../redux/user/user.selectors";
import i18n from "../../translations/i18n";
import dayjs from "../../utils/day";
import DmsCdkMakes from "../dms-cdk-makes/dms-cdk-makes.component";
import DmsCdkMakesRefetch from "../dms-cdk-makes/dms-cdk-makes.refetch.component";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
@@ -89,7 +89,7 @@ export function DmsPostForm({ bodyshop, socket, job, logsRef }) {
job.area_of_damage && job.area_of_damage.impact1
? " " +
t("jobs.labels.dms.damageto", {
area_of_damage: (job.area_of_damage && job.area_of_damage.impact1) || "UNKNOWN"
area_of_damage: (job.area_of_damage && job.area_of_damage.impact1.padStart(2, "0")) || "UNKNOWN"
})
: ""
}`.slice(0, 239),

View File

@@ -1,6 +1,5 @@
import { useMutation } from "@apollo/client";
import { Button, Card, Form, Input, notification, Switch } from "antd";
import dayjs from "../../../../utils/day";
import queryString from "query-string";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
@@ -14,6 +13,7 @@ import { UPDATE_OWNER } from "../../../../graphql/owners.queries";
import { insertAuditTrail } from "../../../../redux/application/application.actions";
import { selectBodyshop, selectCurrentUser } from "../../../../redux/user/user.selectors";
import AuditTrailMapping from "../../../../utils/AuditTrailMappings";
import dayjs from "../../../../utils/day";
import ConfigFormComponents from "../../../config-form-components/config-form-components.component";
import DateTimePicker from "../../../form-date-time-picker/form-date-time-picker.component";
@@ -275,7 +275,19 @@ export function JobChecklistForm({ insertAuditTrail, formItems, bodyshop, curren
>
<DateTimePicker disabled={readOnly} />
</Form.Item>
<Form.Item name="actual_delivery" label={t("jobs.fields.actual_delivery")} disabled={readOnly}>
<Form.Item
name="actual_delivery"
label={t("jobs.fields.actual_delivery")}
rules={[
{
required: bodyshop.deliverchecklist.actual_delivery
? bodyshop.deliverchecklist.actual_delivery
: false
//message: t("general.validation.required"),
}
]}
disabled={readOnly}
>
<DateTimePicker disabled={readOnly} />
</Form.Item>
<Form.Item

View File

@@ -1,6 +1,6 @@
import React, { useCallback, useState } from "react";
import { LockOutlined } from "@ant-design/icons";
import { Badge, Card, Col, Collapse, Form, Input, Row, Space, Tooltip } from "antd";
import React, { useCallback, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
@@ -12,8 +12,9 @@ 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 JobCloseRoGuardTtLifecycle from "./job-close-ro-guard.tt-lifecycle";
// import JobCloseRoGuardSublet from "./job-close-ro-guard.sublet";
import InstanceRenderManager from "../../utils/instanceRenderMgr";
import JobCloseRoGuardTtLifecycle from "./job-close-ro-guard.tt-lifecycle";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser

View File

@@ -1,14 +1,21 @@
import React from "react";
import { useTranslation } from "react-i18next";
import CardTemplate from "./job-detail-cards.template.component";
import Car from "../job-damage-visual/job-damage-visual.component";
import CardTemplate from "./job-detail-cards.template.component";
export default function JobDetailCardsDamageComponent({ loading, data }) {
const { t } = useTranslation();
const { area_of_damage } = data;
return (
<CardTemplate loading={loading} title={t("jobs.labels.cards.damage")}>
{area_of_damage ? <Car dmg1={area_of_damage.impact1} dmg2={area_of_damage.impact2} /> : t("jobs.errors.nodamage")}
{area_of_damage ? (
<Car
dmg1={area_of_damage.impact1 && area_of_damage.impact1.padStart(2, "0")}
dmg2={area_of_damage.impact2 && area_of_damage.impact2.padStart(2, "0")}
/>
) : (
t("jobs.errors.nodamage")
)}
</CardTemplate>
);
}

View File

@@ -26,6 +26,16 @@ export function JobDetailCardsPartsComponent({ loading, data, jobRO }) {
const { t } = useTranslation();
const { joblines_status } = data;
const filteredJobLines = data.joblines.filter(
(j) =>
j.part_type !== null &&
j.part_type !== "PAE" &&
j.part_type !== "PAS" &&
j.part_type !== "PASL" &&
j.part_qty !== 0 &&
j.act_price !== 0
);
//TODO: Correct jobline_statuses view by including the part_qty !== 0 and act_price !== 0
const columns = [
{
title: t("joblines.fields.line_desc"),
@@ -95,7 +105,7 @@ export function JobDetailCardsPartsComponent({ loading, data, jobRO }) {
<div>
<CardTemplate loading={loading} title={t("jobs.labels.cards.parts")}>
<PartsStatusPie joblines_status={joblines_status} />
<Table key="id" columns={columns} dataSource={data ? data.joblines : []} />
<Table key="id" columns={columns} dataSource={filteredJobLines ? filteredJobLines : []} />
</CardTemplate>
</div>
);

View File

@@ -3,6 +3,7 @@ import { Button, Space, notification } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { DELETE_DELIVERY_CHECKLIST, DELETE_INTAKE_CHECKLIST } from "../../graphql/jobs.queries";
import InstanceRenderManager from "../../utils/instanceRenderMgr";
export default function JobAdminDeleteIntake({ job }) {
const { t } = useTranslation();
@@ -47,16 +48,22 @@ export default function JobAdminDeleteIntake({ job }) {
setLoading(false);
};
return (
const InstanceRender = InstanceRenderManager({
imex: true,
rome: "USE_IMEX",
promanager: false
});
return InstanceRender ? (
<>
<Space wrap>
<Button loading={loading} onClick={handleDelete} disabled={!job.intakechecklist}>
{t("jobs.labels.deleteintake")}
</Button>
<Button loading={loading} onClick={handleDeleteDelivery} disabled={!job.deliverychecklist}>
<Button loading={loading} onClick={handleDeleteDelivery} disabled={!job.deliverchecklist}>
{t("jobs.labels.deletedelivery")}
</Button>
</Space>
</>
);
) : null;
}

View File

@@ -189,7 +189,10 @@ export function JobsDetailGeneral({ bodyshop, jobRO, job, form }) {
</Col>
<Col {...lossColDamage}>
{job.area_of_damage ? (
<Car dmg1={job.area_of_damage.impact1} dmg2={job.area_of_damage.impact2} />
<Car
dmg1={job.area_of_damage.impact1 && job.area_of_damage.impact1.padStart(2, "0")}
dmg2={job.area_of_damage.impact2 && job.area_of_damage.impact2.padStart(2, "0")}
/>
) : (
t("jobs.errors.nodamage")
)}

View File

@@ -18,7 +18,6 @@ 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
});

View File

@@ -22,7 +22,7 @@ const CardColorLegend = ({ bodyshop }) => {
});
return (
<Col style={{ marginLeft: "15px" }}>
<Col>
<Typography>{t("production.labels.legend")}</Typography>
<List
grid={{

View File

@@ -6,7 +6,7 @@ import {
PauseCircleOutlined
} from "@ant-design/icons";
import { Card, Col, Row, Space, Tooltip } from "antd";
import React, { useMemo } from "react";
import React from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { DateTimeFormatter } from "../../utils/DateFormatter";
@@ -18,102 +18,77 @@ 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";
/**
* Get the color of the card based on the total hours
* @param ssbuckets
* @param totalHrs
* @returns {{r: number, b: number, g: number}}
*/
const cardColor = (ssbuckets, totalHrs) => {
const bucket = ssbuckets.find((bucket) => bucket.gte <= totalHrs && (!bucket.lt || bucket.lt > 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.rgb || bucket.color;
color = bucket.color;
if (bucket.color.rgb) {
color = bucket.color.rgb;
}
}
return color;
};
/**
* Get the contrast color based on the background color
* @param bgColor
* @returns {string}
*/
const getContrastYIQ = (bgColor) =>
(bgColor.r * 299 + bgColor.g * 587 + bgColor.b * 114) / 1000 >= 128 ? "black" : "white";
function getContrastYIQ(bgColor) {
const yiq = (bgColor.r * 299 + bgColor.g * 587 + bgColor.b * 114) / 1000;
/**
* Production Board Card component
* @param technician
* @param card
* @param bodyshop
* @param cardSettings
* @returns {Element}
* @constructor
*/
export default function ProductionBoardCard({ technician, card, bodyshop, cardSettings }) {
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;
// Destructure metadata
const { metadata } = card;
if (metadata?.employee_body) {
employee_body = bodyshop.employees.find((e) => e.id === metadata.employee_body);
if (card.employee_body) {
employee_body = bodyshop.employees.find((e) => e.id === card.employee_body);
}
if (metadata?.employee_prep) {
employee_prep = bodyshop.employees.find((e) => e.id === metadata.employee_prep);
if (card.employee_prep) {
employee_prep = bodyshop.employees.find((e) => e.id === card.employee_prep);
}
if (metadata?.employee_refinish) {
employee_refinish = bodyshop.employees.find((e) => e.id === metadata.employee_refinish);
if (card.employee_refinish) {
employee_refinish = bodyshop.employees.find((e) => e.id === card.employee_refinish);
}
if (metadata?.employee_csr) {
employee_csr = bodyshop.employees.find((e) => e.id === metadata.employee_csr);
if (card.employee_csr) {
employee_csr = bodyshop.employees.find((e) => e.id === card.employee_csr);
}
// if (metadata.?employee_csr) {
// employee_csr = bodyshop.employees.find((e) => e.id === metadata.employee_csr);
// if (card.employee_csr) {
// employee_csr = bodyshop.employees.find((e) => e.id === card.employee_csr);
// }
const pastDueAlert =
!!metadata?.scheduled_completion &&
((dayjs().isSameOrAfter(dayjs(metadata.scheduled_completion), "day") && "production-completion-past") ||
(dayjs().add(1, "day").isSame(dayjs(metadata.scheduled_completion), "day") && "production-completion-soon"));
!!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 = useMemo(() => {
return metadata?.labhrs && metadata?.larhrs
? metadata.labhrs.aggregate.sum.mod_lb_hrs + metadata.larhrs.aggregate.sum.mod_lb_hrs
: 0;
}, [metadata]);
const bgColor = useMemo(() => cardColor(bodyshop.ssbuckets, totalHrs), [bodyshop.ssbuckets, totalHrs]);
const contrastYIQ = useMemo(() => getContrastYIQ(bgColor), [bgColor]);
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-trello-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 && contrastYIQ,
maxWidth: "250px",
margin: "5px"
color: cardSettings && cardSettings.cardcolor && getContrastYIQ(bgColor)
}}
title={
<Space>
<ProductionAlert record={card} key="alert" />
{metadata?.suspended && <PauseCircleOutlined style={{ color: "orangered" }} />}
{metadata?.iouparent && (
{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}`}>
{metadata?.ro_number || t("general.labels.na")}
{card.ro_number || t("general.labels.na")}
</Link>
</span>
</Space>
@@ -128,7 +103,7 @@ export default function ProductionBoardCard({ technician, card, bodyshop, cardSe
{cardSettings && cardSettings.ownr_nm && (
<Col span={24}>
{cardSettings && cardSettings.compact ? (
<div className="ellipses">{`${metadata.ownr_ln || ""} ${metadata.ownr_co_nm || ""}`}</div>
<div className="ellipses">{`${card.ownr_ln || ""} ${card.ownr_co_nm || ""}`}</div>
) : (
<div className="ellipses">
<OwnerNameDisplay ownerObject={card} />
@@ -137,18 +112,18 @@ export default function ProductionBoardCard({ technician, card, bodyshop, cardSe
</Col>
)}
<Col span={24}>
<div className="ellipses">{`${metadata.v_model_yr || ""} ${
metadata.v_make_desc || ""
} ${metadata.v_model_desc || ""}`}</div>
<div className="ellipses">{`${card.v_model_yr || ""} ${
card.v_make_desc || ""
} ${card.v_model_desc || ""}`}</div>
</Col>
{cardSettings && cardSettings.ins_co_nm && metadata.ins_co_nm && (
{cardSettings && cardSettings.ins_co_nm && card.ins_co_nm && (
<Col span={cardSettings && cardSettings.compact ? 24 : 12}>
<div className="ellipses">{metadata.ins_co_nm || ""}</div>
<div className="ellipses">{card.ins_co_nm || ""}</div>
</Col>
)}
{cardSettings && cardSettings.clm_no && metadata.clm_no && (
{cardSettings && cardSettings.clm_no && card.clm_no && (
<Col span={cardSettings && cardSettings.compact ? 24 : 12}>
<div className="ellipses">{metadata.clm_no || ""}</div>
<div className="ellipses">{card.clm_no || ""}</div>
</Col>
)}
@@ -157,7 +132,7 @@ export default function ProductionBoardCard({ technician, card, bodyshop, cardSe
<Row>
<Col span={cardSettings && cardSettings.compact ? 24 : 12}>{`B: ${
employee_body ? `${employee_body.first_name.substr(0, 3)} ${employee_body.last_name.charAt(0)}` : ""
} ${metadata.labhrs.aggregate.sum.mod_lb_hrs || "?"}h`}</Col>
} ${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>
@@ -165,7 +140,7 @@ export default function ProductionBoardCard({ technician, card, bodyshop, cardSe
employee_refinish
? `${employee_refinish.first_name.substr(0, 3)} ${employee_refinish.last_name.charAt(0)}`
: ""
} ${metadata.larhrs.aggregate.sum.mod_lb_hrs || "?"}h`}</Col>
} ${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>
@@ -176,56 +151,48 @@ export default function ProductionBoardCard({ technician, card, bodyshop, cardSe
<Col span={24}>
<Row>
<Col span={cardSettings && cardSettings.compact ? 24 : 12}>{`B: ${
metadata.labhrs.aggregate.sum.mod_lb_hrs || "?"
card.labhrs.aggregate.sum.mod_lb_hrs || "?"
} hrs`}</Col>
<Col span={cardSettings && cardSettings.compact ? 24 : 12}>{`R: ${
metadata.larhrs.aggregate.sum.mod_lb_hrs || "?"
card.larhrs.aggregate.sum.mod_lb_hrs || "?"
} hrs`}</Col>
</Row>
</Col>
)} */}
{cardSettings && cardSettings.actual_in && metadata.actual_in && (
{cardSettings && cardSettings.actual_in && card.actual_in && (
<Col span={cardSettings && cardSettings.compact ? 24 : 12}>
<Space>
<DownloadOutlined />
<DateTimeFormatter format="MM/DD">{metadata.actual_in}</DateTimeFormatter>
<DateTimeFormatter format="MM/DD">{card.actual_in}</DateTimeFormatter>
</Space>
</Col>
)}
{cardSettings && cardSettings.scheduled_completion && metadata.scheduled_completion && (
{cardSettings && cardSettings.scheduled_completion && card.scheduled_completion && (
<Col span={cardSettings && cardSettings.compact ? 24 : 12}>
<Space className={pastDueAlert}>
<CalendarOutlined />
<DateTimeFormatter format="MM/DD">{metadata.scheduled_completion}</DateTimeFormatter>
<DateTimeFormatter format="MM/DD">{card.scheduled_completion}</DateTimeFormatter>
</Space>
</Col>
)}
{cardSettings && cardSettings.ats && metadata.alt_transport && (
{cardSettings && cardSettings.ats && card.alt_transport && (
<Col span={12}>
<div>{metadata.alt_transport || ""}</div>
<div>{card.alt_transport || ""}</div>
</Col>
)}
{cardSettings && cardSettings.sublets && (
<Col span={12}>
<ProductionSubletsManageComponent subletJobLines={metadata.subletLines} />
<ProductionSubletsManageComponent subletJobLines={card.subletLines} />
</Col>
)}
{cardSettings && cardSettings.production_note && (
<Col span={24}>
{cardSettings && cardSettings.production_note && (
<ProductionListColumnProductionNote
record={{
production_vars: card?.metadata.production_vars,
id: card?.id,
refetch: card?.refetch
}}
/>
)}
{cardSettings && cardSettings.production_note && <ProductionListColumnProductionNote record={card} />}
</Col>
)}
{cardSettings && cardSettings.partsstatus && (
<Col span={24}>
<JobPartsQueueCount parts={metadata.joblines_status} />
<JobPartsQueueCount parts={card.joblines_status} />
</Col>
)}
</Row>

View File

@@ -0,0 +1,125 @@
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>
);
}

View File

@@ -1,7 +1,7 @@
import { SyncOutlined, UnorderedListOutlined } from "@ant-design/icons";
import { SyncOutlined } from "@ant-design/icons";
import { useApolloClient } from "@apollo/client";
import Board from "../../components/trello-board/index";
import { Button, Grid, notification, Skeleton, Space, Statistic } from "antd";
import Board, { moveCard } from "@asseinfo/react-kanban";
import { Button, Grid, notification, Space, Statistic } from "antd";
import { PageHeader } from "@ant-design/pro-layout";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
@@ -19,10 +19,11 @@ import IndefiniteLoading from "../indefinite-loading/indefinite-loading.componen
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 "./production-board-kanban.styles.scss";
import { createBoardData, createFakeBoardData } from "./production-board-kanban.utils.js";
import ProductionBoardKanbanSettings from "./production-board-kanban.settings.component.jsx";
import { createBoardData } from "./production-board-kanban.utils.js";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -30,14 +31,7 @@ const mapStateToProps = createStructuredSelector({
});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(
insertAuditTrail({
jobid,
operation,
type
})
)
insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type }))
});
export function ProductionBoardKanbanComponent({
@@ -48,30 +42,23 @@ export function ProductionBoardKanbanComponent({
insertAuditTrail,
associationSettings
}) {
const [boardLanes, setBoardLanes] = useState({ lanes: [] });
const [boardLanes, setBoardLanes] = useState({
columns: [{ id: "Loading...", title: "Loading...", cards: [] }]
});
const [filter, setFilter] = useState({ search: "", employeeId: null });
const [loading, setLoading] = useState(true);
const [isMoving, setIsMoving] = useState(false);
const orientation = associationSettings?.kanban_settings?.orientation ? "vertical" : "horizontal";
const { t } = useTranslation();
useEffect(() => {
if (associationSettings) {
setLoading(false);
}
}, [associationSettings]);
useEffect(() => {
const boardData = createFakeBoardData(
const boardData = createBoardData(
[...bodyshop.md_ro_statuses.production_statuses, ...(bodyshop.md_ro_statuses.additional_board_statuses || [])],
data,
filter
);
boardData.lanes = boardData.lanes.map((d) => {
boardData.columns = boardData.columns.map((d) => {
return { ...d, title: `${d.title} (${d.cards.length})` };
});
setBoardLanes(boardData);
@@ -80,75 +67,72 @@ export function ProductionBoardKanbanComponent({
const client = useApolloClient();
const handleDragEnd = async (cardId, sourceLaneId, targetLaneId, position, cardDetails) => {
const handleDragEnd = async (card, source, destination) => {
logImEXEvent("kanban_drag_end");
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 sameColumnTransfer = sourceLaneId === targetLaneId;
const movedCardWillBeFirst = destination.toPosition === 0;
const sourceLane = boardLanes.lanes.find((lane) => lane.id === sourceLaneId);
const targetLane = boardLanes.lanes.find((lane) => lane.id === targetLaneId);
const movedCardWillBeLast = destinationColumn.cards.length - destination.toPosition < 1;
const movedCardWillBeFirst = position === 0;
const movedCardWillBeLast = targetLane.cards.length - position < 1;
const lastCardInDestinationColumn = destinationColumn.cards[destinationColumn.cards.length - 1];
const lastCardInTargetLane = targetLane.cards[targetLane.cards.length - 1];
const oldChildCard = sourceLane.cards[position + 1];
const oldChildCard = sourceColumn.cards[source.fromPosition + 1];
const newChildCard = movedCardWillBeLast
? null
: targetLane.cards[sameColumnTransfer ? (position - position > 0 ? position : position + 1) : position];
: destinationColumn.cards[
sameColumnTransfer
? source.fromPosition - destination.toPosition > 0
? destination.toPosition
: destination.toPosition + 1
: destination.toPosition
];
const oldChildCardNewParent = oldChildCard ? cardDetails.kanbanparent : null;
const oldChildCardNewParent = oldChildCard ? card.kanbanparent : null;
let movedCardNewKanbanParent;
if (movedCardWillBeFirst) {
//console.log("==> New Card is first.");
movedCardNewKanbanParent = "-1";
} else if (movedCardWillBeLast) {
movedCardNewKanbanParent = lastCardInTargetLane.id;
// 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 ? cardId : null;
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"
});
try {
const update = await client.mutate({
mutation: generate_UPDATE_JOB_KANBAN(
oldChildCard ? oldChildCard.id : null,
oldChildCardNewParent,
cardId,
movedCardNewKanbanParent,
targetLaneId,
newChildCard ? newChildCard.id : null,
newChildCardNewParent
)
});
insertAuditTrail({
jobid: cardId,
operation: AuditTrailMapping.jobstatuschange(targetLaneId),
type: "jobstatuschange"
});
if (update.errors) {
notification["error"]({
message: t("production.errors.boardupdate", {
message: JSON.stringify(update.errors)
})
});
}
} catch (error) {
if (update.errors) {
notification["error"]({
message: t("production.errors.boardupdate", {
message: error.message
message: JSON.stringify(update.errors)
})
});
} finally {
setIsMoving(false);
}
};
@@ -187,21 +171,26 @@ export function ProductionBoardKanbanComponent({
: standardSizes[selectedBreakpoint[0]]
: "250";
const StickyHeader = ({ title }) => (
<Sticky>
{({ style }) => (
<div className="react-trello-column-header" style={{ ...style, zIndex: "99", backgroundColor: "#e3e3e3" }}>
<UnorderedListOutlined style={{ marginRight: "5px" }} /> {title}
</div>
)}
</Sticky>
);
const stickyHeader = {
renderColumnHeader: ({ title }) => (
<Sticky>
{({
style,
const NormalHeader = ({ title }) => (
<div className="react-trello-column-header" style={{ backgroundColor: "#e3e3e3" }}>
<UnorderedListOutlined style={{ marginRight: "5px" }} /> {title}
</div>
);
// 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 &&
@@ -219,22 +208,13 @@ export function ProductionBoardKanbanComponent({
employeeassignments: true,
scheduled_completion: true,
stickyheader: false,
cardcolor: false,
orientation: false
cardcolor: false
};
const components = {
Card: (cardProps) => ProductionBoardCard({ card: cardProps, technician, bodyshop, cardSettings }),
LaneHeader: cardSettings.stickyheader && orientation === "horizontal" ? StickyHeader : NormalHeader
};
if (loading) {
return <Skeleton active />;
}
return (
<Container width={width}>
<IndefiniteLoading loading={isMoving} />
<PageHeader
title={
<Space>
@@ -250,37 +230,24 @@ export function ProductionBoardKanbanComponent({
<SyncOutlined />
</Button>
<ProductionBoardFilters filter={filter} setFilter={setFilter} loading={isMoving} />
<ProductionBoardKanbanSettings parentLoading={setLoading} associationSettings={associationSettings} />
<ProductionBoardKanbanCardSettings associationSettings={associationSettings} />
</Space>
}
/>
{cardSettings.cardcolor && <CardColorLegend cardSettings={cardSettings} bodyshop={bodyshop} />}
<ProductionListDetailComponent jobs={data} />
{cardSettings.stickyheader ? (
<StickyContainer>
<Board
data={boardLanes}
handleDragEnd={handleDragEnd}
style={{ height: "100%", backgroundColor: "transparent", overflowY: "auto" }}
components={components}
orientation={orientation}
collapsibleLanes
laneDraggable={false}
/>
</StickyContainer>
) : (
<div>
<Board
data={boardLanes}
handleDragEnd={handleDragEnd}
style={{ backgroundColor: "transparent", overflowY: "auto" }}
components={components}
collapsibleLanes
orientation={orientation}
laneDraggable={false}
/>
</div>
)}
<StickyContainer>
<Board
style={{ height: "100%" }}
children={boardLanes}
disableCardDrag={isMoving}
{...(cardSettings.stickyheader && stickyHeader)}
renderCard={(card) => ProductionBoardCard(technician, card, bodyshop, cardSettings)}
onCardDragEnd={handleDragEnd}
/>
</StickyContainer>
</Container>
);
}
@@ -288,9 +255,9 @@ export function ProductionBoardKanbanComponent({
export default connect(mapStateToProps, mapDispatchToProps)(ProductionBoardKanbanComponent);
const Container = styled.div`
.react-trello-card-skeleton,
.react-trello-card,
.react-trello-card-adder-form {
.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;

View File

@@ -1,243 +0,0 @@
import { useMutation } from "@apollo/client";
import { Button, Card, Col, Form, notification, Popover, Row, Checkbox, Tabs, Switch } from "antd";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { UPDATE_KANBAN_SETTINGS } from "../../graphql/user.queries";
const { TabPane } = Tabs;
export default function ProductionBoardKanbanSettings({ associationSettings, parentLoading }) {
const [form] = Form.useForm();
const [open, setOpen] = useState(false);
const [loading, setLoading] = useState(false);
const [hasChanges, setHasChanges] = 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);
parentLoading(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);
parentLoading(false);
setHasChanges(false);
};
const handleValuesChange = (changedValues, allValues) => {
setHasChanges(true);
};
const cardStyle = { minWidth: "50vw", marginTop: 10 };
const renderCardSettings = () => (
<>
<Card title={t("settings.sections.layout")} style={cardStyle}>
<Row gutter={[16, 16]}>
<Col span={4}>
<Form.Item name="compact" valuePropName="checked">
<Checkbox>{t("production.labels.compact")}</Checkbox>
</Form.Item>
</Col>
<Col span={4}>
<Form.Item name="cardcolor" valuePropName="checked">
<Checkbox>{t("production.labels.cardcolor")}</Checkbox>
</Form.Item>
</Col>
</Row>
</Card>
<Card title={t("settings.sections.information")} style={cardStyle}>
<Row gutter={[16, 16]}>
<Col span={4}>
<Form.Item name="ownr_nm" valuePropName="checked">
<Checkbox>{t("production.labels.ownr_nm")}</Checkbox>
</Form.Item>
</Col>
<Col span={4}>
<Form.Item name="clm_no" valuePropName="checked">
<Checkbox>{t("production.labels.clm_no")}</Checkbox>
</Form.Item>
</Col>
<Col span={4}>
<Form.Item name="ins_co_nm" valuePropName="checked">
<Checkbox>{t("production.labels.ins_co_nm")}</Checkbox>
</Form.Item>
</Col>
<Col span={4}>
<Form.Item name="employeeassignments" valuePropName="checked">
<Checkbox>{t("production.labels.employeeassignments")}</Checkbox>
</Form.Item>
</Col>
<Col span={4}>
<Form.Item name="actual_in" valuePropName="checked">
<Checkbox>{t("production.labels.actual_in")}</Checkbox>
</Form.Item>
</Col>
<Col span={4}>
<Form.Item name="scheduled_completion" valuePropName="checked">
<Checkbox>{t("production.labels.scheduled_completion")}</Checkbox>
</Form.Item>
</Col>
<Col span={4}>
<Form.Item name="ats" valuePropName="checked">
<Checkbox>{t("production.labels.ats")}</Checkbox>
</Form.Item>
</Col>
<Col span={4}>
<Form.Item name="production_note" valuePropName="checked">
<Checkbox>{t("production.labels.production_note")}</Checkbox>
</Form.Item>
</Col>
<Col span={4}>
<Form.Item name="sublets" valuePropName="checked">
<Checkbox>{t("production.labels.sublets")}</Checkbox>
</Form.Item>
</Col>
<Col span={4}>
<Form.Item name="partsstatus" valuePropName="checked">
<Checkbox>{t("production.labels.partsstatus")}</Checkbox>
</Form.Item>
</Col>
</Row>
</Card>
<Card title={t("settings.sections.beta")} style={cardStyle}>
<Row gutter={[16, 16]}>
<Col span={4}>
<Form.Item name="stickyheader" valuePropName="checked">
<Checkbox>{t("production.labels.stickyheader")}</Checkbox>
</Form.Item>
</Col>
</Row>
</Card>
</>
);
const renderBoardSettings = () => (
<>
<Card title={t("settings.sections.layout")} style={cardStyle}>
<Row gutter={[16, 16]}>
<Col span={4} style={{ display: "flex", alignItems: "center" }}>
<span style={{ marginRight: "8px" }}>Orientation</span>
<Form.Item name="orientation" valuePropName="checked" style={{ marginBottom: 0 }}>
<Switch checkedChildren="Vertical" unCheckedChildren="Horizontal" defaultChecked />
</Form.Item>
</Col>
</Row>
</Card>
{/*<Card title={t("settings.sections.information")} style={cardStyle}>*/}
{/* <Row gutter={[16, 16]}>*/}
{/* <Col span={4}>*/}
{/* <Form.Item name="board_setting_3" valuePropName="checked">*/}
{/* <Checkbox>{t("board.labels.some_setting_3")}</Checkbox>*/}
{/* </Form.Item>*/}
{/* </Col>*/}
{/* <Col span={4}>*/}
{/* <Form.Item name="board_setting_4" valuePropName="checked">*/}
{/* <Checkbox>{t("board.labels.some_setting_4")}</Checkbox>*/}
{/* </Form.Item>*/}
{/* </Col>*/}
{/* </Row>*/}
{/*</Card>*/}
{/*<Card title={t("settings.sections.beta")} style={cardStyle}>*/}
{/* <Row gutter={[16, 16]}>*/}
{/* <Col span={4}>/!* Add beta settings here if any *!/</Col>*/}
{/* </Row>*/}
{/*</Card>*/}
</>
);
const renderLaneSettings = () => (
<>
<Card title={t("settings.sections.layout")} style={cardStyle}>
<Row gutter={[16, 16]}>
{/*<Col span={4}>*/}
{/* <Form.Item name="lane_setting_1" valuePropName="checked">*/}
{/* <Checkbox>{t("lane.labels.some_setting_1")}</Checkbox>*/}
{/* </Form.Item>*/}
{/*</Col>*/}
{/*<Col span={4}>*/}
{/* <Form.Item name="lane_setting_2" valuePropName="checked">*/}
{/* <Checkbox>{t("lane.labels.some_setting_2")}</Checkbox>*/}
{/* </Form.Item>*/}
{/*</Col>*/}
</Row>
</Card>
{/*<Card title={t("settings.sections.information")} style={cardStyle}>*/}
{/* <Row gutter={[16, 16]}>*/}
{/* <Col span={4}>*/}
{/* <Form.Item name="lane_setting_3" valuePropName="checked">*/}
{/* <Checkbox>{t("lane.labels.some_setting_3")}</Checkbox>*/}
{/* </Form.Item>*/}
{/* </Col>*/}
{/* <Col span={4}>*/}
{/* <Form.Item name="lane_setting_4" valuePropName="checked">*/}
{/* <Checkbox>{t("lane.labels.some_setting_4")}</Checkbox>*/}
{/* </Form.Item>*/}
{/* </Col>*/}
{/* </Row>*/}
{/*</Card>*/}
{/*<Card title={t("settings.sections.beta")} style={cardStyle}>*/}
{/* <Row gutter={[16, 16]}>*/}
{/* <Col span={4}>/!* Add beta settings here if any *!/</Col>*/}
{/* </Row>*/}
{/*</Card>*/}
</>
);
const overlay = (
<Card>
<Form form={form} onFinish={handleFinish} layout="vertical" onValuesChange={handleValuesChange}>
<Tabs defaultActiveKey="1">
<TabPane tab={t("settings.tabs.card")} key="1">
{renderCardSettings()}
</TabPane>
<TabPane tab={t("settings.tabs.board")} key="2">
{renderBoardSettings()}
</TabPane>
<TabPane tab={t("settings.tabs.lane")} key="3">
{renderLaneSettings()}
</TabPane>
</Tabs>
<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={() => 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("settings.buttons.boardSettings")}
</Button>
</Popover>
);
}

View File

@@ -1,31 +1,31 @@
.react-trello-board {
.react-kanban-board {
padding: 5px;
}
.react-trello-card {
.react-kanban-card {
border-radius: 3px;
background-color: #fff;
padding: 4px;
margin-bottom: 7px;
}
// .react-trello-card-skeleton,
// .react-trello-card,
// .react-trello-card-adder-form {
// .react-kanban-card-skeleton,
// .react-kanban-card,
// .react-kanban-card-adder-form {
// box-sizing: border-box;
// max-width: 145px;
// min-width: 145px;
// }
.react-trello-card--dragging {
.react-kanban-card--dragging {
box-shadow: 2px 2px grey;
}
.react-trello-card__description {
.react-kanban-card__description {
padding-top: 10px;
}
.react-trello-card__title {
.react-kanban-card__title {
border-bottom: 1px solid #eee;
padding-bottom: 5px;
font-weight: bold;
@@ -33,31 +33,31 @@
justify-content: space-between;
}
.react-trello-column {
.react-kanban-column {
padding: 10px;
border-radius: 2px;
background-color: #eee;
margin: 5px;
}
.react-trello-column input:focus {
.react-kanban-column input:focus {
outline: none;
}
.react-trello-card-adder-form {
.react-kanban-card-adder-form {
border-radius: 3px;
background-color: #fff;
padding: 10px;
margin-bottom: 7px;
}
.react-trello-card-adder-form input {
.react-kanban-card-adder-form input {
border: 0px;
font-family: inherit;
font-size: inherit;
}
.react-trello-card-adder-button {
.react-kanban-card-adder-button {
width: 100%;
margin-top: 5px;
background-color: transparent;
@@ -70,11 +70,11 @@
font-weight: bold;
}
.react-trello-card-adder-button:hover {
.react-kanban-card-adder-button:hover {
background-color: #ccc;
}
.react-trello-card-adder-form__title {
.react-kanban-card-adder-form__title {
font-weight: bold;
border-bottom: 1px solid #eee;
padding-bottom: 5px;
@@ -85,20 +85,20 @@
padding: 0px;
}
.react-trello-card-adder-form__title:focus {
.react-kanban-card-adder-form__title:focus {
outline: none;
}
.react-trello-card-adder-form__description {
.react-kanban-card-adder-form__description {
width: 100%;
margin-top: 10px;
}
.react-trello-card-adder-form__description:focus {
.react-kanban-card-adder-form__description:focus {
outline: none;
}
.react-trello-card-adder-form__button {
.react-kanban-card-adder-form__button {
background-color: #eee;
border: none;
padding: 5px;
@@ -107,39 +107,39 @@
border-radius: 3px;
}
.react-trello-card-adder-form__button:hover {
.react-kanban-card-adder-form__button:hover {
transition: 0.3s;
cursor: pointer;
background-color: #ccc;
}
.react-trello-column-header {
.react-kanban-column-header {
padding-bottom: 10px;
font-weight: bold;
}
.react-trello-column-header input:focus {
.react-kanban-column-header input:focus {
outline: none;
}
.react-trello-column-header__button {
.react-kanban-column-header__button {
color: #333333;
background-color: #ffffff;
border-color: #cccccc;
}
.react-trello-column-header__button:hover,
.react-trello-column-header__button:focus,
.react-trello-column-header__button:active {
.react-kanban-column-header__button:hover,
.react-kanban-column-header__button:focus,
.react-kanban-column-header__button:active {
background-color: #e6e6e6;
}
.react-trello-column-adder-button {
.react-kanban-column-adder-button {
border: 2px dashed #eee;
height: 132px;
margin: 5px;
}
.react-trello-column-adder-button:hover {
.react-kanban-column-adder-button:hover {
cursor: pointer;
}

View File

@@ -1,5 +1,4 @@
import { groupBy } from "lodash";
import fakeData from "./testData/board300.json";
const sortByParentId = (arr) => {
// return arr.reduce((accumulator, currentValue) => {
@@ -19,8 +18,8 @@ const sortByParentId = (arr) => {
//console.log("sortByParentId -> byParentsIdsList", byParentsIdsList);
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]); //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.
}
if (byParentsIdsList["null"]) byParentsIdsList["null"].map((i) => sortedList.push(i));
@@ -39,19 +38,17 @@ const sortByParentId = (arr) => {
return sortedList;
};
export const createFakeBoardData = () => {
return fakeData;
};
export const createBoardData = (AllStatuses, Jobs, filter) => {
const { search, employeeId } = filter;
const lanes = AllStatuses.map((s) => {
return {
id: s,
title: s,
cards: []
};
});
const boardLanes = {
columns: AllStatuses.map((s) => {
return {
id: s,
title: s,
cards: []
};
})
};
const filteredJobs =
(search === "" || !search) && !employeeId
@@ -78,25 +75,16 @@ export const createBoardData = (AllStatuses, Jobs, filter) => {
Object.keys(DataGroupedByStatus).map((statusGroupKey) => {
try {
const lane = lanes.find((l) => l.id === statusGroupKey);
if (!lane?.cards) return null;
lane.cards = sortByParentId(DataGroupedByStatus[statusGroupKey]).map((job) => {
const { id, title, description, due_date, ...metadata } = job;
return {
id,
title,
description,
label: job.due_date || "",
metadata
};
});
const needle = boardLanes.columns.find((l) => l.id === statusGroupKey);
if (!needle?.cards) return null;
needle.cards = sortByParentId(DataGroupedByStatus[statusGroupKey]);
} catch (error) {
console.log("Error while creating board card", error);
}
return null;
});
return { lanes };
return boardLanes;
};
const CheckSearch = (search, job) => {

View File

@@ -18,6 +18,7 @@ const mapDispatchToProps = (dispatch) => ({
function ProductionListColumnProductionNote({ record, setNoteUpsertContext }) {
const { t } = useTranslation();
const [note, setNote] = useState((record.production_vars && record.production_vars.note) || "");
const [open, setOpen] = useState(false);

View File

@@ -9,7 +9,6 @@ export default function ProductionSubletsManageComponent({ subletJobLines }) {
const { t } = useTranslation();
const [updateJobLine] = useMutation(UPDATE_JOB_LINE_SUBLET);
const [loading, setLoading] = useState(false);
const subletCount = useMemo(() => {
return {
total: subletJobLines.filter((s) => !s.sublet_ignored).length,

View File

@@ -63,7 +63,7 @@ export default function ShopEmployeesListComponent({ loading, employees }) {
value: false
}
],
onFilter: (value, record) => value === record.flate_rate,
onFilter: (value, record) => value === record.flat_rate,
render: (text, record) =>
record.flat_rate ? t("employees.labels.flat_rate") : t("employees.labels.straight_time")
},

View File

@@ -319,6 +319,18 @@ export default function ShopInfoIntakeChecklistComponent({ form }) {
))}
</Select>
</Form.Item>
<Form.Item
name={["deliverchecklist", "actual_delivery"]}
label={t("bodyshop.fields.deliver.require_actual_delivery_date")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
>
<Switch />
</Form.Item>
</SelectorDiv>
</div>
);

View File

@@ -1,11 +0,0 @@
import React from "react";
import { AddCardLink } from "../styles/Base";
import { useTranslation } from "react-i18next";
const AddCardLinkComponent = ({ onClick, laneId }) => {
const { t } = useTranslation();
return <AddCardLink onClick={onClick}>{t("trello.labels.add_card")}</AddCardLink>;
};
export default AddCardLinkComponent;

View File

@@ -1,112 +0,0 @@
import React, { useCallback } from "react";
import PropTypes from "prop-types";
import { CardHeader, CardRightContent, CardTitle, Detail, Footer, MovableCardWrapper } from "../styles/Base";
import InlineInput from "../widgets/InlineInput.jsx";
import Tag from "./Card/Tag.jsx";
import DeleteButton from "../widgets/DeleteButton.jsx";
import { useTranslation } from "react-i18next";
const Card = ({
showDeleteButton = true,
onDelete = () => {},
onClick = () => {},
style = {},
tagStyle = {},
className = "",
id,
title = "no title",
label = "",
description = "",
tags = [],
cardDraggable,
editable,
onChange
}) => {
const { t } = useTranslation();
const handleDelete = useCallback(
(e) => {
onDelete();
e.stopPropagation();
},
[onDelete]
);
const updateCard = (card) => {
onChange({ ...card, id });
};
return (
<MovableCardWrapper data-id={id} onClick={onClick} style={style} className={className}>
<CardHeader>
<CardTitle draggable={cardDraggable}>
{editable ? (
<InlineInput
value={title}
border
placeholder={t("trello.labels.title")}
resize="vertical"
onSave={(value) => updateCard({ title: value })}
/>
) : (
title
)}
</CardTitle>
<CardRightContent>
{editable ? (
<InlineInput
value={label}
border
placeholder={t("trello.labels.label")}
resize="vertical"
onSave={(value) => updateCard({ label: value })}
/>
) : (
label
)}
</CardRightContent>
{showDeleteButton && <DeleteButton onClick={handleDelete} />}
</CardHeader>
<Detail>
{editable ? (
<InlineInput
value={description}
border
placeholder={t("trello.labels.description")}
resize="vertical"
onSave={(value) => updateCard({ description: value })}
/>
) : (
description
)}
</Detail>
{tags && tags.length > 0 && (
<Footer>
{tags.map((tag) => (
<Tag key={tag.title} {...tag} tagStyle={tagStyle} />
))}
</Footer>
)}
</MovableCardWrapper>
);
};
Card.propTypes = {
showDeleteButton: PropTypes.bool,
onDelete: PropTypes.func,
onClick: PropTypes.func,
style: PropTypes.object,
tagStyle: PropTypes.object,
className: PropTypes.string,
id: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
label: PropTypes.string,
description: PropTypes.string,
tags: PropTypes.array,
cardDraggable: PropTypes.bool,
editable: PropTypes.bool,
onChange: PropTypes.func.isRequired
};
export default Card;

View File

@@ -1,21 +0,0 @@
import React from "react";
import PropTypes from "prop-types";
import { TagSpan } from "../../styles/Base";
const Tag = ({ title, color, bgcolor, tagStyle, ...otherProps }) => {
const style = { color: color || "white", backgroundColor: bgcolor || "orange", ...tagStyle };
return (
<TagSpan style={style} {...otherProps}>
{title}
</TagSpan>
);
};
Tag.propTypes = {
title: PropTypes.string.isRequired,
color: PropTypes.string,
bgcolor: PropTypes.string,
tagStyle: PropTypes.object
};
export default Tag;

View File

@@ -1,9 +0,0 @@
import React from "react";
import { LaneFooter } from "../../styles/Base";
import { CollapseBtn, ExpandBtn } from "../../styles/Elements";
const LaneFooterComponent = ({ onClick, collapsed }) => (
<LaneFooter onClick={onClick}>{collapsed ? <ExpandBtn /> : <CollapseBtn />}</LaneFooter>
);
export default LaneFooterComponent;

View File

@@ -1,64 +0,0 @@
import React from "react";
import PropTypes from "prop-types";
import InlineInput from "../../widgets/InlineInput.jsx";
import { LaneHeader, RightContent, Title } from "../../styles/Base";
import LaneMenu from "./LaneHeader/LaneMenu.jsx";
import { useTranslation } from "react-i18next";
const LaneHeaderComponent = ({
updateTitle,
canAddLanes,
onDelete,
onDoubleClick,
editLaneTitle,
label,
title,
titleStyle,
labelStyle,
laneDraggable
}) => {
const { t } = useTranslation();
return (
<LaneHeader onDoubleClick={onDoubleClick} editLaneTitle={editLaneTitle}>
<Title draggable={laneDraggable} style={titleStyle}>
{editLaneTitle ? (
<InlineInput
value={title}
border
placeholder={t("trello.labels.title")}
resize="vertical"
onSave={updateTitle}
/>
) : (
title
)}
</Title>
{label && (
<RightContent>
<span style={labelStyle}>{label}</span>
</RightContent>
)}
{canAddLanes && <LaneMenu onDelete={onDelete} />}
</LaneHeader>
);
};
LaneHeaderComponent.propTypes = {
updateTitle: PropTypes.func,
editLaneTitle: PropTypes.bool,
canAddLanes: PropTypes.bool,
laneDraggable: PropTypes.bool,
label: PropTypes.string,
title: PropTypes.string,
onDelete: PropTypes.func,
onDoubleClick: PropTypes.func
};
LaneHeaderComponent.defaultProps = {
updateTitle: () => {},
editLaneTitle: false,
canAddLanes: false
};
export default LaneHeaderComponent;

View File

@@ -1,41 +0,0 @@
import React from "react";
import { Popover } from "react-popopo";
import { CustomPopoverContainer, CustomPopoverContent } from "../../../styles/Base";
import {
DeleteWrapper,
GenDelButton,
LaneMenuContent,
LaneMenuHeader,
LaneMenuItem,
LaneMenuTitle,
MenuButton
} from "../../../styles/Elements";
import { useTranslation } from "react-i18next";
const LaneMenu = ({ onDelete }) => {
const { t } = useTranslation();
return (
<Popover
position="bottom"
PopoverContainer={CustomPopoverContainer}
PopoverContent={CustomPopoverContent}
trigger={<MenuButton></MenuButton>}
>
<LaneMenuHeader>
<LaneMenuTitle>{t("trello.labels.lane_actions")}</LaneMenuTitle>
<DeleteWrapper>
<GenDelButton>&#10006;</GenDelButton>
</DeleteWrapper>
</LaneMenuHeader>
<LaneMenuContent>
<LaneMenuItem onClick={onDelete}>{t("trello.labels.delete_lane")}</LaneMenuItem>
</LaneMenuContent>
</Popover>
);
};
export default LaneMenu;

View File

@@ -1,13 +0,0 @@
import React from 'react'
import {LoaderDiv, LoadingBar} from '../styles/Loader'
const Loader = () => (
<LoaderDiv>
<LoadingBar />
<LoadingBar />
<LoadingBar />
<LoadingBar />
</LoaderDiv>
)
export default Loader

View File

@@ -1,53 +0,0 @@
import React, { useState } from "react";
import PropTypes from "prop-types";
import { CardForm, CardHeader, CardRightContent, CardTitle, CardWrapper, Detail } from "../styles/Base";
import { AddButton, CancelButton } from "../styles/Elements";
import EditableLabel from "../widgets/EditableLabel.jsx";
import { useTranslation } from "react-i18next";
const NewCardForm = ({ onCancel, onAdd }) => {
const [state, setState] = useState({});
const { t } = useTranslation();
const updateField = (field, value) => {
setState((prevState) => ({ ...prevState, [field]: value }));
};
const handleAdd = () => {
onAdd(state);
};
return (
<CardForm>
<CardWrapper>
<CardHeader>
<CardTitle>
<EditableLabel
placeholder={t("trello.labels.title")}
onChange={(val) => updateField("title", val)}
autoFocus
/>
</CardTitle>
<CardRightContent>
<EditableLabel placeholder={t("trello.labels.label")} onChange={(val) => updateField("label", val)} />
</CardRightContent>
</CardHeader>
<Detail>
<EditableLabel
placeholder={t("trello.labels.description")}
onChange={(val) => updateField("description", val)}
/>
</Detail>
</CardWrapper>
<AddButton onClick={handleAdd}>{t("trello.labels.add_card")}</AddButton>
<CancelButton onClick={onCancel}>{t("trello.labels.cancel")}</CancelButton>
</CardForm>
);
};
NewCardForm.propTypes = {
onCancel: PropTypes.func.isRequired,
onAdd: PropTypes.func.isRequired
};
export default NewCardForm;

View File

@@ -1,57 +0,0 @@
import React, { useRef } from "react";
import PropTypes from "prop-types";
import { LaneTitle, NewLaneButtons, Section } from "../styles/Base";
import { AddButton, CancelButton } from "../styles/Elements";
import NewLaneTitleEditor from "../widgets/NewLaneTitleEditor.jsx";
import { v1 } from "uuid";
import { useTranslation } from "react-i18next";
const NewLane = ({ onCancel, onAdd }) => {
const refInput = useRef(null);
const { t } = useTranslation();
const handleSubmit = () => {
onAdd({
id: v1(),
title: getValue()
});
};
const getValue = () => refInput.current.getValue();
// TODO: Commented out because it was never called and it was causing a error
// const onClickOutside = (a, b, c) => {
// if (getValue().length > 0) {
// handleSubmit();
// } else {
// onCancel();
// }
// };
return (
<Section>
<LaneTitle>
<NewLaneTitleEditor
ref={refInput}
placeholder={t("trello.labels.title")}
onCancel={onCancel}
onSave={handleSubmit}
resize="vertical"
border
autoFocus
/>
</LaneTitle>
<NewLaneButtons>
<AddButton onClick={handleSubmit}>{t("trello.labels.add_lane")}</AddButton>
<CancelButton onClick={onCancel}>{t("trello.labels.cancel")}</CancelButton>
</NewLaneButtons>
</Section>
);
};
NewLane.propTypes = {
onCancel: PropTypes.func.isRequired,
onAdd: PropTypes.func.isRequired
};
export default NewLane;

View File

@@ -1,16 +0,0 @@
import React from "react";
import { NewLaneSection } from "../styles/Base";
import { AddLaneLink } from "../styles/Elements";
import { useTranslation } from "react-i18next";
const NewLaneSectionComponent = ({ onClick }) => {
const { t } = useTranslation();
return (
<NewLaneSection>
<AddLaneLink onClick={onClick}>{t("trello.labels.add_lane")}</AddLaneLink>
</NewLaneSection>
);
};
export default NewLaneSectionComponent;

View File

@@ -1,28 +0,0 @@
import LaneHeader from "./Lane/LaneHeader";
import LaneFooter from "./Lane/LaneFooter";
import Card from "./Card";
import Loader from "./Loader.jsx";
import NewLaneForm from "./NewLaneForm.jsx";
import NewCardForm from "./NewCardForm.jsx";
import AddCardLink from "./AddCardLink";
import NewLaneSection from "./NewLaneSection.jsx";
import { BoardWrapper, StyleHorizontal, GlobalStyle, StyleVertical, ScrollableLane, Section } from "../styles/Base";
const exports = {
StyleHorizontal,
StyleVertical,
GlobalStyle,
BoardWrapper,
Loader,
ScrollableLane,
LaneHeader,
LaneFooter,
Section,
NewLaneForm,
NewLaneSection,
NewCardForm,
Card,
AddCardLink
};
export default exports;

View File

@@ -1,28 +0,0 @@
import { BoardContainer } from "../index";
import classNames from "classnames";
import { useState } from "react";
import { v1 } from "uuid";
const Board = ({ id, className, components, orientation, ...additionalProps }) => {
const [storeId] = useState(id || v1());
const allClassNames = classNames("react-trello-board", className || "");
const OrientationStyle = orientation === "horizontal" ? components.StyleHorizontal : components.StyleVertical;
return (
<>
<components.GlobalStyle />
<OrientationStyle>
<BoardContainer
components={components}
orientation={orientation}
{...additionalProps}
id={storeId}
className={allClassNames}
/>
</OrientationStyle>
</>
);
};
export default Board;

View File

@@ -1,331 +0,0 @@
import React, { useCallback, useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import Container from "../dnd/Container";
import Draggable from "../dnd/Draggable";
import PropTypes from "prop-types";
import pick from "lodash/pick";
import isEqual from "lodash/isEqual";
import Lane from "./Lane";
import { PopoverWrapper } from "react-popopo";
import * as actions from "../../../redux/trello/trello.actions.js";
/**
* BoardContainer is a React component that represents a Trello-like board.
* It uses Redux for state management and provides a variety of props to customize its behavior.
*
* @component
* @param {Object} props - Component props
* @param {string} props.id - The unique identifier for the board
* @param {Object} props.components - Custom components to use in the board
* @param {Object} props.data - The initial data for the board
* @param {boolean} props.draggable - Whether the board is draggable
* @param {boolean} props.laneDraggable - Whether the lanes in the board are draggable
* @param {string} props.laneDragClass - The CSS class to apply when a lane is being dragged
* @param {string} props.laneDropClass - The CSS class to apply when a lane is dropped
* @param {Object} props.style - The CSS styles to apply to the board
* @param {Function} props.onDataChange - Callback function when the board data changes
* @param {Function} props.onCardAdd - Callback function when a card is added
* @param {Function} props.onCardUpdate - Callback function when a card is updated
* @param {Function} props.onCardClick - Callback function when a card is clicked
* @param {Function} props.onBeforeCardDelete - Callback function before a card is deleted
* @param {Function} props.onCardDelete - Callback function when a card is deleted
* @param {Function} props.onLaneScroll - Callback function when a lane is scrolled
* @param {Function} props.onLaneClick - Callback function when a lane is clicked
* @param {Function} props.onLaneAdd - Callback function when a lane is added
* @param {Function} props.onLaneDelete - Callback function when a lane is deleted
* @param {Function} props.onLaneUpdate - Callback function when a lane is updated
* @param {boolean} props.editable - Whether the board is editable
* @param {boolean} props.canAddLanes - Whether lanes can be added to the board
* @param {Object} props.laneStyle - The CSS styles to apply to the lanes
* @param {Function} props.onCardMoveAcrossLanes - Callback function when a card is moved across lanes
* @param {string} props.orientation - The orientation of the board ("horizontal" or "vertical")
* @param {Function} props.eventBusHandle - Function to handle events from the event bus
* @param {Function} props.handleLaneDragStart - Callback function when a lane drag starts
* @param {Function} props.handleLaneDragEnd - Callback function when a lane drag ends
* @param {Object} props.reducerData - The initial data for the Redux reducer
* @param {Object} props.cardStyle - The CSS styles to apply to the cards
* @param {Object} props.otherProps - Any other props to pass to the board
* @returns {JSX.Element} A Trello-like board
*/
const BoardContainer = ({
id,
components,
data,
draggable = false,
laneDraggable = true,
laneDragClass = "react_trello_dragLaneClass",
laneDropClass = "react_trello_dragLaneDropClass",
style,
onDataChange = () => {},
onCardAdd = () => {},
onCardUpdate = () => {},
onCardClick = () => {},
onBeforeCardDelete = () => {},
onCardDelete = () => {},
onLaneScroll = () => {},
onLaneClick = () => {},
onLaneAdd = () => {},
onLaneDelete = () => {},
onLaneUpdate = () => {},
editable = false,
canAddLanes = false,
laneStyle,
onCardMoveAcrossLanes = () => {},
orientation = "horizontal",
eventBusHandle,
handleLaneDragStart = () => {},
handleLaneDragEnd = () => {},
reducerData,
cardStyle,
...otherProps
}) => {
const [addLaneMode, setAddLaneMode] = useState(false);
const dispatch = useDispatch();
const currentReducerData = useSelector((state) => (state.trello.lanes ? state.trello : {}));
const groupName = `TrelloBoard${id}`;
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 "MOVE_CARD":
return dispatch(
actions.moveCardAcrossLanes({
fromLaneId: event.fromLaneId,
toLaneId: event.toLaneId,
cardId: event.cardId,
index: event.index
})
);
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));
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(
({ payload }) => {
handleLaneDragStart(payload.id);
},
[handleLaneDragStart]
);
const onLaneDrop = useCallback(
({ removedIndex, addedIndex, payload }) => {
if (removedIndex !== addedIndex) {
dispatch(actions.moveLane({ oldIndex: removedIndex, newIndex: addedIndex }));
handleLaneDragEnd(removedIndex, addedIndex, payload);
}
},
[dispatch, handleLaneDragEnd]
);
const getCardDetails = useCallback(
(laneId, cardIndex) => {
return currentReducerData.lanes.find((lane) => lane.id === laneId).cards[cardIndex];
},
[currentReducerData]
);
const getLaneDetails = useCallback(
(index) => {
return currentReducerData.lanes[index];
},
[currentReducerData]
);
const hideEditableLane = () => {
setAddLaneMode(false);
};
const showEditableLane = () => {
setAddLaneMode(true);
};
const addNewLane = (params) => {
hideEditableLane();
dispatch(actions.addLane(params));
onLaneAdd(params);
};
const passThroughProps = pick(
{
id,
components,
data,
draggable,
laneDraggable,
laneDragClass,
laneDropClass,
style,
onDataChange,
onCardAdd,
onCardUpdate,
onCardClick,
onBeforeCardDelete,
onCardDelete,
onLaneScroll,
onLaneClick,
onLaneAdd,
onLaneDelete,
onLaneUpdate,
editable,
canAddLanes,
laneStyle,
onCardMoveAcrossLanes,
orientation,
eventBusHandle,
handleLaneDragStart,
handleLaneDragEnd,
reducerData,
cardStyle,
...otherProps
},
[
"onCardMoveAcrossLanes",
"onLaneScroll",
"onLaneDelete",
"onLaneUpdate",
"onCardClick",
"onBeforeCardDelete",
"onCardDelete",
"onCardAdd",
"onCardUpdate",
"onLaneClick",
"laneSortFunction",
"draggable",
"laneDraggable",
"cardDraggable",
"collapsibleLanes",
"canAddLanes",
"hideCardDeleteIcon",
"tagStyle",
"handleDragStart",
"handleDragEnd",
"cardDragClass",
"editLaneTitle",
"orientation"
]
);
return (
<components.BoardWrapper style={style} orientation={orientation} draggable={false}>
<PopoverWrapper>
<Container
orientation={orientation === "vertical" ? "vertical" : "horizontal"}
onDragStart={onDragStart}
dragClass={laneDragClass}
dropClass={laneDropClass}
onDrop={onLaneDrop}
lockAxis={orientation === "vertical" ? "y" : "x"}
getChildPayload={(index) => getLaneDetails(index)}
groupName={groupName}
>
{currentReducerData.lanes.map((lane, index) => {
const { id, droppable, ...laneOtherProps } = lane;
const laneToRender = (
<Lane
key={id}
boardId={groupName}
components={components}
id={id}
getCardDetails={getCardDetails}
index={index}
droppable={droppable === undefined ? true : droppable}
style={laneStyle || lane.style || {}}
labelStyle={lane.labelStyle || {}}
cardStyle={cardStyle || lane.cardStyle}
editable={editable && !lane.disallowAddingCard}
{...laneOtherProps}
{...passThroughProps}
/>
);
return draggable || laneDraggable ? <Draggable key={lane.id}>{laneToRender}</Draggable> : laneToRender;
})}
</Container>
</PopoverWrapper>
{canAddLanes && (
<Container orientation={orientation === "vertical" ? "vertical" : "horizontal"}>
{editable && !addLaneMode ? (
<components.NewLaneSection onClick={showEditableLane} />
) : (
addLaneMode && <components.NewLaneForm onCancel={hideEditableLane} onAdd={addNewLane} />
)}
</Container>
)}
</components.BoardWrapper>
);
};
BoardContainer.propTypes = {
id: PropTypes.string,
components: PropTypes.object,
actions: PropTypes.object,
data: PropTypes.object.isRequired,
reducerData: PropTypes.object,
onDataChange: PropTypes.func,
eventBusHandle: PropTypes.func,
onLaneScroll: PropTypes.func,
onCardClick: PropTypes.func,
onBeforeCardDelete: PropTypes.func,
onCardDelete: PropTypes.func,
onCardAdd: PropTypes.func,
onCardUpdate: PropTypes.func,
onLaneAdd: PropTypes.func,
onLaneDelete: PropTypes.func,
onLaneClick: PropTypes.func,
onLaneUpdate: PropTypes.func,
laneSortFunction: PropTypes.func,
draggable: PropTypes.bool,
collapsibleLanes: PropTypes.bool,
editable: PropTypes.bool,
canAddLanes: PropTypes.bool,
hideCardDeleteIcon: PropTypes.bool,
handleDragStart: PropTypes.func,
handleDragEnd: PropTypes.func,
handleLaneDragStart: PropTypes.func,
handleLaneDragEnd: PropTypes.func,
style: PropTypes.object,
tagStyle: PropTypes.object,
laneDraggable: PropTypes.bool,
cardDraggable: PropTypes.bool,
cardDragClass: PropTypes.string,
laneDragClass: PropTypes.string,
laneDropClass: PropTypes.string,
onCardMoveAcrossLanes: PropTypes.func,
orientation: PropTypes.string,
cardStyle: PropTypes.object
};
export default BoardContainer;

View File

@@ -1,408 +0,0 @@
import React, { useCallback, useEffect, useRef, useState } from "react";
import classNames from "classnames";
import PropTypes from "prop-types";
import { bindActionCreators } from "redux";
import { connect } from "react-redux";
import isEqual from "lodash/isEqual";
import cloneDeep from "lodash/cloneDeep";
import { v1 } from "uuid";
import Container from "../dnd/Container.jsx";
import Draggable from "../dnd/Draggable.jsx";
import * as actions from "../../../redux/trello/trello.actions.js";
/**
* Lane is a React component that represents a lane in a Trello-like board.
* It uses Redux for state management and provides a variety of props to customize its behavior.
*
* @component
* @param {Object} props - Component props
* @param {Object} props.actions - Redux actions
* @param {string} props.id - The unique identifier for the lane
* @param {string} props.boardId - The unique identifier for the board
* @param {string} props.title - The title of the lane
* @param {number} props.index - The index of the lane
* @param {Function} props.laneSortFunction - Function to sort the cards in the lane
* @param {Object} props.style - The CSS styles to apply to the lane
* @param {Object} props.cardStyle - The CSS styles to apply to the cards
* @param {Object} props.tagStyle - The CSS styles to apply to the tags
* @param {Object} props.titleStyle - The CSS styles to apply to the title
* @param {Object} props.labelStyle - The CSS styles to apply to the label
* @param {Array} props.cards - The cards in the lane
* @param {string} props.label - The label of the lane
* @param {boolean} props.draggable - Whether the lane is draggable
* @param {boolean} props.collapsibleLanes - Whether the lanes are collapsible
* @param {boolean} props.droppable - Whether the lane is droppable
* @param {Function} props.onCardMoveAcrossLanes - Callback function when a card is moved across lanes
* @param {Function} props.onCardClick - Callback function when a card is clicked
* @param {Function} props.onBeforeCardDelete - Callback function before a card is deleted
* @param {Function} props.onCardDelete - Callback function when a card is deleted
* @param {Function} props.onCardAdd - Callback function when a card is added
* @param {Function} props.onCardUpdate - Callback function when a card is updated
* @param {Function} props.onLaneDelete - Callback function when a lane is deleted
* @param {Function} props.onLaneUpdate - Callback function when a lane is updated
* @param {Function} props.onLaneClick - Callback function when a lane is clicked
* @param {Function} props.onLaneScroll - Callback function when a lane is scrolled
* @param {boolean} props.editable - Whether the lane is editable
* @param {boolean} props.laneDraggable - Whether the lane is draggable
* @param {boolean} props.cardDraggable - Whether the cards are draggable
* @param {string} props.cardDragClass - The CSS class to apply when a card is being dragged
* @param {string} props.cardDropClass - The CSS class to apply when a card is dropped
* @param {boolean} props.canAddLanes - Whether lanes can be added to the board
* @param {boolean} props.hideCardDeleteIcon - Whether to hide the card delete icon
* @param {Object} props.components - Custom components to use in the lane
* @param {Function} props.getCardDetails - Function to get the details of a card
* @param {Function} props.handleDragStart - Callback function when a drag starts
* @param {Function} props.handleDragEnd - Callback function when a drag ends
* @param {string} props.orientation - The orientation of the lane ("horizontal" or "vertical")
* @param {string} props.className - The CSS class to apply to the lane
* @param {number} props.currentPage - The current page of the lane
* @param {Object} props.otherProps - Any other props to pass to the lane
* @returns {JSX.Element} A lane in a Trello-like board
*/
function Lane({
actions,
id,
boardId,
title,
index,
laneSortFunction,
style = {},
cardStyle = {},
tagStyle = {},
titleStyle = {},
labelStyle = {},
cards,
label,
draggable = false,
collapsibleLanes = false,
droppable = true,
onCardMoveAcrossLanes = () => {},
onCardClick = () => {},
onBeforeCardDelete = () => {},
onCardDelete = () => {},
onCardAdd = () => {},
onCardUpdate = () => {},
onLaneDelete = () => {},
onLaneUpdate = () => {},
onLaneClick = () => {},
onLaneScroll = () => {},
editable = false,
laneDraggable = false,
cardDraggable = true,
cardDragClass,
cardDropClass,
canAddLanes = false,
hideCardDeleteIcon = false,
components = {},
getCardDetails,
handleDragStart = () => {},
handleDragEnd = () => {},
orientation = "vertical",
className,
currentPage,
...otherProps
}) {
const [loading, setLoading] = useState(false);
const [currentPageFinal, setCurrentPageFinal] = useState(currentPage);
const [addCardMode, setAddCardMode] = useState(false);
const [collapsed, setCollapsed] = useState(false);
const [isDraggingOver, setIsDraggingOver] = useState(false);
const laneRef = useRef(null);
useEffect(() => {
if (!isEqual(cards, currentPageFinal)) {
setCurrentPageFinal(currentPage);
}
}, [cards, currentPage, currentPageFinal]);
const handleScroll = useCallback(
(evt) => {
const node = evt.target;
const elemScrollPosition = node.scrollHeight - node.scrollTop - node.clientHeight;
if (elemScrollPosition < 1 && onLaneScroll && !loading) {
const nextPage = currentPageFinal + 1;
setLoading(true);
onLaneScroll(nextPage, id).then((moreCards) => {
if ((moreCards || []).length > 0) {
actions.paginateLane({
laneId: id,
newCards: moreCards,
nextPage: nextPage
});
}
setLoading(false);
});
}
},
[currentPageFinal, loading, onLaneScroll, id, actions]
);
useEffect(() => {
const node = laneRef.current;
if (node) {
node.addEventListener("scroll", handleScroll);
}
return () => {
if (node) {
node.removeEventListener("scroll", handleScroll);
}
};
}, [handleScroll]);
const sortCards = (cards, sortFunction) => {
if (!cards) return [];
if (!sortFunction) return cards;
return cards.concat().sort((card1, card2) => sortFunction(card1, card2));
};
const removeCard = (cardId) => {
if (onBeforeCardDelete && typeof onBeforeCardDelete === "function") {
onBeforeCardDelete(() => {
onCardDelete && onCardDelete(cardId, id);
actions.removeCard({ laneId: id, cardId: cardId });
});
} else {
onCardDelete && onCardDelete(cardId, id);
actions.removeCard({ laneId: id, cardId: cardId });
}
};
const handleCardClick = (e, card) => {
onCardClick && onCardClick(card.id, card.metadata, card.laneId);
e.stopPropagation();
};
const showEditableCard = () => {
setAddCardMode(true);
};
const hideEditableCard = () => {
setAddCardMode(false);
};
const addNewCard = (params) => {
const laneId = id;
const newCardId = v1();
hideEditableCard();
let card = { id: newCardId, ...params };
actions.addCard({ laneId, card });
onCardAdd(card, laneId);
};
const onDragStart = ({ payload }) => {
handleDragStart && handleDragStart(payload.id, payload.laneId);
};
const shouldAcceptDrop = (sourceContainerOptions) => {
return droppable && sourceContainerOptions.groupName === groupName;
};
const onDragEnd = (laneId, result) => {
const { addedIndex, payload } = result;
if (isDraggingOver) {
setIsDraggingOver(false);
}
if (addedIndex != null) {
const newCard = { ...cloneDeep(payload), laneId };
const response = handleDragEnd ? handleDragEnd(payload.id, payload.laneId, laneId, addedIndex, newCard) : true;
if (response === undefined || !!response) {
actions.moveCardAcrossLanes({
fromLaneId: payload.laneId,
toLaneId: laneId,
cardId: payload.id,
index: addedIndex
});
onCardMoveAcrossLanes(payload.laneId, laneId, payload.id, addedIndex);
}
return response;
}
};
const updateCard = (updatedCard) => {
actions.updateCard({ laneId: id, card: updatedCard });
onCardUpdate(id, updatedCard);
};
const removeLane = () => {
actions.removeLane({ laneId: id });
onLaneDelete(id);
};
const updateTitle = (value) => {
actions.updateLane({ id, title: value });
onLaneUpdate(id, { title: value });
};
const toggleLaneCollapsed = () => {
collapsibleLanes && setCollapsed(!collapsed);
};
const groupName = `TrelloBoard${boardId}Lane`;
const renderDragContainer = (isDraggingOver) => {
const stableCards = collapsed ? [] : cards;
const cardList = sortCards(stableCards, laneSortFunction).map((card, idx) => {
const onDeleteCard = () => removeCard(card.id);
const cardToRender = (
<components.Card
key={card.id}
index={idx}
style={card.style || cardStyle}
className="react-trello-card"
onDelete={onDeleteCard}
onClick={(e) => handleCardClick(e, card)}
onChange={(updatedCard) => updateCard(updatedCard)}
showDeleteButton={!hideCardDeleteIcon}
tagStyle={tagStyle}
cardDraggable={cardDraggable}
editable={editable}
{...card}
/>
);
return cardDraggable && (!card.hasOwnProperty("draggable") || card.draggable) ? (
<Draggable key={card.id}>{cardToRender}</Draggable>
) : (
<span key={card.id}>{cardToRender}</span>
);
});
return (
<components.ScrollableLane ref={laneRef} isDraggingOver={isDraggingOver}>
<Container
orientation={orientation === "horizontal" ? "vertical" : "horizontal"}
groupName={groupName}
dragClass={cardDragClass}
dropClass={cardDropClass}
onDragStart={onDragStart}
onDrop={(e) => onDragEnd(id, e)}
onDragEnter={() => setIsDraggingOver(true)}
onDragLeave={() => setIsDraggingOver(false)}
shouldAcceptDrop={shouldAcceptDrop}
getChildPayload={(index) => getCardDetails(id, index)}
>
{cardList}
</Container>
{editable && !addCardMode && <components.AddCardLink onClick={showEditableCard} laneId={id} />}
{addCardMode && <components.NewCardForm onCancel={hideEditableCard} laneId={id} onAdd={addNewCard} />}
</components.ScrollableLane>
);
};
const renderHeader = (pickedProps) => {
return (
<components.LaneHeader
{...pickedProps}
onDelete={removeLane}
onDoubleClick={toggleLaneCollapsed}
updateTitle={updateTitle}
/>
);
};
const allClassNames = classNames("react-trello-lane", collapsed ? "lane-collapsed" : "", className || "");
const showFooter = collapsibleLanes && cards.length > 0;
const passedProps = {
actions,
id,
boardId,
title,
index,
laneSortFunction,
style,
cardStyle,
tagStyle,
titleStyle,
labelStyle,
cards,
label,
draggable,
collapsibleLanes,
droppable,
editable,
laneDraggable,
cardDraggable,
cardDragClass,
cardDropClass,
canAddLanes,
hideCardDeleteIcon,
components,
getCardDetails,
handleDragStart,
handleDragEnd,
orientation,
className,
currentPage,
...otherProps
};
return (
<components.Section
key={id}
onClick={() => onLaneClick && onLaneClick(id)}
draggable={false}
className={allClassNames}
orientation={orientation}
{...passedProps}
>
{renderHeader({ id, cards, ...passedProps })}
{renderDragContainer(isDraggingOver)}
{loading && <components.Loader />}
{showFooter && <components.LaneFooter onClick={toggleLaneCollapsed} collapsed={collapsed} />}
</components.Section>
);
}
Lane.propTypes = {
actions: PropTypes.object,
id: PropTypes.string.isRequired,
boardId: PropTypes.string,
title: PropTypes.node,
index: PropTypes.number,
laneSortFunction: PropTypes.func,
style: PropTypes.object,
cardStyle: PropTypes.object,
tagStyle: PropTypes.object,
titleStyle: PropTypes.object,
labelStyle: PropTypes.object,
cards: PropTypes.array,
label: PropTypes.string,
currentPage: PropTypes.number,
draggable: PropTypes.bool,
collapsibleLanes: PropTypes.bool,
droppable: PropTypes.bool,
onCardMoveAcrossLanes: PropTypes.func,
onCardClick: PropTypes.func,
onBeforeCardDelete: PropTypes.func,
onCardDelete: PropTypes.func,
onCardAdd: PropTypes.func,
onCardUpdate: PropTypes.func,
onLaneDelete: PropTypes.func,
onLaneUpdate: PropTypes.func,
onLaneClick: PropTypes.func,
onLaneScroll: PropTypes.func,
editable: PropTypes.bool,
laneDraggable: PropTypes.bool,
cardDraggable: PropTypes.bool,
cardDragClass: PropTypes.string,
cardDropClass: PropTypes.string,
canAddLanes: PropTypes.bool,
hideCardDeleteIcon: PropTypes.bool,
components: PropTypes.object,
getCardDetails: PropTypes.func,
handleDragStart: PropTypes.func,
handleDragEnd: PropTypes.func,
orientation: PropTypes.string
};
const mapDispatchToProps = (dispatch) => ({
actions: bindActionCreators(actions, dispatch)
});
export default connect(null, mapDispatchToProps)(Lane);

View File

@@ -1,111 +0,0 @@
import React, { Component } from "react";
import PropTypes from "prop-types";
import container, { dropHandlers } from "../smooth-dnd";
container.dropHandler = dropHandlers.reactDropHandler().handler;
container.wrapChild = (p) => p; // don't wrap children they will already be wrapped
class Container extends Component {
constructor(props) {
super(props);
this.getContainerOptions = this.getContainerOptions.bind(this);
this.setRef = this.setRef.bind(this);
this.prevContainer = null;
}
componentDidMount() {
this.prevContainer = this.containerDiv;
this.container = container(this.containerDiv, this.getContainerOptions());
}
componentWillUnmount() {
this.container.dispose();
this.container = null;
}
componentDidUpdate() {
if (this.containerDiv) {
if (this.prevContainer && this.prevContainer !== this.containerDiv) {
this.container.dispose();
this.container = container(this.containerDiv, this.getContainerOptions());
this.prevContainer = this.containerDiv;
}
}
}
render() {
if (this.props.render) {
return this.props.render(this.setRef);
} else {
return (
<div style={this.props.style} ref={this.setRef}>
{this.props.children}
</div>
);
}
}
setRef(element) {
this.containerDiv = element;
}
getContainerOptions() {
const functionProps = {};
const propKeys = [
"onDragStart",
"onDragEnd",
"onDrop",
"getChildPayload",
"shouldAnimateDrop",
"shouldAcceptDrop",
"onDragEnter",
"onDragLeave",
"render",
"onDropReady",
"getGhostParent"
];
propKeys.forEach((key) => {
if (this.props[key]) {
functionProps[key] = (...p) => this.props[key](...p);
}
});
return { ...this.props, ...functionProps };
}
}
Container.propTypes = {
behaviour: PropTypes.oneOf(["move", "copy", "drag-zone"]),
groupName: PropTypes.string,
orientation: PropTypes.oneOf(["horizontal", "vertical"]),
style: PropTypes.object,
dragHandleSelector: PropTypes.string,
className: PropTypes.string,
nonDragAreaSelector: PropTypes.string,
dragBeginDelay: PropTypes.number,
animationDuration: PropTypes.number,
autoScrollEnabled: PropTypes.string,
lockAxis: PropTypes.string,
dragClass: PropTypes.string,
dropClass: PropTypes.string,
onDragStart: PropTypes.func,
onDragEnd: PropTypes.func,
onDrop: PropTypes.func,
getChildPayload: PropTypes.func,
shouldAnimateDrop: PropTypes.func,
shouldAcceptDrop: PropTypes.func,
onDragEnter: PropTypes.func,
onDragLeave: PropTypes.func,
render: PropTypes.func,
getGhostParent: PropTypes.func,
removeOnDropOut: PropTypes.bool
};
Container.defaultProps = {
behaviour: "move",
orientation: "vertical",
className: "reactTrelloBoard"
};
export default Container;

View File

@@ -1,34 +0,0 @@
import React, { Component } from "react";
import PropTypes from "prop-types";
import { constants } from "../smooth-dnd";
const { wrapperClass } = constants;
class Draggable extends Component {
render() {
const { render, className, children, ...restProps } = this.props;
try {
if (render) {
return React.cloneElement(render(), { className: wrapperClass });
}
const clsName = className ? `${className} ` : "";
return (
<div {...restProps} className={`${clsName}${wrapperClass}`}>
{children}
</div>
);
} catch (error) {
console.error("Error rendering Draggable component:", error);
return null; // Return null if an error occurs to prevent crashing
}
}
}
Draggable.propTypes = {
render: PropTypes.func,
className: PropTypes.string,
children: PropTypes.node
};
export default Draggable;

View File

@@ -1,118 +0,0 @@
import update from "immutability-helper";
const updateLanes = (state, lanes) => update(state, { lanes: { $set: lanes } });
const updateLaneCards = (lane, cards) => update(lane, { cards: { $set: cards } });
const LaneHelper = {
initialiseLanes: (state, { lanes }) => {
const newLanes = lanes.map((lane) => {
lane.currentPage = 1;
lane.cards && lane.cards.forEach((c) => (c.laneId = lane.id));
return lane;
});
return updateLanes(state, newLanes);
},
paginateLane: (state, { laneId, newCards, nextPage }) => {
const updatedLanes = LaneHelper.appendCardsToLane(state, { laneId: laneId, newCards: newCards });
updatedLanes.find((lane) => lane.id === laneId).currentPage = nextPage;
return updateLanes(state, updatedLanes);
},
appendCardsToLane: (state, { laneId, newCards, index }) => {
const lane = state.lanes.find((lane) => lane.id === laneId);
newCards = newCards
.map((c) => update(c, { laneId: { $set: laneId } }))
.filter((c) => lane.cards.find((card) => card.id === c.id) == null);
return state.lanes.map((lane) => {
if (lane.id === laneId) {
const cardsToUpdate =
index !== undefined
? [...lane.cards.slice(0, index), ...newCards, ...lane.cards.slice(index)]
: [...lane.cards, ...newCards];
return updateLaneCards(lane, cardsToUpdate);
} else {
return lane;
}
});
},
appendCardToLane: (state, { laneId, card, index }) => {
const newLanes = LaneHelper.appendCardsToLane(state, { laneId: laneId, newCards: [card], index });
return updateLanes(state, newLanes);
},
addLane: (state, lane) => {
const newLane = { cards: [], ...lane };
return updateLanes(state, [...state.lanes, newLane]);
},
updateLane: (state, updatedLane) => {
const newLanes = state.lanes.map((lane) => (updatedLane.id === lane.id ? { ...lane, ...updatedLane } : lane));
return updateLanes(state, newLanes);
},
removeCardFromLane: (state, { laneId, cardId }) => {
const lanes = state.lanes.map((lane) => {
if (lane.id === laneId) {
const newCards = lane.cards.filter((card) => card.id !== cardId);
return updateLaneCards(lane, newCards);
} else {
return lane;
}
});
return updateLanes(state, lanes);
},
moveCardAcrossLanes: (state, { fromLaneId, toLaneId, cardId, index }) => {
let cardToMove = null;
const interimLanes = state.lanes.map((lane) => {
if (lane.id === fromLaneId) {
cardToMove = lane.cards.find((card) => card.id === cardId);
const newCards = lane.cards.filter((card) => card.id !== cardId);
return updateLaneCards(lane, newCards);
} else {
return lane;
}
});
return LaneHelper.appendCardToLane(
{ ...state, lanes: interimLanes },
{
laneId: toLaneId,
card: cardToMove,
index: index
}
);
},
updateCardsForLane: (state, { laneId, cards }) => {
const lanes = state.lanes.map((lane) => (lane.id === laneId ? updateLaneCards(lane, cards) : lane));
return updateLanes(state, lanes);
},
updateCardForLane: (state, { laneId, card: updatedCard }) => {
const lanes = state.lanes.map((lane) => {
if (lane.id === laneId) {
const cards = lane.cards.map((card) => (card.id === updatedCard.id ? { ...card, ...updatedCard } : card));
return updateLaneCards(lane, cards);
} else {
return lane;
}
});
return updateLanes(state, lanes);
},
moveLane: (state, { oldIndex, newIndex }) => {
const laneToMove = state.lanes[oldIndex];
const tempState = update(state, { lanes: { $splice: [[oldIndex, 1]] } });
return update(tempState, { lanes: { $splice: [[newIndex, 0, laneToMove]] } });
},
removeLane: (state, { laneId }) => {
const updatedLanes = state.lanes.filter((lane) => lane.id !== laneId);
return updateLanes(state, updatedLanes);
}
};
export default LaneHelper;

View File

@@ -1,35 +0,0 @@
import React from "react";
import Draggable from "./dnd/Draggable.jsx";
import Container from "./dnd/Container.jsx";
import BoardContainer from "./controllers/BoardContainer.jsx";
import Board from "./controllers/Board.jsx";
import Lane from "./controllers/Lane.jsx";
import DefaultComponents from "./components";
import widgets from "./widgets/index";
import { StyleSheetManager } from "styled-components";
import isPropValid from "@emotion/is-prop-valid";
export { Draggable, Container, BoardContainer, Lane, widgets };
export { DefaultComponents as components };
// Enhanced default export using arrow function for simplicity
const TrelloBoard = ({ components, ...otherProps }) => {
return (
<StyleSheetManager shouldForwardProp={shouldForwardProp}>
<Board components={{ ...DefaultComponents, ...components }} {...otherProps} />
</StyleSheetManager>
);
};
const shouldForwardProp = (propName, target) => {
if (typeof target === "string") {
return isPropValid(propName);
}
return true;
};
export default TrelloBoard;

View File

@@ -1,8 +0,0 @@
import container from './src/container';
import * as constants from './src/constants';
import * as dropHandlers from './src/dropHandlers';
export default container;
export {
constants,
dropHandlers,
};

View File

@@ -1,21 +0,0 @@
export const containerInstance = 'smooth-dnd-container-instance';
export const containersInDraggable = 'smooth-dnd-containers-in-draggable';
export const defaultGroupName = '@@smooth-dnd-default-group@@';
export const wrapperClass = 'smooth-dnd-draggable-wrapper';
export const defaultGrabHandleClass = 'smooth-dnd-default-grap-handle';
export const animationClass = 'animated';
export const translationValue = '__smooth_dnd_draggable_translation_value';
export const visibilityValue = '__smooth_dnd_draggable_visibility_value';
export const ghostClass = 'smooth-dnd-ghost';
export const containerClass = 'smooth-dnd-container';
export const extraSizeForInsertion = 'smooth-dnd-extra-size-for-insertion';
export const stretcherElementClass = 'smooth-dnd-stretcher-element';
export const stretcherElementInstance = 'smooth-dnd-stretcher-instance';
export const isDraggableDetached = 'smoth-dnd-is-draggable-detached';
export const disbaleTouchActions = 'smooth-dnd-disable-touch-action';
export const noUserSelectClass = 'smooth-dnd-no-user-select';

View File

@@ -1,61 +0,0 @@
.smooth-dnd-container *{
box-sizing: border-box;
}
.smooth-dnd-disable-touch-action{
touch-action: none;
}
.smooth-dnd-container{
position: relative;
}
.smooth-dnd-container.vertical{
}
.smooth-dnd-container.horizontal{
white-space: nowrap;
}
.smooth-dnd-container.horizontal .smooth-dnd-draggable-wrapper{
height: 100%;
display: inline-block;
}
.smooth-dnd-draggable-wrapper {
overflow: hidden;
}
.smooth-dnd-draggable-wrapper.animated{
transition: transform ease;
}
.smooth-dnd-ghost {
}
.smooth-dnd-ghost *{
box-sizing: border-box;
}
.smooth-dnd-ghost.animated{
transition: all ease-in-out;
}
/* .smooth-dnd-no-user-select{
user-select: none;
}
.smooth-dnd-stretcher-element{
background-color: transparent;
}
.smooth-dnd-stretcher-element.vertical{
height: 1px;
}
.smooth-dnd-stretcher-element.horizontal{
height: 100%;
display: inline-block;
} */

View File

@@ -1,777 +0,0 @@
import Mediator from './mediator';
import layoutManager from './layoutManager';
import { hasClass, addClass, removeClass, getParent } from './utils';
import { domDropHandler } from './dropHandlers';
import {
defaultGroupName,
wrapperClass,
animationClass,
stretcherElementClass,
stretcherElementInstance,
translationValue,
containerClass,
containerInstance,
containersInDraggable
} from './constants';
const defaultOptions = {
groupName: null,
behaviour: 'move', // move | copy
orientation: 'vertical', // vertical | horizontal
getChildPayload: null,
animationDuration: 250,
autoScrollEnabled: true,
shouldAcceptDrop: null,
shouldAnimateDrop: null
};
function setAnimation(element, add, animationDuration) {
if (add) {
addClass(element, animationClass);
element.style.transitionDuration = animationDuration + 'ms';
} else {
removeClass(element, animationClass);
element.style.removeProperty('transition-duration');
}
}
function getContainer(element) {
return element ? element[containerInstance] : null;
}
function initOptions(props = defaultOptions) {
return Object.assign({}, defaultOptions, props);
}
function isDragRelevant({ element, options }) {
return function(sourceContainer, payload) {
if (options.shouldAcceptDrop) {
return options.shouldAcceptDrop(sourceContainer.getOptions(), payload);
}
const sourceOptions = sourceContainer.getOptions();
if (options.behaviour === 'copy') return false;
const parentWrapper = getParent(element, '.' + wrapperClass);
if (parentWrapper === sourceContainer.element) {
return false;
}
if (sourceContainer.element === element) return true;
if (sourceOptions.groupName && sourceOptions.groupName === options.groupName) return true;
return false;
};
}
function wrapChild(child) {
if (SmoothDnD.wrapChild) {
return SmoothDnD.wrapChild(child);
}
const div = global.document.createElement('div');
div.className = `${wrapperClass}`;
child.parentElement.insertBefore(div, child);
div.appendChild(child);
return div;
}
function wrapChildren(element) {
const draggables = [];
Array.prototype.map.call(element.children, child => {
if (child.nodeType === Node.ELEMENT_NODE) {
let wrapper = child;
if (!hasClass(child, wrapperClass)) {
wrapper = wrapChild(child);
}
wrapper[containersInDraggable] = [];
wrapper[translationValue] = 0;
draggables.push(wrapper);
} else {
if (typeof element.removeChild === "function") {
element.removeChild(child);
}
}
});
return draggables;
}
function unwrapChildren(element) {
Array.prototype.map.call(element.children, child => {
if (child.nodeType === Node.ELEMENT_NODE) {
let wrapper = child;
if (hasClass(child, wrapperClass)) {
element.insertBefore(wrapper, wrapChild.firstElementChild);
element.removeChild(wrapper);
}
}
});
}
function findDraggebleAtPos({ layout }) {
const find = (draggables, pos, startIndex, endIndex, withRespectToMiddlePoints = false) => {
if (endIndex < startIndex) {
return startIndex;
}
// binary serach draggable
if (startIndex === endIndex) {
let { begin, end } = layout.getBeginEnd(draggables[startIndex]);
// mouse pos is inside draggable
// now decide which index to return
if (pos > begin && pos <= end) {
if (withRespectToMiddlePoints) {
return pos < (end + begin) / 2 ? startIndex : startIndex + 1;
} else {
return startIndex;
}
} else {
return null;
}
} else {
const middleIndex = Math.floor((endIndex + startIndex) / 2);
const { begin, end } = layout.getBeginEnd(draggables[middleIndex]);
if (pos < begin) {
return find(draggables, pos, startIndex, middleIndex - 1, withRespectToMiddlePoints);
} else if (pos > end) {
return find(draggables, pos, middleIndex + 1, endIndex, withRespectToMiddlePoints);
} else {
if (withRespectToMiddlePoints) {
return pos < (end + begin) / 2 ? middleIndex : middleIndex + 1;
} else {
return middleIndex;
}
}
}
};
return (draggables, pos, withRespectToMiddlePoints = false) => {
return find(draggables, pos, 0, draggables.length - 1, withRespectToMiddlePoints);
};
}
function resetDraggables({ element, draggables, layout, options }) {
return function() {
draggables.forEach(p => {
setAnimation(p, false);
layout.setTranslation(p, 0);
layout.setVisibility(p, true);
p[containersInDraggable] = [];
});
if (element[stretcherElementInstance]) {
element[stretcherElementInstance].parentNode.removeChild(element[stretcherElementInstance]);
element[stretcherElementInstance] = null;
}
};
}
function setTargetContainer(draggableInfo, element, set = true) {
if (element && set) {
draggableInfo.targetElement = element;
} else {
if (draggableInfo.targetElement === element) {
draggableInfo.targetElement = null;
}
}
}
function handleDrop({ element, draggables, layout, options }) {
const draggablesReset = resetDraggables({ element, draggables, layout, options });
const dropHandler = (SmoothDnD.dropHandler || domDropHandler)({ element, draggables, layout, options });
return function(draggableInfo, { addedIndex, removedIndex }) {
draggablesReset();
// if drop zone is valid => complete drag else do nothing everything will be reverted by draggablesReset()
if (draggableInfo.targetElement || options.removeOnDropOut) {
let actualAddIndex =
addedIndex !== null ? (removedIndex !== null && removedIndex < addedIndex ? addedIndex - 1 : addedIndex) : null;
const dropHandlerParams = {
removedIndex,
addedIndex: actualAddIndex,
payload: draggableInfo.payload,
droppedElement: draggableInfo.element.firstElementChild
};
dropHandler(dropHandlerParams, options.onDrop);
}
};
}
function getContainerProps(element, initialOptions) {
const options = initOptions(initialOptions);
const draggables = wrapChildren(element, options.orientation, options.animationDuration);
// set flex classes before layout is inited for scroll listener
addClass(element, `${containerClass} ${options.orientation}`);
const layout = layoutManager(element, options.orientation, options.animationDuration);
return {
element,
draggables,
options,
layout
};
}
function getRelaventParentContainer(container, relevantContainers) {
let current = container.element;
while (current) {
const containerOfParentElement = getContainer(current.parentElement);
if (containerOfParentElement && relevantContainers.indexOf(containerOfParentElement) > -1) {
return {
container: containerOfParentElement,
draggable: current
};
}
current = current.parentElement;
}
return null;
}
function registerToParentContainer(container, relevantContainers) {
const parentInfo = getRelaventParentContainer(container, relevantContainers);
if (parentInfo) {
parentInfo.container.getChildContainers().push(container);
container.setParentContainer(parentInfo.container);
//current should be draggable
parentInfo.draggable[containersInDraggable].push(container);
}
}
function getRemovedItem({ draggables, element, options }) {
let prevRemovedIndex = null;
return ({ draggableInfo, dragResult }) => {
let removedIndex = prevRemovedIndex;
if (prevRemovedIndex == null && draggableInfo.container.element === element && options.behaviour !== 'copy') {
removedIndex = prevRemovedIndex = draggableInfo.elementIndex;
}
return { removedIndex };
};
}
function setRemovedItemVisibilty({ draggables, layout }) {
return ({ draggableInfo, dragResult }) => {
if (dragResult.removedIndex !== null) {
layout.setVisibility(draggables[dragResult.removedIndex], false);
}
};
}
function getPosition({ element, layout }) {
return ({ draggableInfo }) => {
return {
pos: !getContainer(element).isPosInChildContainer() ? layout.getPosition(draggableInfo.position) : null
};
};
}
function notifyParentOnPositionCapture({ element }) {
let isCaptured = false;
return ({ draggableInfo, dragResult }) => {
if (getContainer(element).getParentContainer() && isCaptured !== (dragResult.pos !== null)) {
isCaptured = dragResult.pos !== null;
getContainer(element)
.getParentContainer()
.onChildPositionCaptured(isCaptured);
}
};
}
function getElementSize({ layout }) {
let elementSize = null;
return ({ draggableInfo, dragResult }) => {
if (dragResult.pos === null) {
return (elementSize = null);
} else {
elementSize = elementSize || layout.getSize(draggableInfo.element);
}
return { elementSize };
};
}
function handleTargetContainer({ element }) {
return ({ draggableInfo, dragResult }) => {
setTargetContainer(draggableInfo, element, !!dragResult.pos);
};
}
function getDragInsertionIndex({ draggables, layout }) {
const findDraggable = findDraggebleAtPos({ layout });
return ({ dragResult: { shadowBeginEnd, pos } }) => {
if (!shadowBeginEnd) {
const index = findDraggable(draggables, pos, true);
return index !== null ? index : draggables.length;
} else {
if (shadowBeginEnd.begin + shadowBeginEnd.beginAdjustment <= pos && shadowBeginEnd.end >= pos) {
// position inside ghost
return null;
}
}
if (pos < shadowBeginEnd.begin + shadowBeginEnd.beginAdjustment) {
return findDraggable(draggables, pos);
} else if (pos > shadowBeginEnd.end) {
return findDraggable(draggables, pos) + 1;
} else {
return draggables.length;
}
};
}
function getDragInsertionIndexForDropZone({ draggables, layout }) {
return ({ dragResult: { pos } }) => {
return pos !== null ? { addedIndex: 0 } : { addedIndex: null };
};
}
function getShadowBeginEndForDropZone({ draggables, layout }) {
let prevAddedIndex = null;
return ({ dragResult: { addedIndex } }) => {
if (addedIndex !== prevAddedIndex) {
prevAddedIndex = addedIndex;
const { begin, end } = layout.getBeginEndOfContainer();
return {
shadowBeginEnd: {
rect: layout.getTopLeftOfElementBegin(begin, end)
}
};
}
};
}
function invalidateShadowBeginEndIfNeeded(params) {
const shadowBoundsGetter = getShadowBeginEnd(params);
return ({ draggableInfo, dragResult }) => {
if (draggableInfo.invalidateShadow) {
return shadowBoundsGetter({ draggableInfo, dragResult });
}
return null;
};
}
function getNextAddedIndex(params) {
const getIndexForPos = getDragInsertionIndex(params);
return ({ dragResult }) => {
let index = null;
if (dragResult.pos !== null) {
index = getIndexForPos({ dragResult });
if (index === null) {
index = dragResult.addedIndex;
}
}
return {
addedIndex: index
};
};
}
function resetShadowAdjustment() {
let lastAddedIndex = null;
return ({ dragResult: { addedIndex, shadowBeginEnd } }) => {
if (addedIndex !== lastAddedIndex && lastAddedIndex !== null && shadowBeginEnd) {
shadowBeginEnd.beginAdjustment = 0;
}
lastAddedIndex = addedIndex;
};
}
function handleInsertionSizeChange({ element, draggables, layout, options }) {
let strectherElement = null;
return function({ dragResult: { addedIndex, removedIndex, elementSize } }) {
if (removedIndex === null) {
if (addedIndex !== null) {
if (!strectherElement) {
const containerBeginEnd = layout.getBeginEndOfContainer();
containerBeginEnd.end = containerBeginEnd.begin + layout.getSize(element);
const hasScrollBar = layout.getScrollSize(element) > layout.getSize(element);
const containerEnd = hasScrollBar
? containerBeginEnd.begin + layout.getScrollSize(element) - layout.getScrollValue(element)
: containerBeginEnd.end;
const lastDraggableEnd =
draggables.length > 0
? layout.getBeginEnd(draggables[draggables.length - 1]).end -
draggables[draggables.length - 1][translationValue]
: containerBeginEnd.begin;
if (lastDraggableEnd + elementSize > containerEnd) {
strectherElement = global.document.createElement('div');
strectherElement.className = stretcherElementClass + ' ' + options.orientation;
const stretcherSize = elementSize + lastDraggableEnd - containerEnd;
layout.setSize(strectherElement.style, `${stretcherSize}px`);
element.appendChild(strectherElement);
element[stretcherElementInstance] = strectherElement;
return {
containerBoxChanged: true
};
}
}
} else {
if (strectherElement) {
layout.setTranslation(strectherElement, 0);
let toRemove = strectherElement;
strectherElement = null;
element.removeChild(toRemove);
element[stretcherElementInstance] = null;
return {
containerBoxChanged: true
};
}
}
}
};
}
function calculateTranslations({ element, draggables, layout }) {
let prevAddedIndex = null;
let prevRemovedIndex = null;
return function({ dragResult: { addedIndex, removedIndex, elementSize } }) {
if (addedIndex !== prevAddedIndex || removedIndex !== prevRemovedIndex) {
for (let index = 0; index < draggables.length; index++) {
if (index !== removedIndex) {
const draggable = draggables[index];
let translate = 0;
if (removedIndex !== null && removedIndex < index) {
translate -= layout.getSize(draggables[removedIndex]);
}
if (addedIndex !== null && addedIndex <= index) {
translate += elementSize;
}
layout.setTranslation(draggable, translate);
}
}
prevAddedIndex = addedIndex;
prevRemovedIndex = removedIndex;
return { addedIndex, removedIndex };
}
};
}
function getShadowBeginEnd({ draggables, layout }) {
let prevAddedIndex = null;
return ({ draggableInfo, dragResult }) => {
const { addedIndex, removedIndex, elementSize, pos, shadowBeginEnd } = dragResult;
if (pos !== null) {
if (addedIndex !== null && (draggableInfo.invalidateShadow || addedIndex !== prevAddedIndex)) {
if (prevAddedIndex) prevAddedIndex = addedIndex;
let beforeIndex = addedIndex - 1;
let begin = 0;
let afterBounds = null;
let beforeBounds = null;
if (beforeIndex === removedIndex) {
beforeIndex--;
}
if (beforeIndex > -1) {
const beforeSize = layout.getSize(draggables[beforeIndex]);
beforeBounds = layout.getBeginEnd(draggables[beforeIndex]);
if (elementSize < beforeSize) {
const threshold = (beforeSize - elementSize) / 2;
begin = beforeBounds.end - threshold;
} else {
begin = beforeBounds.end;
}
} else {
beforeBounds = { end: layout.getBeginEndOfContainer().begin };
}
let end = 10000;
let afterIndex = addedIndex;
if (afterIndex === removedIndex) {
afterIndex++;
}
if (afterIndex < draggables.length) {
const afterSize = layout.getSize(draggables[afterIndex]);
afterBounds = layout.getBeginEnd(draggables[afterIndex]);
if (elementSize < afterSize) {
const threshold = (afterSize - elementSize) / 2;
end = afterBounds.begin + threshold;
} else {
end = afterBounds.begin;
}
} else {
afterBounds = { begin: layout.getContainerRectangles().end };
}
const shadowRectTopLeft =
beforeBounds && afterBounds ? layout.getTopLeftOfElementBegin(beforeBounds.end, afterBounds.begin) : null;
return {
shadowBeginEnd: {
begin,
end,
rect: shadowRectTopLeft,
beginAdjustment: shadowBeginEnd ? shadowBeginEnd.beginAdjustment : 0
}
};
} else {
return null;
}
} else {
prevAddedIndex = null;
return {
shadowBeginEnd: null
};
}
};
}
function handleFirstInsertShadowAdjustment() {
let lastAddedIndex = null;
return ({ dragResult: { pos, addedIndex, shadowBeginEnd }, draggableInfo: { invalidateShadow } }) => {
if (pos !== null) {
if (addedIndex != null && lastAddedIndex === null) {
if (pos < shadowBeginEnd.begin) {
const beginAdjustment = pos - shadowBeginEnd.begin - 5;
shadowBeginEnd.beginAdjustment = beginAdjustment;
}
lastAddedIndex = addedIndex;
}
} else {
lastAddedIndex = null;
}
};
}
function fireDragEnterLeaveEvents({ options }) {
let wasDragIn = false;
return ({ dragResult: { pos } }) => {
const isDragIn = !!pos;
if (isDragIn !== wasDragIn) {
wasDragIn = isDragIn;
if (isDragIn) {
options.onDragEnter && options.onDragEnter();
} else {
options.onDragLeave && options.onDragLeave();
return {
dragLeft: true
};
}
}
};
}
function fireOnDropReady({ options }) {
let lastAddedIndex = null;
return ({ dragResult: { addedIndex, removedIndex }, draggableInfo: { payload, element } }) => {
if (options.onDropReady && lastAddedIndex !== addedIndex) {
lastAddedIndex = addedIndex;
let adjustedAddedIndex = addedIndex;
if (removedIndex !== null && addedIndex > removedIndex) {
adjustedAddedIndex--;
}
options.onDropReady({ addedIndex: adjustedAddedIndex, removedIndex, payload, element: element.firstElementChild });
}
}
}
function getDragHandler(params) {
if (params.options.behaviour === 'drop-zone') {
// sorting is disabled in container, addedIndex will always be 0 if dropped in
return compose(params)(
getRemovedItem,
setRemovedItemVisibilty,
getPosition,
notifyParentOnPositionCapture,
getElementSize,
handleTargetContainer,
getDragInsertionIndexForDropZone,
getShadowBeginEndForDropZone,
fireDragEnterLeaveEvents,
fireOnDropReady
);
} else {
return compose(params)(
getRemovedItem,
setRemovedItemVisibilty,
getPosition,
notifyParentOnPositionCapture,
getElementSize,
handleTargetContainer,
invalidateShadowBeginEndIfNeeded,
getNextAddedIndex,
resetShadowAdjustment,
handleInsertionSizeChange,
calculateTranslations,
getShadowBeginEnd,
handleFirstInsertShadowAdjustment,
fireDragEnterLeaveEvents,
fireOnDropReady
);
}
}
function getDefaultDragResult() {
return {
addedIndex: null,
removedIndex: null,
elementSize: null,
pos: null,
shadowBeginEnd: null
};
}
function compose(params) {
return (...functions) => {
const hydratedFunctions = functions.map(p => p(params));
let result = null;
return draggableInfo => {
result = hydratedFunctions.reduce((dragResult, fn) => {
return Object.assign(dragResult, fn({ draggableInfo, dragResult }));
}, result || getDefaultDragResult());
return result;
};
};
}
// Container definition begin
function Container(element) {
return function(options) {
let dragResult = null;
let lastDraggableInfo = null;
const props = getContainerProps(element, options);
let dragHandler = getDragHandler(props);
let dropHandler = handleDrop(props);
let parentContainer = null;
let posIsInChildContainer = false;
let childContainers = [];
function processLastDraggableInfo() {
if (lastDraggableInfo !== null) {
lastDraggableInfo.invalidateShadow = true;
dragResult = dragHandler(lastDraggableInfo);
lastDraggableInfo.invalidateShadow = false;
}
}
function onChildPositionCaptured(isCaptured) {
posIsInChildContainer = isCaptured;
if (parentContainer) {
parentContainer.onChildPositionCaptured(isCaptured);
if (lastDraggableInfo) {
dragResult = dragHandler(lastDraggableInfo);
}
}
}
function setDraggables(draggables, element, options) {
const newDraggables = wrapChildren(element, options.orientation, options.animationDuration);
for (let i = 0; i < newDraggables.length; i++) {
draggables[i] = newDraggables[i];
}
for (let i = 0; i < draggables.length - newDraggables.length; i++) {
draggables.pop();
}
}
function prepareDrag(container, relevantContainers) {
const element = container.element;
const draggables = props.draggables;
const options = container.getOptions();
setDraggables(draggables, element, options);
container.layout.invalidateRects();
registerToParentContainer(container, relevantContainers);
draggables.forEach(p => setAnimation(p, true, options.animationDuration));
}
props.layout.setScrollListener(function() {
processLastDraggableInfo();
});
function handleDragLeftDeferedTranslation() {
if (dragResult.dragLeft && props.options.behaviour !== 'drop-zone') {
dragResult.dragLeft = false;
setTimeout(() => {
if (dragResult) calculateTranslations(props)({ dragResult });
}, 20);
}
}
function dispose(container) {
unwrapChildren(container.element);
}
return {
element,
draggables: props.draggables,
isDragRelevant: isDragRelevant(props),
getScale: props.layout.getContainerScale,
layout: props.layout,
getChildContainers: () => childContainers,
onChildPositionCaptured,
dispose,
prepareDrag,
isPosInChildContainer: () => posIsInChildContainer,
handleDrag: function(draggableInfo) {
lastDraggableInfo = draggableInfo;
dragResult = dragHandler(draggableInfo);
handleDragLeftDeferedTranslation();
return dragResult;
},
handleDrop: function(draggableInfo) {
lastDraggableInfo = null;
onChildPositionCaptured(false);
dragHandler = getDragHandler(props);
dropHandler(draggableInfo, dragResult);
dragResult = null;
parentContainer = null;
childContainers = [];
},
getDragResult: function() {
return dragResult;
},
getTranslateCalculator: function(...params) {
return calculateTranslations(props)(...params);
},
setParentContainer: e => {
parentContainer = e;
},
getParentContainer: () => parentContainer,
onTranslated: () => {
processLastDraggableInfo();
},
getOptions: () => props.options,
setDraggables: () => {
setDraggables(props.draggables, element, props.options);
}
};
};
}
const options = {
behaviour: 'move',
groupName: 'bla bla', // if not defined => container will not interfere with other containers
orientation: 'vertical',
dragHandleSelector: null,
nonDragAreaSelector: 'some selector',
dragBeginDelay: 0,
animationDuration: 180,
autoScrollEnabled: true,
lockAxis: true,
dragClass: null,
dropClass: null,
onDragStart: (index, payload) => {},
onDrop: ({ removedIndex, addedIndex, payload, element }) => {},
getChildPayload: index => null,
shouldAnimateDrop: (sourceContainerOptions, payload) => true,
shouldAcceptDrop: (sourceContainerOptions, payload) => true,
onDragEnter: () => {},
onDragLeave: () => { },
onDropReady: ({ removedIndex, addedIndex, payload, element }) => { },
};
// exported part of container
function SmoothDnD(element, options) {
const containerIniter = Container(element);
const container = containerIniter(options);
element[containerInstance] = container;
Mediator.register(container);
return {
dispose: function() {
Mediator.unregister(container);
container.layout.dispose();
container.dispose(container);
}
};
}
export default SmoothDnD;

View File

@@ -1,208 +0,0 @@
import { getScrollingAxis, getVisibleRect } from "./utils";
const maxSpeed = 1500; // px/s
// const minSpeed = 20; // px/s
function addScrollValue(element, axis, value) {
if (element) {
if (element !== window) {
if (axis === "x") {
element.scrollLeft += value;
} else {
element.scrollTop += value;
}
} else {
if (axis === "x") {
element.scrollBy(value, 0);
} else {
element.scrollBy(0, value);
}
}
}
}
const createAnimator = (element, axis = "y") => {
let isAnimating = false;
let request = null;
let startTime = null;
let direction = null;
let speed = null;
function animate(_direction, _speed) {
direction = _direction;
speed = _speed;
isAnimating = true;
if (isAnimating) {
start();
}
}
function start() {
if (request === null) {
request = requestAnimationFrame((timestamp) => {
if (startTime === null) {
startTime = timestamp;
}
const timeDiff = timestamp - startTime;
startTime = timestamp;
let distanceDiff = (timeDiff / 1000) * speed;
distanceDiff = direction === "begin" ? 0 - distanceDiff : distanceDiff;
addScrollValue(element, axis, distanceDiff);
request = null;
start();
});
}
}
function stop() {
if (isAnimating) {
cancelAnimationFrame(request);
isAnimating = false;
startTime = null;
request = null;
}
}
return {
animate,
stop
};
};
function getAutoScrollInfo(position, scrollableInfo) {
const { left, right, top, bottom } = scrollableInfo.rect;
const { x, y } = position;
if (x < left || x > right || y < top || y > bottom) {
return null;
}
let begin;
let end;
let pos;
if (scrollableInfo.axis === "x") {
begin = left;
end = right;
pos = x;
} else {
begin = top;
end = bottom;
pos = y;
}
const moveDistance = 100;
if (end - pos < moveDistance) {
return {
direction: "end",
speedFactor: (moveDistance - (end - pos)) / moveDistance
};
} else if (pos - begin < moveDistance) {
// console.log(pos - begin);
return {
direction: "begin",
speedFactor: (moveDistance - (pos - begin)) / moveDistance
};
}
}
function scrollableInfo(element) {
const result = {
element,
rect: getVisibleRect(element, element.getBoundingClientRect()),
descendants: [],
invalidate,
axis: null,
dispose
};
function dispose() {
element.removeEventListener("scroll", invalidate);
}
function invalidate() {
result.rect = getVisibleRect(element, element.getBoundingClientRect());
result.descendants.forEach((p) => p.invalidate());
}
element.addEventListener("scroll", invalidate);
return result;
}
function handleCurrentElement(current, scrollables, firstDescendentScrollable) {
const scrollingAxis = getScrollingAxis(current);
if (scrollingAxis) {
if (!scrollables.some((p) => p.element === current)) {
const info = scrollableInfo(current);
if (firstDescendentScrollable) {
info.descendants.push(firstDescendentScrollable);
}
firstDescendentScrollable = info;
if (scrollingAxis === "xy") {
scrollables.push(Object.assign({}, info, { axis: "x" }));
scrollables.push(Object.assign({}, info, { axis: "y" }, { descendants: [] }));
} else {
scrollables.push(Object.assign({}, info, { axis: scrollingAxis }));
}
}
}
return { current: current.parentElement, firstDescendentScrollable };
}
function getScrollableElements(containerElements) {
const scrollables = [];
let firstDescendentScrollable = null;
containerElements.forEach((el) => {
let current = el;
firstDescendentScrollable = null;
while (current) {
const result = handleCurrentElement(current, scrollables, firstDescendentScrollable);
current = result.current;
firstDescendentScrollable = result.firstDescendentScrollable;
}
});
return scrollables;
}
function getScrollableAnimator(scrollableInfo) {
return Object.assign(scrollableInfo, createAnimator(scrollableInfo.element, scrollableInfo.axis));
}
function getWindowAnimators() {
function getWindowRect() {
return {
left: 0,
right: global.innerWidth,
top: 0,
bottom: global.innerHeight
};
}
return [
Object.assign({ rect: getWindowRect(), axis: "y" }, createAnimator(global)),
Object.assign({ rect: getWindowRect(), axis: "x" }, createAnimator(global, "x"))
];
}
const dragScroller = (containers) => {
const scrollablesInfo = getScrollableElements(containers.map((p) => p.element));
const animators = [...scrollablesInfo.map(getScrollableAnimator), ...getWindowAnimators()];
return ({ draggableInfo, reset }) => {
if (animators.length) {
if (reset) {
animators.forEach((p) => p.stop());
scrollablesInfo.forEach((p) => p.dispose());
return null;
}
animators.forEach((animator) => {
const scrollParams = getAutoScrollInfo(draggableInfo.mousePosition, animator);
if (scrollParams) {
animator.animate(scrollParams.direction, scrollParams.speedFactor * maxSpeed);
} else {
animator.stop();
}
});
}
};
};
export default dragScroller;

View File

@@ -1,49 +0,0 @@
import { addChildAt, removeChildAt } from './utils';
import {
wrapperClass,
animationClass,
containersInDraggable
} from './constants';
export function domDropHandler({ element, draggables, layout, options }) {
return (dropResult, onDrop) => {
const { removedIndex, addedIndex, droppedElement } = dropResult;
let removedWrapper = null;
if (removedIndex !== null) {
removedWrapper = removeChildAt(element, removedIndex);
draggables.splice(removedIndex, 1);
}
if (addedIndex !== null) {
const wrapper = global.document.createElement('div');
wrapper.className = `${wrapperClass}`;
wrapper.appendChild(removedWrapper && removedWrapper.firstElementChild ? removedWrapper.firstElementChild : droppedElement);
wrapper[containersInDraggable] = [];
addChildAt(element, wrapper, addedIndex);
if (addedIndex >= draggables.length) {
draggables.push(wrapper);
} else {
draggables.splice(addedIndex, 0, wrapper);
}
}
if (onDrop) {
onDrop(dropResult);
}
};
}
export function reactDropHandler() {
const handler = ({ element, draggables, layout, options }) => {
return (dropResult, onDrop) => {
if (onDrop) {
onDrop(dropResult);
}
};
};
return {
handler
};
}

View File

@@ -1,288 +0,0 @@
import * as Utils from './utils';
import { translationValue, visibilityValue, extraSizeForInsertion, containersInDraggable } from './constants';
const horizontalMap = {
size: 'offsetWidth',
distanceToParent: 'offsetLeft',
translate: 'transform',
begin: 'left',
end: 'right',
dragPosition: 'x',
scrollSize: 'scrollWidth',
offsetSize: 'offsetWidth',
scrollValue: 'scrollLeft',
scale: 'scaleX',
setSize: 'width',
setters: {
'translate': (val) => `translate3d(${val}px, 0, 0)`
}
};
const verticalMap = {
size: 'offsetHeight',
distanceToParent: 'offsetTop',
translate: 'transform',
begin: 'top',
end: 'bottom',
dragPosition: 'y',
scrollSize: 'scrollHeight',
offsetSize: 'offsetHeight',
scrollValue: 'scrollTop',
scale: 'scaleY',
setSize: 'height',
setters: {
'translate': (val) => `translate3d(0,${val}px, 0)`
}
};
function orientationDependentProps(map) {
function get(obj, prop) {
const mappedProp = map[prop];
return obj[mappedProp || prop];
}
function set(obj, prop, value) {
requestAnimationFrame(() => {
obj[map[prop]] = map.setters[prop] ? map.setters[prop](value) : value;
});
}
return { get, set };
}
export default function layoutManager(containerElement, orientation, _animationDuration) {
containerElement[extraSizeForInsertion] = 0;
const animationDuration = _animationDuration;
const map = orientation === 'horizontal' ? horizontalMap : verticalMap;
const propMapper = orientationDependentProps(map);
const values = {
translation: 0
};
let registeredScrollListener = null;
global.addEventListener('resize', function() {
invalidateContainerRectangles(containerElement);
// invalidateContainerScale(containerElement);
});
setTimeout(() => {
invalidate();
}, 10);
// invalidate();
const scrollListener = Utils.listenScrollParent(containerElement, function() {
invalidateContainerRectangles(containerElement);
registeredScrollListener && registeredScrollListener();
});
function invalidate() {
invalidateContainerRectangles(containerElement);
invalidateContainerScale(containerElement);
}
let visibleRect;
function invalidateContainerRectangles(containerElement) {
values.rect = Utils.getContainerRect(containerElement);
values.visibleRect = Utils.getVisibleRect(containerElement, values.rect);
}
function invalidateContainerScale(containerElement) {
const rect = containerElement.getBoundingClientRect();
values.scaleX = containerElement.offsetWidth ? ((rect.right - rect.left) / containerElement.offsetWidth) : 1;
values.scaleY = containerElement.offsetHeight ? ((rect.bottom - rect.top) / containerElement.offsetHeight) : 1;
}
function getContainerRectangles() {
return {
rect: values.rect,
visibleRect: values.visibleRect
};
}
function getBeginEndOfDOMRect(rect) {
return {
begin: propMapper.get(rect, 'begin'),
end: propMapper.get(rect, 'end')
};
}
function getBeginEndOfContainer() {
const begin = propMapper.get(values.rect, 'begin') + values.translation;
const end = propMapper.get(values.rect, 'end') + values.translation;
return { begin, end };
}
function getBeginEndOfContainerVisibleRect() {
const begin = propMapper.get(values.visibleRect, 'begin') + values.translation;
const end = propMapper.get(values.visibleRect, 'end') + values.translation;
return { begin, end };
}
function getContainerScale() {
return { scaleX: values.scaleX, scaleY: values.scaleY };
}
function getSize(element) {
return propMapper.get(element, 'size') * propMapper.get(values, 'scale');
}
function getDistanceToOffsetParent(element) {
const distance = propMapper.get(element, 'distanceToParent') + (element[translationValue] || 0);
return distance * propMapper.get(values, 'scale');
}
function getBeginEnd(element) {
const begin = getDistanceToOffsetParent(element) + (propMapper.get(values.rect, 'begin') + values.translation) - propMapper.get(containerElement, 'scrollValue');
return {
begin,
end: begin + getSize(element) * propMapper.get(values, 'scale')
};
}
function setSize(element, size) {
propMapper.set(element, 'setSize', size);
}
function getAxisValue(position) {
return propMapper.get(position, 'dragPosition');
}
function updateDescendantContainerRects(container) {
container.layout.invalidateRects();
container.onTranslated();
if (container.getChildContainers()) {
container.getChildContainers().forEach(p => updateDescendantContainerRects(p));
}
}
function setTranslation(element, translation) {
if (!translation) {
element.style.removeProperty('transform');
} else {
propMapper.set(element.style, 'translate', translation);
}
element[translationValue] = translation;
if (element[containersInDraggable]) {
setTimeout(() => {
element[containersInDraggable].forEach(p => {
updateDescendantContainerRects(p);
});
}, animationDuration + 20);
}
}
function getTranslation(element) {
return element[translationValue];
}
function setVisibility(element, isVisible) {
if (element[visibilityValue] === undefined || element[visibilityValue] !== isVisible) {
if (isVisible) {
element.style.removeProperty('visibility');
} else {
element.style.visibility = 'hidden';
}
element[visibilityValue] = isVisible;
}
}
function isVisible(element) {
return element[visibilityValue] === undefined || element[visibilityValue];
}
function isInVisibleRect(x, y) {
let { left, top, right, bottom } = values.visibleRect;
// if there is no wrapper in rect size will be 0 and wont accept any drop
// so make sure at least there is 30px difference
if (bottom - top < 2) {
bottom = top + 30;
}
const containerRect = values.rect;
if (orientation === 'vertical') {
return x > containerRect.left && x < containerRect.right && y > top && y < bottom;
} else {
return x > left && x < right && y > containerRect.top && y < containerRect.bottom;
}
}
function setScrollListener(callback) {
registeredScrollListener = callback;
}
function getTopLeftOfElementBegin(begin) {
let top = 0;
let left = 0;
if (orientation === 'horizontal') {
left = begin;
top = values.rect.top;
} else {
left = values.rect.left;
top = begin;
}
return {
top, left
};
}
function getScrollSize(element) {
return propMapper.get(element, 'scrollSize');
}
function getScrollValue(element) {
return propMapper.get(element, 'scrollValue');
}
function setScrollValue(element, val) {
return propMapper.set(element, 'scrollValue', val);
}
function dispose() {
if (scrollListener) {
scrollListener.dispose();
}
if (visibleRect) {
visibleRect.parentNode.removeChild(visibleRect);
visibleRect = null;
}
}
function getPosition(position) {
return isInVisibleRect(position.x, position.y) ? getAxisValue(position) : null;
}
function invalidateRects() {
invalidateContainerRectangles(containerElement);
}
return {
getSize,
//getDistanceToContainerBegining,
getContainerRectangles,
getBeginEndOfDOMRect,
getBeginEndOfContainer,
getBeginEndOfContainerVisibleRect,
getBeginEnd,
getAxisValue,
setTranslation,
getTranslation,
setVisibility,
isVisible,
isInVisibleRect,
dispose,
getContainerScale,
setScrollListener,
setSize,
getTopLeftOfElementBegin,
getScrollSize,
getScrollValue,
setScrollValue,
invalidate,
invalidateRects,
getPosition,
};
}

View File

@@ -1,479 +0,0 @@
import './polyfills';
import * as Utils from './utils';
import * as constants from './constants';
import { addStyleToHead, addCursorStyleToBody, removeStyle } from './styles';
import dragScroller from './dragscroller';
const grabEvents = ['mousedown', 'touchstart'];
const moveEvents = ['mousemove', 'touchmove'];
const releaseEvents = ['mouseup', 'touchend'];
let dragListeningContainers = null;
let grabbedElement = null;
let ghostInfo = null;
let draggableInfo = null;
let containers = [];
let isDragging = false;
let removedElement = null;
let handleDrag = null;
let handleScroll = null;
let sourceContainer = null;
let sourceContainerLockAxis = null;
let cursorStyleElement = null;
// Utils.addClass(document.body, 'clearfix');
const isMobile = Utils.isMobile();
function listenEvents() {
if (typeof window !== 'undefined') {
addGrabListeners();
}
}
function addGrabListeners() {
grabEvents.forEach(e => {
global.document.addEventListener(e, onMouseDown, { passive: false });
});
}
function addMoveListeners() {
moveEvents.forEach(e => {
global.document.addEventListener(e, onMouseMove, { passive: false });
});
}
function removeMoveListeners() {
moveEvents.forEach(e => {
global.document.removeEventListener(e, onMouseMove, { passive: false });
});
}
function addReleaseListeners() {
releaseEvents.forEach(e => {
global.document.addEventListener(e, onMouseUp, { passive: false });
});
}
function removeReleaseListeners() {
releaseEvents.forEach(e => {
global.document.removeEventListener(e, onMouseUp, { passive: false });
});
}
function getGhostParent() {
if (draggableInfo.ghostParent) {
return draggableInfo.ghostParent;
}
if (grabbedElement) {
return grabbedElement.parentElement || global.document.body;
} else {
return global.document.body;
}
}
function getGhostElement(wrapperElement, { x, y }, container, cursor) {
const { scaleX = 1, scaleY = 1 } = container.getScale();
const { left, top, right, bottom } = wrapperElement.getBoundingClientRect();
const midX = left + (right - left) / 2;
const midY = top + (bottom - top) / 2;
const ghost = wrapperElement.cloneNode(true);
ghost.style.zIndex = 1000;
ghost.style.boxSizing = 'border-box';
ghost.style.position = 'fixed';
ghost.style.left = left + 'px';
ghost.style.top = top + 'px';
ghost.style.width = right - left + 'px';
ghost.style.height = bottom - top + 'px';
ghost.style.overflow = 'visible';
ghost.style.transition = null;
ghost.style.removeProperty('transition');
ghost.style.pointerEvents = 'none';
if (container.getOptions().dragClass) {
setTimeout(() => {
Utils.addClass(ghost.firstElementChild, container.getOptions().dragClass);
const dragCursor = global.getComputedStyle(ghost.firstElementChild).cursor;
cursorStyleElement = addCursorStyleToBody(dragCursor);
});
} else {
cursorStyleElement = addCursorStyleToBody(cursor);
}
Utils.addClass(ghost, container.getOptions().orientation);
Utils.addClass(ghost, constants.ghostClass);
return {
ghost: ghost,
centerDelta: { x: midX - x, y: midY - y },
positionDelta: { left: left - x, top: top - y }
};
}
function getDraggableInfo(draggableElement) {
const container = containers.filter(p => draggableElement.parentElement === p.element)[0];
const draggableIndex = container.draggables.indexOf(draggableElement);
const getGhostParent = container.getOptions().getGhostParent;
return {
container,
element: draggableElement,
elementIndex: draggableIndex,
payload: container.getOptions().getChildPayload
? container.getOptions().getChildPayload(draggableIndex)
: undefined,
targetElement: null,
position: { x: 0, y: 0 },
groupName: container.getOptions().groupName,
ghostParent: getGhostParent ? getGhostParent() : null,
};
}
function handleDropAnimation(callback) {
function endDrop() {
Utils.removeClass(ghostInfo.ghost, 'animated');
ghostInfo.ghost.style.transitionDuration = null;
getGhostParent().removeChild(ghostInfo.ghost);
callback();
}
function animateGhostToPosition({ top, left }, duration, dropClass) {
Utils.addClass(ghostInfo.ghost, 'animated');
if (dropClass) {
Utils.addClass(ghostInfo.ghost.firstElementChild, dropClass);
}
ghostInfo.ghost.style.transitionDuration = duration + 'ms';
ghostInfo.ghost.style.left = left + 'px';
ghostInfo.ghost.style.top = top + 'px';
setTimeout(function() {
endDrop();
}, duration + 20);
}
function shouldAnimateDrop(options) {
return options.shouldAnimateDrop
? options.shouldAnimateDrop(draggableInfo.container.getOptions(), draggableInfo.payload)
: true;
}
if (draggableInfo.targetElement) {
const container = containers.filter(p => p.element === draggableInfo.targetElement)[0];
if (shouldAnimateDrop(container.getOptions())) {
const dragResult = container.getDragResult();
animateGhostToPosition(
dragResult.shadowBeginEnd.rect,
Math.max(150, container.getOptions().animationDuration / 2),
container.getOptions().dropClass
);
} else {
endDrop();
}
} else {
const container = containers.filter(p => p === draggableInfo.container)[0];
const { behaviour, removeOnDropOut } = container.getOptions();
if (behaviour === 'move' && !removeOnDropOut && container.getDragResult()) {
const { removedIndex, elementSize } = container.getDragResult();
const layout = container.layout;
// drag ghost to back
container.getTranslateCalculator({
dragResult: {
removedIndex,
addedIndex: removedIndex,
elementSize
}
});
const prevDraggableEnd =
removedIndex > 0
? layout.getBeginEnd(container.draggables[removedIndex - 1]).end
: layout.getBeginEndOfContainer().begin;
animateGhostToPosition(
layout.getTopLeftOfElementBegin(prevDraggableEnd),
container.getOptions().animationDuration,
container.getOptions().dropClass
);
} else {
Utils.addClass(ghostInfo.ghost, 'animated');
ghostInfo.ghost.style.transitionDuration = container.getOptions().animationDuration + 'ms';
ghostInfo.ghost.style.opacity = '0';
ghostInfo.ghost.style.transform = 'scale(0.90)';
setTimeout(function() {
endDrop();
}, container.getOptions().animationDuration);
}
}
}
const handleDragStartConditions = (function handleDragStartConditions() {
let startEvent;
let delay;
let clb;
let timer = null;
const moveThreshold = 1;
const maxMoveInDelay = 5;
function onMove(event) {
const { clientX: currentX, clientY: currentY } = getPointerEvent(event);
if (!delay) {
if (
Math.abs(startEvent.clientX - currentX) > moveThreshold ||
Math.abs(startEvent.clientY - currentY) > moveThreshold
) {
return callCallback();
}
} else {
if (
Math.abs(startEvent.clientX - currentX) > maxMoveInDelay ||
Math.abs(startEvent.clientY - currentY) > maxMoveInDelay
) {
deregisterEvent();
}
}
}
function onUp() {
deregisterEvent();
}
function onHTMLDrag() {
deregisterEvent();
}
function registerEvents() {
if (delay) {
timer = setTimeout(callCallback, delay);
}
moveEvents.forEach(e => global.document.addEventListener(e, onMove), {
passive: false
});
releaseEvents.forEach(e => global.document.addEventListener(e, onUp), {
passive: false
});
global.document.addEventListener('drag', onHTMLDrag, {
passive: false
});
}
function deregisterEvent() {
clearTimeout(timer);
moveEvents.forEach(e => global.document.removeEventListener(e, onMove), {
passive: false
});
releaseEvents.forEach(e => global.document.removeEventListener(e, onUp), {
passive: false
});
global.document.removeEventListener('drag', onHTMLDrag, {
passive: false
});
}
function callCallback() {
clearTimeout(timer);
deregisterEvent();
clb();
}
return function(_startEvent, _delay, _clb) {
startEvent = getPointerEvent(_startEvent);
delay = (typeof _delay === 'number') ? _delay : (isMobile ? 200 : 0);
clb = _clb;
registerEvents();
};
})();
function onMouseDown(event) {
const e = getPointerEvent(event);
if (!isDragging && (e.button === undefined || e.button === 0)) {
grabbedElement = Utils.getParent(e.target, '.' + constants.wrapperClass);
if (grabbedElement) {
const containerElement = Utils.getParent(grabbedElement, '.' + constants.containerClass);
const container = containers.filter(p => p.element === containerElement)[0];
const dragHandleSelector = container.getOptions().dragHandleSelector;
const nonDragAreaSelector = container.getOptions().nonDragAreaSelector;
let startDrag = true;
if (dragHandleSelector && !Utils.getParent(e.target, dragHandleSelector)) {
startDrag = false;
}
if (nonDragAreaSelector && Utils.getParent(e.target, nonDragAreaSelector)) {
startDrag = false;
}
if (startDrag) {
handleDragStartConditions(e, container.getOptions().dragBeginDelay, () => {
Utils.clearSelection();
initiateDrag(e, Utils.getElementCursor(event.target));
addMoveListeners();
addReleaseListeners();
});
}
}
}
}
function onMouseUp() {
removeMoveListeners();
removeReleaseListeners();
handleScroll({ reset: true });
if (cursorStyleElement) {
removeStyle(cursorStyleElement);
cursorStyleElement = null;
}
if (draggableInfo) {
handleDropAnimation(() => {
Utils.removeClass(global.document.body, constants.disbaleTouchActions);
Utils.removeClass(global.document.body, constants.noUserSelectClass);
fireOnDragStartEnd(false);
(dragListeningContainers || []).forEach(p => {
p.handleDrop(draggableInfo);
});
dragListeningContainers = null;
grabbedElement = null;
ghostInfo = null;
draggableInfo = null;
isDragging = false;
sourceContainer = null;
sourceContainerLockAxis = null;
handleDrag = null;
});
}
}
function getPointerEvent(e) {
return e.touches ? e.touches[0] : e;
}
function dragHandler(dragListeningContainers) {
let targetContainers = dragListeningContainers;
return function(draggableInfo) {
let containerBoxChanged = false;
targetContainers.forEach(p => {
const dragResult = p.handleDrag(draggableInfo);
containerBoxChanged |= dragResult.containerBoxChanged || false;
dragResult.containerBoxChanged = false;
});
handleScroll({ draggableInfo });
if (containerBoxChanged) {
containerBoxChanged = false;
setTimeout(() => {
containers.forEach(p => {
p.layout.invalidateRects();
p.onTranslated();
});
}, 10);
}
};
}
function getScrollHandler(container, dragListeningContainers) {
if (container.getOptions().autoScrollEnabled) {
return dragScroller(dragListeningContainers);
} else {
return () => null;
}
}
function fireOnDragStartEnd(isStart) {
containers.forEach(p => {
const fn = isStart ? p.getOptions().onDragStart : p.getOptions().onDragEnd;
if (fn) {
const options = {
isSource: p === draggableInfo.container,
payload: draggableInfo.payload
};
if (p.isDragRelevant(draggableInfo.container, draggableInfo.payload)) {
options.willAcceptDrop = true;
} else {
options.willAcceptDrop = false;
}
fn(options);
}
});
}
function initiateDrag(position, cursor) {
isDragging = true;
const container = containers.filter(p => grabbedElement.parentElement === p.element)[0];
container.setDraggables();
sourceContainer = container;
sourceContainerLockAxis = container.getOptions().lockAxis ? container.getOptions().lockAxis.toLowerCase() : null;
draggableInfo = getDraggableInfo(grabbedElement);
ghostInfo = getGhostElement(
grabbedElement,
{ x: position.clientX, y: position.clientY },
draggableInfo.container,
cursor
);
draggableInfo.position = {
x: position.clientX + ghostInfo.centerDelta.x,
y: position.clientY + ghostInfo.centerDelta.y
};
draggableInfo.mousePosition = {
x: position.clientX,
y: position.clientY
};
Utils.addClass(global.document.body, constants.disbaleTouchActions);
Utils.addClass(global.document.body, constants.noUserSelectClass);
dragListeningContainers = containers.filter(p => p.isDragRelevant(container, draggableInfo.payload));
handleDrag = dragHandler(dragListeningContainers);
if (handleScroll) {
handleScroll({ reset: true });
}
handleScroll = getScrollHandler(container, dragListeningContainers);
dragListeningContainers.forEach(p => p.prepareDrag(p, dragListeningContainers));
fireOnDragStartEnd(true);
handleDrag(draggableInfo);
getGhostParent().appendChild(ghostInfo.ghost);
}
function onMouseMove(event) {
event.preventDefault();
const e = getPointerEvent(event);
if (!draggableInfo) {
initiateDrag(e, Utils.getElementCursor(event.target));
} else {
// just update ghost position && draggableInfo position
if (sourceContainerLockAxis) {
if (sourceContainerLockAxis === 'y') {
ghostInfo.ghost.style.top = `${e.clientY + ghostInfo.positionDelta.top}px`;
draggableInfo.position.y = e.clientY + ghostInfo.centerDelta.y;
draggableInfo.mousePosition.y = e.clientY;
} else if (sourceContainerLockAxis === 'x') {
ghostInfo.ghost.style.left = `${e.clientX + ghostInfo.positionDelta.left}px`;
draggableInfo.position.x = e.clientX + ghostInfo.centerDelta.x;
draggableInfo.mousePosition.x = e.clientX;
}
} else {
ghostInfo.ghost.style.left = `${e.clientX + ghostInfo.positionDelta.left}px`;
ghostInfo.ghost.style.top = `${e.clientY + ghostInfo.positionDelta.top}px`;
draggableInfo.position.x = e.clientX + ghostInfo.centerDelta.x;
draggableInfo.position.y = e.clientY + ghostInfo.centerDelta.y;
draggableInfo.mousePosition.x = e.clientX;
draggableInfo.mousePosition.y = e.clientY;
}
handleDrag(draggableInfo);
}
}
function Mediator() {
listenEvents();
return {
register: function(container) {
containers.push(container);
},
unregister: function(container) {
containers.splice(containers.indexOf(container), 1);
}
};
}
addStyleToHead();
export default Mediator();

View File

@@ -1,17 +0,0 @@
(function(constructor) {
if (constructor && constructor.prototype && !constructor.prototype.matches) {
constructor.prototype.matches =
constructor.prototype.matchesSelector ||
constructor.prototype.mozMatchesSelector ||
constructor.prototype.msMatchesSelector ||
constructor.prototype.oMatchesSelector ||
constructor.prototype.webkitMatchesSelector ||
function(s) {
var matches = (this.document || this.ownerDocument).querySelectorAll(s),
i = matches.length;
while (--i >= 0 && matches.item(i) !== this) {}
return i > -1;
};
}
})(global.Node || global.Element);

View File

@@ -1,118 +0,0 @@
import * as constants from "./constants";
const verticalWrapperClass = {
overflow: "hidden",
display: "block"
};
const horizontalWrapperClass = {
height: "100%",
display: "inline-block",
"vertical-align": "top",
"white-space": "normal"
};
const stretcherElementHorizontalClass = {
display: "inline-block"
};
const css = {
[`.${constants.containerClass}`]: {
position: "relative"
},
[`.${constants.containerClass} *`]: {
"box-sizing": "border-box"
},
[`.${constants.containerClass}.horizontal`]: {
"white-space": "nowrap"
},
[`.${constants.containerClass}.horizontal > .${constants.stretcherElementClass}`]: stretcherElementHorizontalClass,
[`.${constants.containerClass}.horizontal > .${constants.wrapperClass}`]: horizontalWrapperClass,
[`.${constants.containerClass}.vertical > .${constants.wrapperClass}`]: verticalWrapperClass,
[`.${constants.wrapperClass}`]: {
// 'overflow': 'hidden'
},
[`.${constants.wrapperClass}.horizontal`]: horizontalWrapperClass,
[`.${constants.wrapperClass}.vertical`]: verticalWrapperClass,
[`.${constants.wrapperClass}.animated`]: {
transition: "transform ease"
},
[`.${constants.ghostClass} *`]: {
//'perspective': '800px',
"box-sizing": "border-box"
},
[`.${constants.ghostClass}.animated`]: {
transition: "all ease-in-out"
},
[`.${constants.disbaleTouchActions} *`]: {
"touch-actions": "none",
"-ms-touch-actions": "none"
},
[`.${constants.noUserSelectClass} *`]: {
"-webkit-touch-callout": "none",
"-webkit-user-select": "none",
"-khtml-user-select": "none",
"-moz-user-select": "none",
"-ms-user-select": "none",
"user-select": "none"
}
};
function convertToCssString(css) {
return Object.keys(css).reduce((styleString, propName) => {
const propValue = css[propName];
if (typeof propValue === "object") {
return `${styleString}${propName}{${convertToCssString(propValue)}}`;
}
return `${styleString}${propName}:${propValue};`;
}, "");
}
function addStyleToHead() {
if (typeof window !== "undefined") {
const head = global.document.head || global.document.getElementsByTagName("head")[0];
const style = global.document.createElement("style");
const cssString = convertToCssString(css);
style.type = "text/css";
if (style.styleSheet) {
style.styleSheet.cssText = cssString;
} else {
style.appendChild(global.document.createTextNode(cssString));
}
head.appendChild(style);
}
}
function addCursorStyleToBody(cursor) {
if (cursor && typeof window !== "undefined") {
const head = global.document.head || global.document.getElementsByTagName("head")[0];
const style = global.document.createElement("style");
const cssString = convertToCssString({
"body *": {
cursor: `${cursor} !important`
}
});
style.type = "text/css";
if (style.styleSheet) {
style.styleSheet.cssText = cssString;
} else {
style.appendChild(global.document.createTextNode(cssString));
}
head.appendChild(style);
return style;
}
return null;
}
function removeStyle(styleElement) {
if (styleElement && typeof window !== "undefined") {
const head = global.document.head || global.document.getElementsByTagName("head")[0];
head.removeChild(styleElement);
}
}
export { addStyleToHead, addCursorStyleToBody, removeStyle };

View File

@@ -1,282 +0,0 @@
export const getIntersection = (rect1, rect2) => {
return {
left: Math.max(rect1.left, rect2.left),
top: Math.max(rect1.top, rect2.top),
right: Math.min(rect1.right, rect2.right),
bottom: Math.min(rect1.bottom, rect2.bottom)
};
};
export const getIntersectionOnAxis = (rect1, rect2, axis) => {
if (axis === "x") {
return {
left: Math.max(rect1.left, rect2.left),
top: rect1.top,
right: Math.min(rect1.right, rect2.right),
bottom: rect1.bottom
};
} else {
return {
left: rect1.left,
top: Math.max(rect1.top, rect2.top),
right: rect1.right,
bottom: Math.min(rect1.bottom, rect2.bottom)
};
}
};
export const getContainerRect = element => {
const _rect = element.getBoundingClientRect();
const rect = {
left: _rect.left,
right: _rect.right + 10,
top: _rect.top,
bottom: _rect.bottom
};
if (hasBiggerChild(element, "x") && !isScrollingOrHidden(element, "x")) {
const width = rect.right - rect.left;
rect.right = rect.right + element.scrollWidth - width;
}
if (hasBiggerChild(element, "y") && !isScrollingOrHidden(element, "y")) {
const height = rect.bottom - rect.top;
rect.bottom = rect.bottom + element.scrollHeight - height;
}
return rect;
};
export const getScrollingAxis = element => {
const style = global.getComputedStyle(element);
const overflow = style["overflow"];
const general = overflow === "auto" || overflow === "scroll";
if (general) return "xy";
const overFlowX = style[`overflow-x`];
const xScroll = overFlowX === "auto" || overFlowX === "scroll";
const overFlowY = style[`overflow-y`];
const yScroll = overFlowY === "auto" || overFlowY === "scroll";
return `${xScroll ? "x" : ""}${yScroll ? "y" : ""}` || null;
};
export const isScrolling = (element, axis) => {
const style = global.getComputedStyle(element);
const overflow = style["overflow"];
const overFlowAxis = style[`overflow-${axis}`];
const general = overflow === "auto" || overflow === "scroll";
const dimensionScroll = overFlowAxis === "auto" || overFlowAxis === "scroll";
return general || dimensionScroll;
};
export const isScrollingOrHidden = (element, axis) => {
const style = global.getComputedStyle(element);
const overflow = style["overflow"];
const overFlowAxis = style[`overflow-${axis}`];
const general =
overflow === "auto" || overflow === "scroll" || overflow === "hidden";
const dimensionScroll =
overFlowAxis === "auto" ||
overFlowAxis === "scroll" ||
overFlowAxis === "hidden";
return general || dimensionScroll;
};
export const hasBiggerChild = (element, axis) => {
if (axis === "x") {
return element.scrollWidth > element.clientWidth;
} else {
return element.scrollHeight > element.clientHeight;
}
};
export const hasScrollBar = (element, axis) => {
return hasBiggerChild(element, axis) && isScrolling(element, axis);
};
export const getVisibleRect = (element, elementRect) => {
let currentElement = element;
let rect = elementRect || getContainerRect(element);
currentElement = element.parentElement;
while (currentElement) {
if (
hasBiggerChild(currentElement, "x") &&
isScrollingOrHidden(currentElement, "x")
) {
rect = getIntersectionOnAxis(
rect,
currentElement.getBoundingClientRect(),
"x"
);
}
if (
hasBiggerChild(currentElement, "y") &&
isScrollingOrHidden(currentElement, "y")
) {
rect = getIntersectionOnAxis(
rect,
currentElement.getBoundingClientRect(),
"y"
);
}
currentElement = currentElement.parentElement;
}
return rect;
};
export const listenScrollParent = (element, clb) => {
let scrollers = [];
const dispose = () => {
scrollers.forEach(p => {
p.removeEventListener("scroll", clb);
});
global.removeEventListener("scroll", clb);
};
setTimeout(function() {
let currentElement = element;
while (currentElement) {
if (
isScrolling(currentElement, "x") ||
isScrolling(currentElement, "y")
) {
currentElement.addEventListener("scroll", clb);
scrollers.push(currentElement);
}
currentElement = currentElement.parentElement;
}
global.addEventListener("scroll", clb);
}, 10);
return {
dispose
};
};
export const hasParent = (element, parent) => {
let current = element;
while (current) {
if (current === parent) {
return true;
}
current = current.parentElement;
}
return false;
};
export const getParent = (element, selector) => {
let current = element;
while (current) {
if (current.matches(selector)) {
return current;
}
current = current.parentElement;
}
return null;
};
export const hasClass = (element, cls) => {
return (
element.className
.split(" ")
.map(p => p)
.indexOf(cls) > -1
);
};
export const addClass = (element, cls) => {
if (element) {
element.className = element.className || ''
const classes = element.className.split(" ").filter(p => p);
if (classes.indexOf(cls) === -1) {
classes.unshift(cls);
element.className = classes.join(" ");
}
}
};
export const removeClass = (element, cls) => {
if (element) {
const classes = element.className.split(" ").filter(p => p && p !== cls);
element.className = classes.join(" ");
}
};
export const debounce = (fn, delay, immediate) => {
let timer = null;
return (...params) => {
if (timer) {
clearTimeout(timer);
}
if (immediate && !timer) {
fn.call(this, ...params);
} else {
timer = setTimeout(() => {
timer = null;
fn.call(this, ...params);
}, delay);
}
};
};
export const removeChildAt = (parent, index) => {
return parent.removeChild(parent.children[index]);
};
export const addChildAt = (parent, child, index) => {
if (index >= parent.children.lenght) {
parent.appendChild(child);
} else {
parent.insertBefore(child, parent.children[index]);
}
};
export const isMobile = () => {
if (typeof window !== 'undefined') {
if (
global.navigator.userAgent.match(/Android/i) ||
global.navigator.userAgent.match(/webOS/i) ||
global.navigator.userAgent.match(/iPhone/i) ||
global.navigator.userAgent.match(/iPad/i) ||
global.navigator.userAgent.match(/iPod/i) ||
global.navigator.userAgent.match(/BlackBerry/i) ||
global.navigator.userAgent.match(/Windows Phone/i)
) {
return true;
} else {
return false;
}
}
return false;
};
export const clearSelection = () => {
if (global.getSelection) {
if (global.getSelection().empty) {
// Chrome
global.getSelection().empty();
} else if (global.getSelection().removeAllRanges) {
// Firefox
global.getSelection().removeAllRanges();
}
} else if (global.document.selection) {
// IE?
global.document.selection.empty();
}
};
export const getElementCursor = (element) => {
if (element) {
const style = global.getComputedStyle(element);
if (style) {
return style.cursor;
}
}
return null;
}

View File

@@ -1,361 +0,0 @@
import { PopoverContainer, PopoverContent } from "react-popopo";
import styled, { createGlobalStyle, css } from "styled-components";
const getBoardWrapperStyles = (props) => {
if (props.orientation === "vertical") {
return ` `;
}
if (props.orientation === "horizontal") {
return `
display: flex;
flex-direction: row;
align-items: flex-start;
`;
}
return "";
};
const getSectionStyles = (props) => {
if (props.orientation === "horizontal") {
return `
display: inline-flex;
`;
}
return `
margin-bottom: 10px;
`;
};
export const GlobalStyle = createGlobalStyle`
.comPlainTextContentEditable {
-webkit-user-modify: read-write-plaintext-only;
cursor: text;
}
.smooth-dnd-container.horizontal {
}
.comPlainTextContentEditable--has-placeholder::before {
content: attr(placeholder);
opacity: 0.5;
color: inherit;
cursor: text;
}
.react_trello_dragClass {
transform: rotate(3deg);
}
.react_trello_dragLaneClass {
transform: rotate(3deg);
}
.icon-overflow-menu-horizontal:before {
content: "\\E91F";
}
.icon-lg,
.icon-sm {
color: #798d99;
}
.icon-lg {
height: 32px;
font-size: 16px;
line-height: 32px;
width: 32px;
}
.react-trello-column-header {
border-radius: 5px;
}
`;
export const StyleHorizontal = styled.div``;
export const StyleVertical = styled.div`
.react-trello-column-header {
text-align: left;
}
.smooth-dnd-container {
// TODO ? This is the question. We need the same drag-zone we get in horizontal mode
min-height: 50px; // Not needed, just for extra landing space
}
.smooth-dnd-container.horizontal {
// TODO: This is what is currently providing us multi row cols, and may need to be adjusted with new DND Library
display: flex; /* Allows wrapping */
flex-wrap: wrap; /* Allows wrapping */
//background-color: yellow !important;
}
.smooth-dnd-ghost {
//background-color: red !important;
}
.react-trello-card {
//background-color: orange !important;
margin: 5px;
// TODO: This is what is currently providing us multi row cols, and may need to be adjusted with new DND Library
flex: 0 1 auto;
}
.smooth-dnd-stretcher-element {
//background-color: purple !important;
}
.smooth-dnd-draggable-wrapper {
//background-color: blue !important;
flex: 0 1 auto; /* Allows items to grow and shrink */
}
.react-trello-board {
overflow-y: hidden !important;
}
`;
export const CustomPopoverContainer = styled(PopoverContainer)`
position: absolute;
right: 10px;
flex-flow: column nowrap;
`;
export const CustomPopoverContent = styled(PopoverContent)`
visibility: hidden;
margin-top: -5px;
opacity: 0;
position: absolute;
z-index: 10;
box-shadow: 2px 2px 8px rgba(0, 0, 0, 0.3);
transition: all 0.3s ease 0ms;
border-radius: 3px;
min-width: 7em;
flex-flow: column nowrap;
background-color: #fff;
color: #000;
padding: 5px;
left: 50%;
transform: translateX(-50%);
${(props) =>
props.active &&
`
visibility: visible;
opacity: 1;
transition-delay: 100ms;
`} &::before {
visibility: hidden;
}
a {
color: rgba(255, 255, 255, 0.56);
padding: 0.5em 1em;
margin: 0;
text-decoration: none;
&:hover {
background-color: #00bcd4 !important;
color: #37474f;
}
}
`;
export const BoardWrapper = styled.div`
background-color: #ffffff;
overflow-y: scroll;
padding: 5px;
color: #393939;
${getBoardWrapperStyles};
`;
export const Header = styled.header`
margin-bottom: 10px;
display: flex;
flex-direction: row;
align-items: flex-start;
`;
export const Section = styled.section`
background-color: #e3e3e3;
border-radius: 3px;
margin: 2px 2px;
position: relative;
padding: 5px;
flex-direction: column;
${getSectionStyles};
`;
export const LaneHeader = styled(Header)`
margin-bottom: 0;
${(props) =>
props.editLaneTitle &&
css`
padding: 0;
line-height: 30px;
`} ${(props) =>
!props.editLaneTitle &&
css`
padding: 0 5px;
`};
`;
export const LaneFooter = styled.div`
display: flex;
justify-content: center;
align-items: center;
width: 100%;
position: relative;
height: 10px;
`;
export const ScrollableLane = styled.div`
flex: 1;
overflow-y: auto;
min-width: 250px;
overflow-x: hidden;
align-self: center;
flex-direction: column;
justify-content: space-between;
`;
export const Title = styled.span`
font-weight: bold;
font-size: 15px;
line-height: 18px;
cursor: ${(props) => (props.draggable ? "grab" : `auto`)};
width: 70%;
`;
export const RightContent = styled.span`
width: 38%;
text-align: right;
padding-right: 10px;
font-size: 13px;
`;
export const CardWrapper = styled.article`
border-radius: 3px;
border-bottom: 1px solid #ccc;
background-color: #fff;
position: relative;
padding: 10px;
cursor: pointer;
max-width: 250px;
margin-bottom: 7px;
min-width: 230px;
`;
export const MovableCardWrapper = styled(CardWrapper)`
&:hover {
background-color: #f0f0f0;
color: #000;
}
`;
export const CardHeader = styled(Header)`
border-bottom: 1px solid #eee;
padding-bottom: 6px;
color: #000;
`;
export const CardTitle = styled(Title)`
font-size: 14px;
`;
export const CardRightContent = styled(RightContent)`
font-size: 10px;
`;
export const Detail = styled.div`
font-size: 12px;
color: #4d4d4d;
white-space: pre-wrap;
`;
export const Footer = styled.div`
border-top: 1px solid #eee;
padding-top: 6px;
text-align: right;
display: flex;
justify-content: flex-end;
flex-direction: row;
flex-wrap: wrap;
`;
export const TagSpan = styled.span`
padding: 2px 3px;
border-radius: 3px;
margin: 2px 5px;
font-size: 70%;
`;
export const AddCardLink = styled.a`
border-radius: 0 0 3px 3px;
color: #838c91;
display: block;
padding: 5px 2px;
margin-top: 10px;
position: relative;
text-decoration: none;
cursor: pointer;
&:hover {
//background-color: #cdd2d4;
color: #4d4d4d;
text-decoration: underline;
}
`;
export const LaneTitle = styled.div`
font-size: 15px;
width: 268px;
height: auto;
`;
export const LaneSection = styled.section`
background-color: #2b6aa3;
border-radius: 3px;
margin: 5px;
position: relative;
padding: 5px;
display: inline-flex;
height: auto;
flex-direction: column;
`;
export const NewLaneSection = styled(LaneSection)`
width: 200px;
`;
export const NewLaneButtons = styled.div`
margin-top: 10px;
`;
export const CardForm = styled.div`
background-color: #e3e3e3;
`;
export const InlineInput = styled.textarea`
overflow-x: hidden; /* for Firefox (issue #5) */
word-wrap: break-word;
min-height: 18px;
max-height: 112px; /* optional, but recommended */
resize: none;
width: 100%;
height: 18px;
font-size: inherit;
font-weight: inherit;
line-height: inherit;
text-align: inherit;
background-color: transparent;
box-shadow: none;
box-sizing: border-box;
border-radius: 3px;
border: 0;
padding: 0 8px;
outline: 0;
${(props) =>
props.border &&
css`
&:focus {
box-shadow: inset 0 0 0 2px #0079bf;
}
`} &:focus {
background-color: white;
}
`;

View File

@@ -1,251 +0,0 @@
import styled from 'styled-components'
import {CardWrapper, MovableCardWrapper} from './Base'
export const DeleteWrapper = styled.div`
text-align: center;
position: absolute;
top: -1px;
right: 2px;
cursor: pointer;
`
export const GenDelButton = styled.button`
transition: all 0.5s ease;
display: inline-block;
border: none;
font-size: 15px;
height: 15px;
padding: 0;
margin-top: 5px;
text-align: center;
width: 15px;
background: inherit;
cursor: pointer;
`
export const DelButton = styled.button`
transition: all 0.5s ease;
display: inline-block;
border: none;
font-size: 8px;
height: 15px;
line-height: 1px;
margin: 0 0 8px;
padding: 0;
text-align: center;
width: 15px;
background: inherit;
cursor: pointer;
opacity: 0;
${MovableCardWrapper}:hover & {
opacity: 1;
}
`
export const MenuButton = styled.button`
transition: all 0.5s ease;
display: inline-block;
border: none;
outline: none;
font-size: 16px;
font-weight: bold;
height: 15px;
line-height: 1px;
margin: 0 0 8px;
padding: 0;
text-align: center;
width: 15px;
background: inherit;
cursor: pointer;
`
export const LaneMenuHeader = styled.div`
position: relative;
margin-bottom: 4px;
text-align: center;
`
export const LaneMenuContent = styled.div`
overflow-x: hidden;
overflow-y: auto;
padding: 0 12px 12px;
`
export const LaneMenuItem = styled.div`
cursor: pointer;
display: block;
font-weight: 700;
padding: 6px 12px;
position: relative;
margin: 0 -12px;
text-decoration: none;
&:hover {
background-color: #3179ba;
color: #fff;
}
`
export const LaneMenuTitle = styled.span`
box-sizing: border-box;
color: #6b808c;
display: block;
line-height: 30px;
border-bottom: 1px solid rgba(9, 45, 66, 0.13);
margin: 0 6px;
overflow: hidden;
padding: 0 32px;
position: relative;
text-overflow: ellipsis;
white-space: nowrap;
z-index: 1;
`
export const DeleteIcon = styled.span`
position: relative;
display: inline-block;
width: 4px;
height: 4px;
opacity: 1;
overflow: hidden;
border: 1px solid #83bd42;
border-radius: 50%;
padding: 4px;
background-color: #83bd42;
${CardWrapper}:hover & {
opacity: 1;
}
&:hover::before,
&:hover::after {
background: red;
}
&:before,
&:after {
content: '';
position: absolute;
height: 2px;
width: 60%;
top: 45%;
left: 20%;
background: #fff;
border-radius: 5px;
}
&:before {
-webkit-transform: rotate(45deg);
-moz-transform: rotate(45deg);
-o-transform: rotate(45deg);
transform: rotate(45deg);
}
&:after {
-webkit-transform: rotate(-45deg);
-moz-transform: rotate(-45deg);
-o-transform: rotate(-45deg);
transform: rotate(-45deg);
}
`
export const ExpandCollapseBase = styled.span`
width: 36px;
margin: 0 auto;
font-size: 14px;
position: relative;
cursor: pointer;
`
export const CollapseBtn = styled(ExpandCollapseBase)`
&:before {
content: '';
position: absolute;
top: 0;
left: 0;
border-bottom: 7px solid #444;
border-left: 7px solid transparent;
border-right: 7px solid transparent;
border-radius: 6px;
}
&:after {
content: '';
position: absolute;
left: 4px;
top: 4px;
border-bottom: 3px solid #e3e3e3;
border-left: 3px solid transparent;
border-right: 3px solid transparent;
}
`
export const ExpandBtn = styled(ExpandCollapseBase)`
&:before {
content: '';
position: absolute;
top: 0;
left: 0;
border-top: 7px solid #444;
border-left: 7px solid transparent;
border-right: 7px solid transparent;
border-radius: 6px;
}
&:after {
content: '';
position: absolute;
left: 4px;
top: 0px;
border-top: 3px solid #e3e3e3;
border-left: 3px solid transparent;
border-right: 3px solid transparent;
}
`
export const AddButton = styled.button`
background: #5aac44;
color: #fff;
transition: background 0.3s ease;
min-height: 32px;
padding: 4px 16px;
vertical-align: top;
margin-top: 0;
margin-right: 8px;
font-weight: bold;
border-radius: 3px;
font-size: 14px;
cursor: pointer;
margin-bottom: 0;
`
export const CancelButton = styled.button`
background: #999999;
color: #fff;
transition: background 0.3s ease;
min-height: 32px;
padding: 4px 16px;
vertical-align: top;
margin-top: 0;
font-weight: bold;
border-radius: 3px;
font-size: 14px;
cursor: pointer;
margin-bottom: 0;
`
export const AddLaneLink = styled.button`
background: #2b6aa3;
border: none;
color: #fff;
transition: background 0.3s ease;
min-height: 32px;
padding: 4px 16px;
vertical-align: top;
margin-top: 0;
margin-right: 0px;
border-radius: 4px;
font-size: 13px;
cursor: pointer;
margin-bottom: 0;
`

View File

@@ -1,43 +0,0 @@
import styled, {keyframes} from 'styled-components'
const keyframeAnimation = keyframes`
0% {
transform: scale(1);
}
20% {
transform: scale(1, 2.2);
}
40% {
transform: scale(1);
}
`
export const LoaderDiv = styled.div`
text-align: center;
margin: 15px 0;
`
export const LoadingBar = styled.div`
display: inline-block;
margin: 0 2px;
width: 4px;
height: 18px;
border-radius: 4px;
animation: ${keyframeAnimation} 1s ease-in-out infinite;
background-color: #777;
&:nth-child(1) {
animation-delay: 0.0001s;
}
&:nth-child(2) {
animation-delay: 0.09s;
}
&:nth-child(3) {
animation-delay: 0.18s;
}
&:nth-child(4) {
animation-delay: 0.27s;
}
`

View File

@@ -1,15 +0,0 @@
import React from "react";
import { DeleteWrapper } from "../styles/Elements";
import { Button } from "antd";
const DeleteButton = (props) => {
return (
<DeleteWrapper {...props}>
<Button type="primary" danger>
Delete
</Button>
</DeleteWrapper>
);
};
export default DeleteButton;

View File

@@ -1,87 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
class EditableLabel extends React.Component {
constructor({value}) {
super()
this.state = {value: value}
}
getText = el => {
return el.innerText
}
onTextChange = ev => {
const value = this.getText(ev.target)
this.setState({value: value})
}
componentDidMount() {
if (this.props.autoFocus) {
this.refDiv.focus()
}
}
onBlur = () => {
this.props.onChange(this.state.value)
}
onPaste = ev => {
ev.preventDefault()
const value = ev.clipboardData.getData('text')
document.execCommand('insertText', false, value)
}
getClassName = () => {
const placeholder = this.state.value === '' ? 'comPlainTextContentEditable--has-placeholder' : ''
return `comPlainTextContentEditable ${placeholder}`
}
onKeyDown = e => {
if (e.keyCode === 13) {
this.props.onChange(this.state.value)
this.refDiv.blur()
e.preventDefault()
}
if (e.keyCode === 27) {
this.refDiv.value = this.props.value
this.setState({value: this.props.value})
// this.refDiv.blur()
e.preventDefault()
e.stopPropagation()
}
}
render() {
const placeholder = this.props.value.length > 0 ? false : this.props.placeholder
return (
<div
ref={ref => (this.refDiv = ref)}
contentEditable="true"
className={this.getClassName()}
onPaste={this.onPaste}
onBlur={this.onBlur}
onInput={this.onTextChange}
onKeyDown={this.onKeyDown}
placeholder={placeholder}
/>
)
}
}
EditableLabel.propTypes = {
onChange: PropTypes.func,
placeholder: PropTypes.string,
autoFocus: PropTypes.bool,
inline: PropTypes.bool,
value: PropTypes.string
}
EditableLabel.defaultProps = {
onChange: () => {},
placeholder: '',
autoFocus: false,
inline: false,
value: ''
}
export default EditableLabel

View File

@@ -1,106 +0,0 @@
import React, { useEffect, useRef, useState } from "react";
import PropTypes from "prop-types";
import { InlineInput } from "../styles/Base";
import autosize from "autosize";
const InlineInputController = ({ onSave, border, placeholder, value, autoFocus, resize, onCancel }) => {
const inputRef = useRef(null);
const [inputValue, setInputValue] = useState(value);
// Effect for autosizing and initial autoFocus
useEffect(() => {
if (inputRef.current && resize !== "none") {
autosize(inputRef.current);
}
if (inputRef.current && autoFocus) {
inputRef.current.focus();
}
}, [resize, autoFocus]);
// Effect to update value when props change
useEffect(() => {
setInputValue(value);
}, [value]);
const handleFocus = (e) => e.target.select();
const handleMouseDown = (e) => {
if (document.activeElement !== e.target) {
e.preventDefault();
inputRef.current.focus();
}
};
const handleBlur = () => {
updateValue();
};
const handleKeyDown = (e) => {
if (e.keyCode === 13) {
// Enter
inputRef.current.blur();
e.preventDefault();
} else if (e.keyCode === 27) {
// Escape
setInputValue(value); // Reset to initial value
inputRef.current.blur();
e.preventDefault();
} else if (e.keyCode === 9) {
// Tab
if (inputValue.length === 0) {
onCancel();
}
inputRef.current.blur();
e.preventDefault();
}
};
const updateValue = () => {
if (inputValue !== value) {
onSave(inputValue);
}
};
return (
<InlineInput
ref={inputRef}
border={border}
onMouseDown={handleMouseDown}
onFocus={handleFocus}
onBlur={handleBlur}
onKeyDown={handleKeyDown}
placeholder={inputValue.length === 0 ? undefined : placeholder}
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
dataGramm="false"
rows={1}
autoFocus={autoFocus}
/>
);
};
InlineInputController.propTypes = {
onSave: PropTypes.func,
onCancel: PropTypes.func,
border: PropTypes.bool,
placeholder: PropTypes.string,
value: PropTypes.string,
autoFocus: PropTypes.bool,
resize: PropTypes.oneOf(["none", "vertical", "horizontal"])
};
InlineInputController.defaultProps = {
onSave: () => {},
onCancel: () => {},
placeholder: "",
value: "",
border: false,
autoFocus: false,
resize: "none"
};
export default InlineInputController;

View File

@@ -1,94 +0,0 @@
import React from "react";
import PropTypes from "prop-types";
import { InlineInput } from "../styles/Base";
import autosize from "autosize";
class NewLaneTitleEditor extends React.Component {
onKeyDown = (e) => {
if (e.keyCode === 13) {
this.refInput.blur();
this.props.onSave();
e.preventDefault();
}
if (e.keyCode === 27) {
this.cancel();
e.preventDefault();
}
if (e.keyCode === 9) {
if (this.getValue().length === 0) {
this.cancel();
} else {
this.props.onSave();
}
e.preventDefault();
}
};
cancel = () => {
this.setValue("");
this.props.onCancel();
this.refInput.blur();
};
getValue = () => this.refInput.value;
setValue = (value) => (this.refInput.value = value);
saveValue = () => {
if (this.getValue() !== this.props.value) {
this.props.onSave(this.getValue());
}
};
focus = () => this.refInput.focus();
setRef = (ref) => {
this.refInput = ref;
if (this.props.resize !== "none") {
autosize(this.refInput);
}
};
render() {
const { autoFocus, resize, border, autoResize, value, placeholder } = this.props;
return (
<InlineInput
style={{ resize: resize }}
ref={this.setRef}
border={border}
onKeyDown={this.onKeyDown}
placeholder={value.length === 0 ? undefined : placeholder}
defaultValue={value}
rows={3}
autoResize={autoResize}
autoFocus={autoFocus}
/>
);
}
}
NewLaneTitleEditor.propTypes = {
onSave: PropTypes.func,
onCancel: PropTypes.func,
border: PropTypes.bool,
placeholder: PropTypes.string,
value: PropTypes.string,
autoFocus: PropTypes.bool,
autoResize: PropTypes.bool,
resize: PropTypes.oneOf(["none", "vertical", "horizontal"])
};
NewLaneTitleEditor.defaultProps = {
inputRef: () => {},
onSave: () => {},
onCancel: () => {},
placeholder: "",
value: "",
border: false,
autoFocus: false,
autoResize: false,
resize: "none"
};
export default NewLaneTitleEditor;

View File

@@ -1,11 +0,0 @@
import DeleteButton from "./DeleteButton";
import EditableLabel from "./EditableLabel";
import InlineInput from "./InlineInput";
const exports = {
DeleteButton,
EditableLabel,
InlineInput
};
export default exports;

View File

@@ -905,7 +905,7 @@ export const QUERY_JOB_CARD_DETAILS = gql`
}
joblines(
where: { removed: { _eq: false }, part_type: { _is_null: false, _nin: ["PAE", "PAS", "PASL"] } }
where: { removed: { _eq: false } }
order_by: { line_no: asc }
) {
id

View File

@@ -45,7 +45,7 @@ if (import.meta.env.PROD) {
maskAllText: false,
blockAllMedia: true
}),
new Sentry.browserTracingIntegration()
new Sentry.BrowserTracing({})
],
tracePropagationTargets: [
"api.imex.online",

View File

@@ -1,5 +1,5 @@
import { Button, Result, Space, Steps } from "antd";
import { PageHeader } from "@ant-design/pro-layout";
import { Button, Result, Space, Steps } from "antd";
import React, { useContext, useState } from "react";
import { useTranslation } from "react-i18next";
@@ -9,14 +9,10 @@ import JobsCreateJobsInfo from "../../components/jobs-create-jobs-info/jobs-crea
import JobsCreateOwnerInfoContainer from "../../components/jobs-create-owner-info/jobs-create-owner-info.container";
import JobsCreateVehicleInfoContainer from "../../components/jobs-create-vehicle-info/jobs-create-vehicle-info.container";
import JobCreateContext from "../../pages/jobs-create/jobs-create.context";
import FormsFieldChanged from "../../components/form-fields-changed-alert/form-fields-changed-alert.component";
export default function JobsCreateComponent({ form }) {
const [pageIndex, setPageIndex] = useState(0);
// const [errorMessage, setErrorMessage] = useState(null);
const [errorMessage] = useState(null);
const [errorMessage, setErrorMessage] = useState(null);
const [state] = useContext(JobCreateContext);
const { t } = useTranslation();
@@ -44,10 +40,11 @@ export default function JobsCreateComponent({ form }) {
const next = () => {
setPageIndex(pageIndex + 1);
console.log("NExt");
console.log("Next");
};
const prev = () => {
setPageIndex(pageIndex - 1);
console.log("Previous");
};
const { Step } = Steps;
@@ -56,26 +53,26 @@ export default function JobsCreateComponent({ form }) {
<PageHeader
extra={
<Space wrap>
{pageIndex > 0 && <Button onClick={() => prev()}>Previous</Button>}
{pageIndex > 0 && <Button onClick={() => prev()}>{t("general.actions.previous")}</Button>}
{pageIndex < steps.length - 1 && (
<Button
type="primary"
onClick={() => {
next();
// form
// .validateFields()
// .then((r) => {
// if (steps[pageIndex].validation) {
// setErrorMessage(null);
// next();
// } else {
// setErrorMessage(steps[pageIndex].error);
// }
// })
// .catch((error) => console.log("error", error));
form
.validateFields()
.then((r) => {
if (steps[pageIndex].validation) {
setErrorMessage(null);
next();
} else {
setErrorMessage(steps[pageIndex].error);
}
})
.catch((error) => console.log("error", error));
}}
>
Next
{t("general.actions.next")}
</Button>
)}
{pageIndex === steps.length - 1 && (
@@ -107,17 +104,17 @@ export default function JobsCreateComponent({ form }) {
}}
onClick={() => {
setPageIndex(idx);
// form
// .validateFields()
// .then((r) => {
// if (steps[pageIndex].validation) {
// setErrorMessage(null);
// setPageIndex(idx);
// } else {
// setErrorMessage(steps[pageIndex].error);
// }
// })
// .catch((error) => console.log("error", error));
form
.validateFields()
.then((r) => {
if (steps[pageIndex].validation) {
setErrorMessage(null);
setPageIndex(idx);
} else {
setErrorMessage(steps[pageIndex].error);
}
})
.catch((error) => console.log("error", error));
}}
/>
))}
@@ -147,7 +144,7 @@ export default function JobsCreateComponent({ form }) {
) : (
<div>
<ProgressButtons top />
<FormsFieldChanged form={form} />
{errorMessage ? (
<div>
<AlertComponent message={errorMessage} type="error" />

View File

@@ -9,7 +9,6 @@ import messagingReducer from "./messaging/messaging.reducer";
import modalsReducer from "./modals/modals.reducer";
import techReducer from "./tech/tech.reducer";
import userReducer from "./user/user.reducer";
import trelloReducer from "./trello/trello.reducer";
// const persistConfig = {
// key: "root",
@@ -31,8 +30,11 @@ const rootReducer = combineReducers({
modals: modalsReducer,
application: persistReducer(applicationPersistConfig, applicationReducer),
tech: techReducer,
media: mediaReducer,
trello: trelloReducer
media: mediaReducer
});
export default withReduxStateSync(rootReducer);
export default withReduxStateSync(
// persistReducer(persistConfig,
rootReducer
//)
);

View File

@@ -29,9 +29,7 @@ export const store = configureStore({
reducer: rootReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: false,
// TODO: (Note) This is a production board change
immutableCheck: false
serializableCheck: false
}).concat(middlewares),
// middleware: middlewares,
devTools: import.meta.env.DEV,

View File

@@ -1,14 +0,0 @@
import { createAction } from "redux-actions";
export const loadBoard = createAction("LOAD_BOARD");
export const addLane = createAction("ADD_LANE");
export const addCard = createAction("ADD_CARD");
export const updateCard = createAction("UPDATE_CARD");
export const removeCard = createAction("REMOVE_CARD");
export const moveCardAcrossLanes = createAction("MOVE_CARD");
export const updateCards = createAction("UPDATE_CARDS");
export const updateLanes = createAction("UPDATE_LANES");
export const updateLane = createAction("UPDATE_LANE");
export const paginateLane = createAction("PAGINATE_LANE");
export const moveLane = createAction("MOVE_LANE");
export const removeLane = createAction("REMOVE_LANE");

View File

@@ -1,35 +0,0 @@
import Lh from "../../components/trello-board/helpers/LaneHelper";
const boardReducer = (state = { lanes: [] }, action) => {
const { payload, type } = action;
switch (type) {
case "LOAD_BOARD":
return Lh.initialiseLanes(state, payload);
case "ADD_CARD":
return Lh.appendCardToLane(state, payload);
case "REMOVE_CARD":
return Lh.removeCardFromLane(state, payload);
case "MOVE_CARD":
return Lh.moveCardAcrossLanes(state, payload);
case "UPDATE_CARDS":
return Lh.updateCardsForLane(state, payload);
case "UPDATE_CARD":
return Lh.updateCardForLane(state, payload);
case "UPDATE_LANES":
return Lh.updateLanes(state, payload);
case "UPDATE_LANE":
return Lh.updateLane(state, payload);
case "PAGINATE_LANE":
return Lh.paginateLane(state, payload);
case "MOVE_LANE":
return Lh.moveLane(state, payload);
case "REMOVE_LANE":
return Lh.removeLane(state, payload);
case "ADD_LANE":
return Lh.addLane(state, payload);
default:
return state;
}
};
export default boardReducer;

View File

@@ -295,7 +295,8 @@
"dailypainttarget": "Scoreboard - Daily Paint Target",
"default_adjustment_rate": "Default Labor Deduction Adjustment Rate",
"deliver": {
"templates": "Delivery Templates"
"templates": "Delivery Templates",
"require_actual_delivery_date": "Require Actual Delivery"
},
"dms": {
"apcontrol": "AP Control Number",
@@ -646,7 +647,10 @@
"payers": "Payers"
},
"cdk_dealerid": "CDK Dealer ID",
"costsmapping": "Costs Mapping",
"dms_allocations": "DMS Allocations",
"pbs_serialnumber": "PBS Serial Number",
"profitsmapping": "Profits Mapping",
"title": "DMS"
},
"emaillater": "Email Later",
@@ -1139,6 +1143,8 @@
"download": "Download",
"edit": "Edit",
"login": "Login",
"next": "Next",
"previous": "Previous",
"print": "Print",
"refresh": "Refresh",
"remove": "Remove",
@@ -2914,7 +2920,8 @@
"parts_not_recieved_vendor": "Parts Not Received by Vendor",
"parts_received_not_scheduled": "Parts Received for Jobs Not Scheduled",
"payments_by_date": "Payments by Date",
"payments_by_date_type": "Payments by Date and Type",
"payments_by_date_payment": "Payments by Date and Payment Type",
"payments_by_date_type": "Payments by Date and Customer Type",
"production_by_category": "Production by Category",
"production_by_category_one": "Production filtered by Category",
"production_by_csr": "Production by CSR",
@@ -2933,6 +2940,8 @@
"purchases_by_cost_center_summary": "Purchases by Cost Center (Summary)",
"purchases_by_date_range_detail": "Purchases by Date - Detail",
"purchases_by_date_range_summary": "Purchases by Date - Summary",
"purchases_by_ro_detail_date": "Purchases by RO - Detail",
"purchases_by_ro_summary_date": "Purchases by RO - Summary",
"purchases_by_vendor_detailed_date_range": "Purchases By Vendor - Detailed",
"purchases_by_vendor_summary_date_range": "Purchases by Vendor - Summary",
"purchases_grouped_by_vendor_detailed": "Purchases Grouped by Vendor - Detailed",
@@ -3460,18 +3469,6 @@
"validation": {
"unique_vendor_name": "You must enter a unique vendor name."
}
},
"trello": {
"labels": {
"add_card": "Add Card",
"add_lane": "Add Lane",
"delete_lane": "Delete Lane",
"lane_actions": "Lane Actions",
"title": "Title",
"description": "Description",
"label": "Label",
"cancel": "Cancel"
}
}
}
}
}

View File

@@ -295,7 +295,8 @@
"dailypainttarget": "",
"default_adjustment_rate": "",
"deliver": {
"templates": ""
"templates": "",
"require_actual_delivery_date": ""
},
"dms": {
"apcontrol": "",
@@ -646,7 +647,10 @@
"payers": ""
},
"cdk_dealerid": "",
"costsmapping": "",
"dms_allocations": "",
"pbs_serialnumber": "",
"profitsmapping": "",
"title": ""
},
"emaillater": "",
@@ -1139,6 +1143,8 @@
"download": "",
"edit": "Editar",
"login": "",
"next": "",
"previous": "",
"print": "",
"refresh": "",
"remove": "",
@@ -2914,6 +2920,7 @@
"parts_not_recieved_vendor": "",
"parts_received_not_scheduled": "",
"payments_by_date": "",
"payments_by_date_payment": "",
"payments_by_date_type": "",
"production_by_category": "",
"production_by_category_one": "",
@@ -2933,6 +2940,8 @@
"purchases_by_cost_center_summary": "",
"purchases_by_date_range_detail": "",
"purchases_by_date_range_summary": "",
"purchases_by_ro_detail_date": "",
"purchases_by_ro_summary_date": "",
"purchases_by_vendor_detailed_date_range": "",
"purchases_by_vendor_summary_date_range": "",
"purchases_grouped_by_vendor_detailed": "",
@@ -3460,18 +3469,6 @@
"validation": {
"unique_vendor_name": ""
}
},
"trello": {
"labels": {
"add_card": "",
"add_lane": "",
"delete_lane": "",
"lane_actions": "",
"title": "",
"description": "",
"label": "",
"cancel": ""
}
}
}
}
}

View File

@@ -295,7 +295,8 @@
"dailypainttarget": "",
"default_adjustment_rate": "",
"deliver": {
"templates": ""
"templates": "",
"require_actual_delivery_date": ""
},
"dms": {
"apcontrol": "",
@@ -646,7 +647,10 @@
"payers": ""
},
"cdk_dealerid": "",
"costsmapping": "",
"dms_allocations": "",
"pbs_serialnumber": "",
"profitsmapping": "",
"title": ""
},
"emaillater": "",
@@ -1139,6 +1143,8 @@
"download": "",
"edit": "modifier",
"login": "",
"next": "",
"previous": "",
"print": "",
"refresh": "",
"remove": "",
@@ -2914,6 +2920,7 @@
"parts_not_recieved_vendor": "",
"parts_received_not_scheduled": "",
"payments_by_date": "",
"payments_by_date_payment": "",
"payments_by_date_type": "",
"production_by_category": "",
"production_by_category_one": "",
@@ -2933,6 +2940,8 @@
"purchases_by_cost_center_summary": "",
"purchases_by_date_range_detail": "",
"purchases_by_date_range_summary": "",
"purchases_by_ro_detail_date": "",
"purchases_by_ro_summary_date": "",
"purchases_by_vendor_detailed_date_range": "",
"purchases_by_vendor_summary_date_range": "",
"purchases_grouped_by_vendor_detailed": "",
@@ -3460,18 +3469,6 @@
"validation": {
"unique_vendor_name": ""
}
},
"trello": {
"labels": {
"add_card": "",
"add_lane": "",
"delete_lane": "",
"lane_actions": "",
"title": "",
"description": "",
"label": "",
"cancel": ""
}
}
}
}
}

View File

@@ -1081,6 +1081,32 @@ export const TemplateList = (type, context) => {
},
group: "purchases"
},
purchases_by_ro_detail_date: {
title: i18n.t("reportcenter.templates.purchases_by_ro_detail_date"),
description: "",
subject: i18n.t("reportcenter.templates.purchases_by_ro_detail_date"),
key: "purchases_by_ro_detail_date",
//idtype: "vendor",
disabled: false,
rangeFilter: {
object: i18n.t("reportcenter.labels.objects.jobs"),
field: i18n.t("jobs.fields.date_invoiced")
},
group: "purchases"
},
purchases_by_ro_summary_date: {
title: i18n.t("reportcenter.templates.purchases_by_ro_summary_date"),
description: "",
subject: i18n.t("reportcenter.templates.purchases_by_ro_summary_date"),
key: "purchases_by_ro_summary_date",
//idtype: "vendor",
disabled: false,
rangeFilter: {
object: i18n.t("reportcenter.labels.objects.jobs"),
field: i18n.t("jobs.fields.date_invoiced")
},
group: "purchases"
},
job_costing_ro_date_summary: {
title: i18n.t("reportcenter.templates.job_costing_ro_date_summary"),
description: "",
@@ -1168,6 +1194,17 @@ export const TemplateList = (type, context) => {
},
group: "customers"
},
payments_by_date_payment: {
title: i18n.t("reportcenter.templates.payments_by_date_payment"),
subject: i18n.t("reportcenter.templates.payments_by_date_payment"),
key: "payments_by_date_payment",
disabled: false,
rangeFilter: {
object: i18n.t("reportcenter.labels.objects.payments"),
field: i18n.t("payments.fields.date")
},
group: "customers"
},
schedule: {
title: i18n.t("reportcenter.templates.schedule"),
subject: i18n.t("reportcenter.templates.schedule"),

View File

@@ -5,7 +5,7 @@ import * as path from "path";
import * as url from "url";
import { defineConfig } from "vite";
import { ViteEjsPlugin } from "vite-plugin-ejs";
import eslint from "vite-plugin-eslint";
import eslint from 'vite-plugin-eslint';
//import CompressionPlugin from 'vite-plugin-compression';
import { VitePWA } from "vite-plugin-pwa";
@@ -103,7 +103,7 @@ export default defineConfig({
}),
reactVirtualized(),
react(),
eslint()
eslint(),
// CompressionPlugin(), //Cloudfront already compresses assets, so not needed.
],
define: {

View File

@@ -0,0 +1,10 @@
-- Could not auto-generate a down migration.
-- Please write an appropriate down migration for the SQL below:
-- CREATE OR REPLACE VIEW "public"."joblines_status" AS
-- SELECT j.jobid,
-- j.status,
-- count(1) AS count,
-- j.part_type
-- FROM joblines j
-- WHERE ((j.part_type IS NOT NULL) AND (j.part_type <> 'PAE'::text) AND (j.part_type <> 'PAS'::text) AND (j.part_type <> 'PASL'::text) AND (j.part_qty <> 0) AND (j.act_price <> 0) AND (j.removed IS FALSE))
-- GROUP BY j.jobid, j.status, j.part_type;

View File

@@ -0,0 +1,8 @@
CREATE OR REPLACE VIEW "public"."joblines_status" AS
SELECT j.jobid,
j.status,
count(1) AS count,
j.part_type
FROM joblines j
WHERE ((j.part_type IS NOT NULL) AND (j.part_type <> 'PAE'::text) AND (j.part_type <> 'PAS'::text) AND (j.part_type <> 'PASL'::text) AND (j.part_qty <> 0) AND (j.act_price <> 0) AND (j.removed IS FALSE))
GROUP BY j.jobid, j.status, j.part_type;

View File

@@ -0,0 +1,10 @@
-- Could not auto-generate a down migration.
-- Please write an appropriate down migration for the SQL below:
-- CREATE OR REPLACE VIEW "public"."joblines_status" AS
-- SELECT j.jobid,
-- j.status,
-- count(1) AS count,
-- j.part_type
-- FROM joblines j
-- WHERE ((j.part_type IS NOT NULL) AND (j.part_type <> 'PAE'::text) AND (j.part_type <> 'PAS'::text) AND (j.part_type <> 'PASL'::text) AND (j.part_qty <> (0)::numeric) AND (j.act_price <> (0)::numeric) AND (j.removed IS FALSE))
-- GROUP BY j.jobid, j.status, j.part_type;

View File

@@ -0,0 +1,8 @@
CREATE OR REPLACE VIEW "public"."joblines_status" AS
SELECT j.jobid,
j.status,
count(1) AS count,
j.part_type
FROM joblines j
WHERE ((j.part_type IS NOT NULL) AND (j.part_type <> 'PAE'::text) AND (j.part_type <> 'PAS'::text) AND (j.part_type <> 'PASL'::text) AND (j.part_qty <> (0)::numeric) AND (j.act_price <> (0)::numeric) AND (j.removed IS FALSE))
GROUP BY j.jobid, j.status, j.part_type;

View File

@@ -174,10 +174,7 @@ async function OpenSearchUpdateHandler(req, res) {
const bulkOperation = [];
slicedArray.forEach((bill) => {
bulkOperation.push({ index: { _index: "bills", _id: bill.id } });
bulkOperation.push({
...omit(bill, ["job"]),
bodyshopid: bill.job.bodyshopid
});
bulkOperation.push({ ...bill, bodyshopid: bill.job.bodyshopid });
});
promiseQueue.push(bulkOperation);
}

2963
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -19,43 +19,48 @@
"makeitpretty": "prettier --write \"**/*.{css,js,json,jsx,scss}\""
},
"dependencies": {
"@aws-sdk/client-secrets-manager": "^3.583.0",
"@aws-sdk/client-ses": "^3.583.0",
"@aws-sdk/credential-provider-node": "^3.583.0",
"@opensearch-project/opensearch": "^2.8.0",
"aws4": "^1.13.0",
"axios": "^1.7.2",
"@aws-sdk/client-secrets-manager": "^3.525.0",
"@aws-sdk/client-ses": "^3.525.0",
"@aws-sdk/credential-provider-node": "^3.525.0",
"@azure/storage-blob": "^12.17.0",
"@opensearch-project/opensearch": "^2.5.0",
"aws4": "^1.12.0",
"axios": "^1.6.5",
"better-queue": "^3.8.12",
"bluebird": "^3.7.2",
"body-parser": "^1.20.2",
"cloudinary": "^2.2.0",
"canvas": "^2.11.2",
"chart.js": "^4.4.3",
"cloudinary": "^2.0.2",
"compression": "^1.7.4",
"cookie-parser": "^1.4.6",
"cors": "2.8.5",
"csrf": "^3.1.0",
"dinero.js": "^1.9.1",
"dotenv": "^16.4.5",
"express": "^4.19.2",
"firebase-admin": "^12.1.1",
"express": "^4.18.3",
"firebase-admin": "^12.0.0",
"graphql": "^16.8.1",
"graphql-request": "^6.1.0",
"graylog2": "^0.2.1",
"inline-css": "^4.0.2",
"intuit-oauth": "^4.1.2",
"json-2-csv": "^5.5.1",
"intuit-oauth": "^4.0.0",
"json-2-csv": "^5.5.0",
"lodash": "^4.17.21",
"moment": "^2.30.1",
"moment-timezone": "^0.5.45",
"multer": "^1.4.5-lts.1",
"node-mailjet": "^6.0.5",
"node-persist": "^4.0.1",
"nodemailer": "^6.9.13",
"phone": "^3.1.44",
"node-quickbooks": "^2.0.44",
"nodemailer": "^6.9.11",
"phone": "^3.1.42",
"recursive-diff": "^1.0.9",
"rimraf": "^5.0.7",
"soap": "^1.0.3",
"socket.io": "^4.7.5",
"rimraf": "^5.0.5",
"soap": "^1.0.0",
"socket.io": "^4.7.4",
"ssh2-sftp-client": "^10.0.3",
"stripe": "^14.19.0",
"twilio": "^4.23.0",
"uuid": "^9.0.1",
"xml2js": "^0.6.2",

View File

@@ -71,16 +71,7 @@ exports.default = function ({ bodyshop, jobs_by_pk, qbo = false, items, taxCodes
{
local: false,
federal: InstanceManager({ imex: true, rome: false }),
state:
jobs_by_pk.state_tax_rate === 0
? false
: jobline.db_ref === "900511" ||
jobline.db_ref === "900510" ||
(jobline.mod_lb_hrs === 0 && //Extending IO-1375 as a part of IO-2023
jobline.act_price > 0 &&
jobline.lbr_op === "OP14")
? true
: jobline.tax_part
state: checkStateTax(jobline, jobs_by_pk)
},
bodyshop.md_responsibility_centers.sales_tax_codes
);
@@ -156,7 +147,7 @@ exports.default = function ({ bodyshop, jobs_by_pk, qbo = false, items, taxCodes
{
local: false,
federal: InstanceManager({ imex: true, rome: false }),
state: jobs_by_pk.state_tax_rate === 0 ? false : true
state: jobs_by_pk.tax_lbr_rt === 0 ? false : true
},
bodyshop.md_responsibility_centers.sales_tax_codes
);
@@ -224,7 +215,7 @@ exports.default = function ({ bodyshop, jobs_by_pk, qbo = false, items, taxCodes
{
local: false,
federal: InstanceManager({ imex: true, rome: false }),
state: jobs_by_pk.state_tax_rate === 0 ? false : true
state: jobs_by_pk.tax_paint_mat_rt === 0 ? false : true
},
bodyshop.md_responsibility_centers.sales_tax_codes
);
@@ -293,7 +284,7 @@ exports.default = function ({ bodyshop, jobs_by_pk, qbo = false, items, taxCodes
{
local: false,
federal: InstanceManager({ imex: true, rome: false }),
state: jobs_by_pk.state_tax_rate === 0 ? false : true
state: jobs_by_pk.tax_shop_mat_rt === 0 ? false : true
},
bodyshop.md_responsibility_centers.sales_tax_codes
);
@@ -379,7 +370,7 @@ exports.default = function ({ bodyshop, jobs_by_pk, qbo = false, items, taxCodes
{
local: false,
federal: InstanceManager({ imex: true, rome: false }),
state: jobs_by_pk.state_tax_rate === 0 ? false : true
state: jobs_by_pk.tax_tow_rt === 0 ? false : true
},
bodyshop.md_responsibility_centers.sales_tax_codes
);
@@ -439,7 +430,7 @@ exports.default = function ({ bodyshop, jobs_by_pk, qbo = false, items, taxCodes
{
local: false,
federal: InstanceManager({ imex: true, rome: false }),
state: jobs_by_pk.state_tax_rate === 0 ? false : true
state: jobs_by_pk.tax_str_rt === 0 ? false : true
},
bodyshop.md_responsibility_centers.sales_tax_codes
);
@@ -499,7 +490,7 @@ exports.default = function ({ bodyshop, jobs_by_pk, qbo = false, items, taxCodes
{
local: false,
federal: InstanceManager({ imex: true, rome: false }),
state: jobs_by_pk.state_tax_rate === 0 ? false : true
state: jobs_by_pk.tax_lbr_rt === 0 ? false : true
},
bodyshop.md_responsibility_centers.sales_tax_codes
);
@@ -641,7 +632,7 @@ exports.default = function ({ bodyshop, jobs_by_pk, qbo = false, items, taxCodes
}
}
//QB USA with GST
//QB USA with GST and PST
//This was required for the No. 1 Collision Group.
if (
bodyshop.accountingconfig &&
@@ -660,6 +651,17 @@ exports.default = function ({ bodyshop, jobs_by_pk, qbo = false, items, taxCodes
Qty: 1
}
});
InvoiceLineAdd.push({
DetailType: "SalesItemLineDetail",
Amount: Dinero(jobs_by_pk.job_totals.totals.state_tax).toFormat(DineroQbFormat),
SalesItemLineDetail: {
...(jobs_by_pk.class ? { ClassRef: { value: classes[jobs_by_pk.class] } } : {}),
ItemRef: {
value: items[bodyshop.md_responsibility_centers.taxes.state.accountitem]
},
Qty: 1
}
});
}
} else {
//Handle insurance profile adjustments
@@ -879,6 +881,64 @@ exports.createMultiQbPayerLines = function ({ bodyshop, jobs_by_pk, qbo = false,
return InvoiceLineAdd;
};
function checkStateTax(jobline, jobs_by_pk) {
const isPaintOrShopMat = jobline.db_ref === "936008" || jobline.db_ref === "936007";
if (isPaintOrShopMat) {
if (jobline.db_ref === "936008") {
if (jobs_by_pk.tax_paint_mat_rt === 0) {
return false;
} else {
return true;
}
}
if (jobline.db_ref === "936007") {
if (jobs_by_pk.tax_shop_mat_rt === 0) {
return false;
} else {
return true;
}
}
}
const isAdditionalCost =
(jobline.lbr_op === "OP13" ||
(jobline.lbr_op === "OP14" && jobline.act_price > 0 && jobline.mod_lb_hrs === 0) ||
(jobline.db_ref && jobline.db_ref.startsWith("9360")) ||
(jobline.db_ref && jobline.db_ref.startsWith("90051"))) &&
!isPaintOrShopMat;
if (!jobline.part_type && isAdditionalCost) {
if (jobs_by_pk.tax_lbr_rt === 0) {
return false;
} else {
return true;
}
}
if (jobline.tax_part === false) {
return false;
} else {
if (jobline.part_type) {
if (
!jobs_by_pk.parts_tax_rates[`${jobline.part_type.toUpperCase()}`] ||
jobs_by_pk.parts_tax_rates[`${jobline.part_type.toUpperCase()}`].prt_tax_in === false ||
jobs_by_pk.parts_tax_rates[`${jobline.part_type.toUpperCase()}`].prt_tax_rt === 0
) {
return false;
} else {
return true;
}
} else {
if (jobs_by_pk.tax_lbr_rt === 0) {
return false;
} else {
return true;
}
}
}
}
function CheckQBOUSATaxID({ jobline, job, type }) {
//Replacing this to be all non-taxable items with the refactor of parts tax rates.
return "NON";

View File

@@ -16,403 +16,402 @@ const { DiscountNotAlreadyCounted } = InstanceManager({
promanager: "USE_ROME"
});
exports.defaultRoute = async function (req, res) {
try {
CdkBase.createLogEvent(req, "DEBUG", `Received request to calculate allocations for ${req.body.jobid}`);
const jobData = await QueryJobData(req, req.BearerToken, req.body.jobid);
return res.status(200).json({ data: calculateAllocations(req, jobData) });
} catch (error) {
console.log(error);
CdkBase.createLogEvent(req, "ERROR", `Error encountered in CdkCalculateAllocations. ${error}`);
res.status(500).json({ error: `Error encountered in CdkCalculateAllocations. ${error}` });
}
};
exports.default = async function (socket, jobid) {
try {
CdkBase.createLogEvent(socket, "DEBUG", `Received request to calculate allocations for ${jobid}`);
const job = await QueryJobData(socket, jobid);
const { bodyshop } = job;
const taxAllocations = InstanceManager({
executeFunction: true,
deubg: true,
args: [],
imex: () => ({
local: {
center: bodyshop.md_responsibility_centers.taxes.local.name,
sale: Dinero(job.job_totals.totals.local_tax),
cost: Dinero(),
profitCenter: bodyshop.md_responsibility_centers.taxes.local,
costCenter: bodyshop.md_responsibility_centers.taxes.local
},
state: {
center: bodyshop.md_responsibility_centers.taxes.state.name,
sale: Dinero(job.job_totals.totals.state_tax),
cost: Dinero(),
profitCenter: bodyshop.md_responsibility_centers.taxes.state,
costCenter: bodyshop.md_responsibility_centers.taxes.state
},
federal: {
center: bodyshop.md_responsibility_centers.taxes.federal.name,
sale: Dinero(job.job_totals.totals.federal_tax),
cost: Dinero(),
profitCenter: bodyshop.md_responsibility_centers.taxes.federal,
costCenter: bodyshop.md_responsibility_centers.taxes.federal
}
}),
rome: () => ({
tax_ty1: {
center: bodyshop.md_responsibility_centers.taxes[`tax_ty1`].name,
sale: Dinero(job.job_totals.totals.us_sales_tax_breakdown[`ty1Tax`]),
cost: Dinero(),
profitCenter: bodyshop.md_responsibility_centers.taxes[`tax_ty1`],
costCenter: bodyshop.md_responsibility_centers.taxes[`tax_ty1`]
},
tax_ty2: {
center: bodyshop.md_responsibility_centers.taxes[`tax_ty2`].name,
sale: Dinero(job.job_totals.totals.us_sales_tax_breakdown[`ty2Tax`]),
cost: Dinero(),
profitCenter: bodyshop.md_responsibility_centers.taxes[`tax_ty2`],
costCenter: bodyshop.md_responsibility_centers.taxes[`tax_ty2`]
},
tax_ty3: {
center: bodyshop.md_responsibility_centers.taxes[`tax_ty3`].name,
sale: Dinero(job.job_totals.totals.us_sales_tax_breakdown[`ty3Tax`]),
cost: Dinero(),
profitCenter: bodyshop.md_responsibility_centers.taxes[`tax_ty3`],
costCenter: bodyshop.md_responsibility_centers.taxes[`tax_ty3`]
},
tax_ty4: {
center: bodyshop.md_responsibility_centers.taxes[`tax_ty4`].name,
sale: Dinero(job.job_totals.totals.us_sales_tax_breakdown[`ty4Tax`]),
cost: Dinero(),
profitCenter: bodyshop.md_responsibility_centers.taxes[`tax_ty4`],
costCenter: bodyshop.md_responsibility_centers.taxes[`tax_ty4`]
},
tax_ty5: {
center: bodyshop.md_responsibility_centers.taxes[`tax_ty5`].name,
sale: Dinero(job.job_totals.totals.us_sales_tax_breakdown[`ty5Tax`]),
cost: Dinero(),
profitCenter: bodyshop.md_responsibility_centers.taxes[`tax_ty5`],
costCenter: bodyshop.md_responsibility_centers.taxes[`tax_ty5`]
}
})
});
//Determine if there are MAPA and MASH lines already on the estimate.
//If there are, don't do anything extra (mitchell estimate)
//Otherwise, calculate them and add them to the default MAPA and MASH centers.
let hasMapaLine = false;
let hasMashLine = false;
const profitCenterHash = job.joblines.reduce((acc, val) => {
//Check the Parts Assignment
if (val.db_ref === "936008") {
//If either of these DB REFs change, they also need to change in job-totals/job-costing calculations.
hasMapaLine = true;
}
if (val.db_ref === "936007") {
hasMashLine = true;
}
if (val.profitcenter_part) {
if (!acc[val.profitcenter_part]) acc[val.profitcenter_part] = Dinero();
let DineroAmount = Dinero({
amount: Math.round(val.act_price * 100)
}).multiply(val.part_qty || 1);
DineroAmount = DineroAmount.add(
((val.prt_dsmk_m && val.prt_dsmk_m !== 0) || (val.prt_dsmk_p && val.prt_dsmk_p !== 0)) &&
DiscountNotAlreadyCounted(val, job.joblines)
? val.prt_dsmk_m
? Dinero({ amount: Math.round(val.prt_dsmk_m * 100) })
: Dinero({
amount: Math.round(val.act_price * 100)
})
.multiply(val.part_qty || 0)
.percentage(Math.abs(val.prt_dsmk_p || 0))
.multiply(val.prt_dsmk_p > 0 ? 1 : -1)
: Dinero()
);
acc[val.profitcenter_part] = acc[val.profitcenter_part].add(DineroAmount);
}
if (val.profitcenter_labor && val.mod_lbr_ty) {
//Check the Labor Assignment.
if (!acc[val.profitcenter_labor]) acc[val.profitcenter_labor] = Dinero();
acc[val.profitcenter_labor] = acc[val.profitcenter_labor].add(
Dinero({
amount: Math.round(job[`rate_${val.mod_lbr_ty.toLowerCase()}`] * 100)
}).multiply(val.mod_lb_hrs)
);
}
return acc;
}, {});
const selectedDmsAllocationConfig = bodyshop.md_responsibility_centers.dms_defaults.find(
(d) => d.name === job.dms_allocation
);
CdkBase.createLogEvent(
socket,
"DEBUG",
`Using DMS Allocation ${selectedDmsAllocationConfig && selectedDmsAllocationConfig.name} for cost export.`
);
let costCenterHash = {};
//Check whether to skip this if PBS and using AP module.
const disablebillwip = !!bodyshop?.pbs_configuration?.disablebillwip;
if (!disablebillwip) {
costCenterHash = job.bills.reduce((bill_acc, bill_val) => {
bill_val.billlines.map((line_val) => {
if (!bill_acc[selectedDmsAllocationConfig.costs[line_val.cost_center]])
bill_acc[selectedDmsAllocationConfig.costs[line_val.cost_center]] = Dinero();
let lineDinero = Dinero({
amount: Math.round((line_val.actual_cost || 0) * 100)
})
.multiply(line_val.quantity)
.multiply(bill_val.is_credit_memo ? -1 : 1);
bill_acc[selectedDmsAllocationConfig.costs[line_val.cost_center]] =
bill_acc[selectedDmsAllocationConfig.costs[line_val.cost_center]].add(lineDinero);
return null;
});
return bill_acc;
}, {});
}
job.timetickets.forEach((ticket) => {
//Get the total amount of the ticket.
let TicketTotal = Dinero({
amount: Math.round(
ticket.rate *
(ticket.employee && ticket.employee.flat_rate ? ticket.productivehrs || 0 : ticket.actualhrs || 0) *
100
)
});
//Add it to the right cost center.
if (!costCenterHash[selectedDmsAllocationConfig.costs[ticket.ciecacode]])
costCenterHash[selectedDmsAllocationConfig.costs[ticket.ciecacode]] = Dinero();
costCenterHash[selectedDmsAllocationConfig.costs[ticket.ciecacode]] =
costCenterHash[selectedDmsAllocationConfig.costs[ticket.ciecacode]].add(TicketTotal);
});
if (!hasMapaLine && job.job_totals.rates.mapa.total.amount > 0) {
// console.log("Adding MAPA Line Manually.");
const mapaAccountName = selectedDmsAllocationConfig.profits.MAPA;
const mapaAccount = bodyshop.md_responsibility_centers.profits.find((c) => c.name === mapaAccountName);
if (mapaAccount) {
if (!profitCenterHash[mapaAccountName]) profitCenterHash[mapaAccountName] = Dinero();
profitCenterHash[mapaAccountName] = profitCenterHash[mapaAccountName].add(
Dinero(job.job_totals.rates.mapa.total)
);
} else {
//console.log("NO MAPA ACCOUNT FOUND!!");
}
}
if (!hasMashLine && job.job_totals.rates.mash.total.amount > 0) {
// console.log("Adding MASH Line Manually.");
const mashAccountName = selectedDmsAllocationConfig.profits.MASH;
const mashAccount = bodyshop.md_responsibility_centers.profits.find((c) => c.name === mashAccountName);
if (mashAccount) {
if (!profitCenterHash[mashAccountName]) profitCenterHash[mashAccountName] = Dinero();
profitCenterHash[mashAccountName] = profitCenterHash[mashAccountName].add(
Dinero(job.job_totals.rates.mash.total)
);
} else {
// console.log("NO MASH ACCOUNT FOUND!!");
}
}
console.log(
Number.isInteger(bodyshop?.cdk_configuration?.sendmaterialscosting),
typeof Number.isInteger(bodyshop?.cdk_configuration?.sendmaterialscosting)
);
if (!!bodyshop?.cdk_configuration?.sendmaterialscosting) {
//Manually send the percentage of the costing.
//Paint Mat
const mapaAccountName = selectedDmsAllocationConfig.costs.MAPA;
const mapaAccount = bodyshop.md_responsibility_centers.costs.find((c) => c.name === mapaAccountName);
if (mapaAccount) {
if (!costCenterHash[mapaAccountName])
costCenterHash[mapaAccountName] = Dinero();
if (job.bodyshop.use_paint_scale_data === true) {
if (job.mixdata.length > 0) {
costCenterHash[mapaAccountName] = costCenterHash[
mapaAccountName
].add(
Dinero({
amount: Math.round(
((job.mixdata[0] && job.mixdata[0].totalliquidcost) || 0) *
100
),
})
);
} else {
costCenterHash[mapaAccountName] = costCenterHash[
mapaAccountName
].add(
Dinero(job.job_totals.rates.mapa.total).percentage(
bodyshop?.cdk_configuration?.sendmaterialscosting
)
);
}
} else {
costCenterHash[mapaAccountName] = costCenterHash[mapaAccountName].add(
Dinero(job.job_totals.rates.mapa.total).percentage(
bodyshop?.cdk_configuration?.sendmaterialscosting
)
);
}
} else {
//console.log("NO MAPA ACCOUNT FOUND!!");
}
//Shop Mat
const mashAccountName = selectedDmsAllocationConfig.costs.MASH;
const mashAccount = bodyshop.md_responsibility_centers.costs.find((c) => c.name === mashAccountName);
if (mashAccount) {
if (!costCenterHash[mashAccountName]) costCenterHash[mashAccountName] = Dinero();
costCenterHash[mashAccountName] = costCenterHash[mashAccountName].add(
Dinero(job.job_totals.rates.mash.total).percentage(bodyshop?.cdk_configuration?.sendmaterialscosting)
);
} else {
// console.log("NO MASH ACCOUNT FOUND!!");
}
}
const { ca_bc_pvrt } = job;
if (ca_bc_pvrt) {
// const pvrtAccount = bodyshop.md_responsibility_centers.profits.find(
// (c) => c.name === mashAccountName
// );
taxAllocations.state.sale = taxAllocations.state.sale.add(
Dinero({ amount: Math.round((ca_bc_pvrt || 0) * 100) })
);
}
if (job.towing_payable && job.towing_payable !== 0) {
const towAccountName = selectedDmsAllocationConfig.profits.TOW;
const towAccount = bodyshop.md_responsibility_centers.profits.find((c) => c.name === towAccountName);
if (towAccount) {
if (!profitCenterHash[towAccountName]) profitCenterHash[towAccountName] = Dinero();
profitCenterHash[towAccountName] = profitCenterHash[towAccountName].add(
Dinero({
amount: Math.round((job.towing_payable || 0) * 100)
})
);
} else {
// console.log("NO MASH ACCOUNT FOUND!!");
}
}
if (job.storage_payable && job.storage_payable !== 0) {
const storageAccountName = selectedDmsAllocationConfig.profits.TOW;
const towAccount = bodyshop.md_responsibility_centers.profits.find((c) => c.name === storageAccountName);
if (towAccount) {
if (!profitCenterHash[storageAccountName]) profitCenterHash[storageAccountName] = Dinero();
profitCenterHash[storageAccountName] = profitCenterHash[storageAccountName].add(
Dinero({
amount: Math.round((job.storage_payable || 0) * 100)
})
);
} else {
// console.log("NO MASH ACCOUNT FOUND!!");
}
}
if (job.adjustment_bottom_line && job.adjustment_bottom_line !== 0) {
const otherAccountName = selectedDmsAllocationConfig.profits.PAO;
const otherAccount = bodyshop.md_responsibility_centers.profits.find((c) => c.name === otherAccountName);
if (otherAccount) {
if (!profitCenterHash[otherAccountName]) profitCenterHash[otherAccountName] = Dinero();
profitCenterHash[otherAccountName] = profitCenterHash[otherAccountName].add(
Dinero({
amount: Math.round((job.adjustment_bottom_line || 0) * 100)
})
);
} else {
// console.log("NO MASH ACCOUNT FOUND!!");
}
}
if (InstanceManager({ rome: true })) {
//profile level adjustments
Object.keys(job.job_totals.parts.adjustments).forEach((key) => {
const accountName = selectedDmsAllocationConfig.profits[key];
const otherAccount = bodyshop.md_responsibility_centers.profits.find((c) => c.name === accountName);
if (otherAccount) {
if (!profitCenterHash[accountName]) profitCenterHash[accountName] = Dinero();
profitCenterHash[accountName] = profitCenterHash[accountName].add(
Dinero(job.job_totals.parts.adjustments[key])
);
} else {
CdkBase.createLogEvent(
socket,
"ERROR",
`Error encountered in CdkCalculateAllocations. Unable to find adjustment account. ${error}`
);
}
});
}
const jobAllocations = _.union(Object.keys(profitCenterHash), Object.keys(costCenterHash)).map((key) => {
const profitCenter = bodyshop.md_responsibility_centers.profits.find((c) => c.name === key);
const costCenter = bodyshop.md_responsibility_centers.costs.find((c) => c.name === key);
return {
center: key,
sale: profitCenterHash[key] ? profitCenterHash[key] : Dinero(),
cost: costCenterHash[key] ? costCenterHash[key] : Dinero(),
profitCenter,
costCenter
};
});
return [
...jobAllocations,
...Object.keys(taxAllocations)
.filter((key) => taxAllocations[key].sale.getAmount() > 0 || taxAllocations[key].cost.getAmount() > 0)
.map((key) => {
if (
key === "federal" &&
selectedDmsAllocationConfig.gst_override &&
selectedDmsAllocationConfig.gst_override !== ""
) {
const ret = { ...taxAllocations[key], tax: key };
ret.costCenter.dms_acctnumber = selectedDmsAllocationConfig.gst_override;
ret.profitCenter.dms_acctnumber = selectedDmsAllocationConfig.gst_override;
return ret;
} else {
return { ...taxAllocations[key], tax: key };
}
})
];
const jobData = await QueryJobData(socket, "Bearer " + socket.handshake.auth.token, jobid);
return calculateAllocations(socket, jobData);
} catch (error) {
console.log(error);
CdkBase.createLogEvent(socket, "ERROR", `Error encountered in CdkCalculateAllocations. ${error}`);
}
};
async function QueryJobData(socket, jobid) {
CdkBase.createLogEvent(socket, "DEBUG", `Querying job data for id ${jobid}`);
async function QueryJobData(connectionData, token, jobid) {
CdkBase.createLogEvent(connectionData, "DEBUG", `Querying job data for id ${jobid}`);
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {});
const result = await client
.setHeaders({ Authorization: `Bearer ${socket.handshake.auth.token}` })
.request(queries.GET_CDK_ALLOCATIONS, { id: jobid });
CdkBase.createLogEvent(socket, "TRACE", `Job data query result ${JSON.stringify(result, null, 2)}`);
const result = await client.setHeaders({ Authorization: token }).request(queries.GET_CDK_ALLOCATIONS, { id: jobid });
CdkBase.createLogEvent(connectionData, "TRACE", `Job data query result ${JSON.stringify(result, null, 2)}`);
return result.jobs_by_pk;
}
function calculateAllocations(connectionData, job) {
const { bodyshop } = job;
const taxAllocations = InstanceManager({
executeFunction: true,
deubg: true,
args: [],
imex: () => ({
local: {
center: bodyshop.md_responsibility_centers.taxes.local.name,
sale: Dinero(job.job_totals.totals.local_tax),
cost: Dinero(),
profitCenter: bodyshop.md_responsibility_centers.taxes.local,
costCenter: bodyshop.md_responsibility_centers.taxes.local
},
state: {
center: bodyshop.md_responsibility_centers.taxes.state.name,
sale: Dinero(job.job_totals.totals.state_tax),
cost: Dinero(),
profitCenter: bodyshop.md_responsibility_centers.taxes.state,
costCenter: bodyshop.md_responsibility_centers.taxes.state
},
federal: {
center: bodyshop.md_responsibility_centers.taxes.federal.name,
sale: Dinero(job.job_totals.totals.federal_tax),
cost: Dinero(),
profitCenter: bodyshop.md_responsibility_centers.taxes.federal,
costCenter: bodyshop.md_responsibility_centers.taxes.federal
}
}),
rome: () => ({
tax_ty1: {
center: bodyshop.md_responsibility_centers.taxes[`tax_ty1`].name,
sale: Dinero(job.job_totals.totals.us_sales_tax_breakdown[`ty1Tax`]),
cost: Dinero(),
profitCenter: bodyshop.md_responsibility_centers.taxes[`tax_ty1`],
costCenter: bodyshop.md_responsibility_centers.taxes[`tax_ty1`]
},
tax_ty2: {
center: bodyshop.md_responsibility_centers.taxes[`tax_ty2`].name,
sale: Dinero(job.job_totals.totals.us_sales_tax_breakdown[`ty2Tax`]),
cost: Dinero(),
profitCenter: bodyshop.md_responsibility_centers.taxes[`tax_ty2`],
costCenter: bodyshop.md_responsibility_centers.taxes[`tax_ty2`]
},
tax_ty3: {
center: bodyshop.md_responsibility_centers.taxes[`tax_ty3`].name,
sale: Dinero(job.job_totals.totals.us_sales_tax_breakdown[`ty3Tax`]),
cost: Dinero(),
profitCenter: bodyshop.md_responsibility_centers.taxes[`tax_ty3`],
costCenter: bodyshop.md_responsibility_centers.taxes[`tax_ty3`]
},
tax_ty4: {
center: bodyshop.md_responsibility_centers.taxes[`tax_ty4`].name,
sale: Dinero(job.job_totals.totals.us_sales_tax_breakdown[`ty4Tax`]),
cost: Dinero(),
profitCenter: bodyshop.md_responsibility_centers.taxes[`tax_ty4`],
costCenter: bodyshop.md_responsibility_centers.taxes[`tax_ty4`]
},
tax_ty5: {
center: bodyshop.md_responsibility_centers.taxes[`tax_ty5`].name,
sale: Dinero(job.job_totals.totals.us_sales_tax_breakdown[`ty5Tax`]),
cost: Dinero(),
profitCenter: bodyshop.md_responsibility_centers.taxes[`tax_ty5`],
costCenter: bodyshop.md_responsibility_centers.taxes[`tax_ty5`]
}
})
});
//Determine if there are MAPA and MASH lines already on the estimate.
//If there are, don't do anything extra (mitchell estimate)
//Otherwise, calculate them and add them to the default MAPA and MASH centers.
let hasMapaLine = false;
let hasMashLine = false;
const profitCenterHash = job.joblines.reduce((acc, val) => {
//Check the Parts Assignment
if (val.db_ref === "936008") {
//If either of these DB REFs change, they also need to change in job-totals/job-costing calculations.
hasMapaLine = true;
}
if (val.db_ref === "936007") {
hasMashLine = true;
}
if (val.profitcenter_part) {
if (!acc[val.profitcenter_part]) acc[val.profitcenter_part] = Dinero();
let DineroAmount = Dinero({
amount: Math.round(val.act_price * 100)
}).multiply(val.part_qty || 1);
DineroAmount = DineroAmount.add(
((val.prt_dsmk_m && val.prt_dsmk_m !== 0) || (val.prt_dsmk_p && val.prt_dsmk_p !== 0)) &&
DiscountNotAlreadyCounted(val, job.joblines)
? val.prt_dsmk_m
? Dinero({ amount: Math.round(val.prt_dsmk_m * 100) })
: Dinero({
amount: Math.round(val.act_price * 100)
})
.multiply(val.part_qty || 0)
.percentage(Math.abs(val.prt_dsmk_p || 0))
.multiply(val.prt_dsmk_p > 0 ? 1 : -1)
: Dinero()
);
acc[val.profitcenter_part] = acc[val.profitcenter_part].add(DineroAmount);
}
if (val.profitcenter_labor && val.mod_lbr_ty) {
//Check the Labor Assignment.
if (!acc[val.profitcenter_labor]) acc[val.profitcenter_labor] = Dinero();
acc[val.profitcenter_labor] = acc[val.profitcenter_labor].add(
Dinero({
amount: Math.round(job[`rate_${val.mod_lbr_ty.toLowerCase()}`] * 100)
}).multiply(val.mod_lb_hrs)
);
}
return acc;
}, {});
const selectedDmsAllocationConfig = bodyshop.md_responsibility_centers.dms_defaults.find(
(d) => d.name === job.dms_allocation
);
CdkBase.createLogEvent(
connectionData,
"DEBUG",
`Using DMS Allocation ${selectedDmsAllocationConfig && selectedDmsAllocationConfig.name} for cost export.`
);
let costCenterHash = {};
//Check whether to skip this if PBS and using AP module.
const disablebillwip = !!bodyshop?.pbs_configuration?.disablebillwip;
if (!disablebillwip) {
costCenterHash = job.bills.reduce((bill_acc, bill_val) => {
bill_val.billlines.map((line_val) => {
if (!bill_acc[selectedDmsAllocationConfig.costs[line_val.cost_center]])
bill_acc[selectedDmsAllocationConfig.costs[line_val.cost_center]] = Dinero();
let lineDinero = Dinero({
amount: Math.round((line_val.actual_cost || 0) * 100)
})
.multiply(line_val.quantity)
.multiply(bill_val.is_credit_memo ? -1 : 1);
bill_acc[selectedDmsAllocationConfig.costs[line_val.cost_center]] =
bill_acc[selectedDmsAllocationConfig.costs[line_val.cost_center]].add(lineDinero);
return null;
});
return bill_acc;
}, {});
}
job.timetickets.forEach((ticket) => {
//Get the total amount of the ticket.
let TicketTotal = Dinero({
amount: Math.round(
ticket.rate *
(ticket.employee && ticket.employee.flat_rate ? ticket.productivehrs || 0 : ticket.actualhrs || 0) *
100
)
});
//Add it to the right cost center.
if (!costCenterHash[selectedDmsAllocationConfig.costs[ticket.ciecacode]])
costCenterHash[selectedDmsAllocationConfig.costs[ticket.ciecacode]] = Dinero();
costCenterHash[selectedDmsAllocationConfig.costs[ticket.ciecacode]] =
costCenterHash[selectedDmsAllocationConfig.costs[ticket.ciecacode]].add(TicketTotal);
});
if (!hasMapaLine && job.job_totals.rates.mapa.total.amount > 0) {
// console.log("Adding MAPA Line Manually.");
const mapaAccountName = selectedDmsAllocationConfig.profits.MAPA;
const mapaAccount = bodyshop.md_responsibility_centers.profits.find((c) => c.name === mapaAccountName);
if (mapaAccount) {
if (!profitCenterHash[mapaAccountName]) profitCenterHash[mapaAccountName] = Dinero();
profitCenterHash[mapaAccountName] = profitCenterHash[mapaAccountName].add(
Dinero(job.job_totals.rates.mapa.total)
);
} else {
//console.log("NO MAPA ACCOUNT FOUND!!");
}
}
if (!hasMashLine && job.job_totals.rates.mash.total.amount > 0) {
// console.log("Adding MASH Line Manually.");
const mashAccountName = selectedDmsAllocationConfig.profits.MASH;
const mashAccount = bodyshop.md_responsibility_centers.profits.find((c) => c.name === mashAccountName);
if (mashAccount) {
if (!profitCenterHash[mashAccountName]) profitCenterHash[mashAccountName] = Dinero();
profitCenterHash[mashAccountName] = profitCenterHash[mashAccountName].add(
Dinero(job.job_totals.rates.mash.total)
);
} else {
// console.log("NO MASH ACCOUNT FOUND!!");
}
}
// console.log(
// Number.isInteger(bodyshop?.cdk_configuration?.sendmaterialscosting),
// typeof Number.isInteger(bodyshop?.cdk_configuration?.sendmaterialscosting)
// );
if (!!bodyshop?.cdk_configuration?.sendmaterialscosting) {
//Manually send the percentage of the costing.
//Paint Mat
const mapaAccountName = selectedDmsAllocationConfig.costs.MAPA;
const mapaAccount = bodyshop.md_responsibility_centers.costs.find((c) => c.name === mapaAccountName);
if (mapaAccount) {
if (!costCenterHash[mapaAccountName]) costCenterHash[mapaAccountName] = Dinero();
if (job.bodyshop.use_paint_scale_data === true) {
if (job.mixdata.length > 0) {
costCenterHash[mapaAccountName] = costCenterHash[mapaAccountName].add(
Dinero({
amount: Math.round(((job.mixdata[0] && job.mixdata[0].totalliquidcost) || 0) * 100)
})
);
} else {
costCenterHash[mapaAccountName] = costCenterHash[mapaAccountName].add(
Dinero(job.job_totals.rates.mapa.total).percentage(bodyshop?.cdk_configuration?.sendmaterialscosting)
);
}
} else {
costCenterHash[mapaAccountName] = costCenterHash[mapaAccountName].add(
Dinero(job.job_totals.rates.mapa.total).percentage(bodyshop?.cdk_configuration?.sendmaterialscosting)
);
}
} else {
//console.log("NO MAPA ACCOUNT FOUND!!");
}
//Shop Mat
const mashAccountName = selectedDmsAllocationConfig.costs.MASH;
const mashAccount = bodyshop.md_responsibility_centers.costs.find((c) => c.name === mashAccountName);
if (mashAccount) {
if (!costCenterHash[mashAccountName]) costCenterHash[mashAccountName] = Dinero();
costCenterHash[mashAccountName] = costCenterHash[mashAccountName].add(
Dinero(job.job_totals.rates.mash.total).percentage(bodyshop?.cdk_configuration?.sendmaterialscosting)
);
} else {
// console.log("NO MASH ACCOUNT FOUND!!");
}
}
const { ca_bc_pvrt } = job;
if (ca_bc_pvrt) {
// const pvrtAccount = bodyshop.md_responsibility_centers.profits.find(
// (c) => c.name === mashAccountName
// );
taxAllocations.state.sale = taxAllocations.state.sale.add(Dinero({ amount: Math.round((ca_bc_pvrt || 0) * 100) }));
}
if (job.towing_payable && job.towing_payable !== 0) {
const towAccountName = selectedDmsAllocationConfig.profits.TOW;
const towAccount = bodyshop.md_responsibility_centers.profits.find((c) => c.name === towAccountName);
if (towAccount) {
if (!profitCenterHash[towAccountName]) profitCenterHash[towAccountName] = Dinero();
profitCenterHash[towAccountName] = profitCenterHash[towAccountName].add(
Dinero({
amount: Math.round((job.towing_payable || 0) * 100)
})
);
} else {
// console.log("NO MASH ACCOUNT FOUND!!");
}
}
if (job.storage_payable && job.storage_payable !== 0) {
const storageAccountName = selectedDmsAllocationConfig.profits.TOW;
const towAccount = bodyshop.md_responsibility_centers.profits.find((c) => c.name === storageAccountName);
if (towAccount) {
if (!profitCenterHash[storageAccountName]) profitCenterHash[storageAccountName] = Dinero();
profitCenterHash[storageAccountName] = profitCenterHash[storageAccountName].add(
Dinero({
amount: Math.round((job.storage_payable || 0) * 100)
})
);
} else {
// console.log("NO MASH ACCOUNT FOUND!!");
}
}
if (job.adjustment_bottom_line && job.adjustment_bottom_line !== 0) {
const otherAccountName = selectedDmsAllocationConfig.profits.PAO;
const otherAccount = bodyshop.md_responsibility_centers.profits.find((c) => c.name === otherAccountName);
if (otherAccount) {
if (!profitCenterHash[otherAccountName]) profitCenterHash[otherAccountName] = Dinero();
profitCenterHash[otherAccountName] = profitCenterHash[otherAccountName].add(
Dinero({
amount: Math.round((job.adjustment_bottom_line || 0) * 100)
})
);
} else {
// console.log("NO MASH ACCOUNT FOUND!!");
}
}
if (InstanceManager({ rome: true })) {
//profile level adjustments
Object.keys(job.job_totals.parts.adjustments).forEach((key) => {
const accountName = selectedDmsAllocationConfig.profits[key];
const otherAccount = bodyshop.md_responsibility_centers.profits.find((c) => c.name === accountName);
if (otherAccount) {
if (!profitCenterHash[accountName]) profitCenterHash[accountName] = Dinero();
profitCenterHash[accountName] = profitCenterHash[accountName].add(
Dinero(job.job_totals.parts.adjustments[key])
);
} else {
CdkBase.createLogEvent(
connectionData,
"ERROR",
`Error encountered in CdkCalculateAllocations. Unable to find adjustment account. ${error}`
);
}
});
}
const jobAllocations = _.union(Object.keys(profitCenterHash), Object.keys(costCenterHash)).map((key) => {
const profitCenter = bodyshop.md_responsibility_centers.profits.find((c) => c.name === key);
const costCenter = bodyshop.md_responsibility_centers.costs.find((c) => c.name === key);
return {
center: key,
sale: profitCenterHash[key] ? profitCenterHash[key] : Dinero(),
cost: costCenterHash[key] ? costCenterHash[key] : Dinero(),
profitCenter,
costCenter
};
});
return [
...jobAllocations,
...Object.keys(taxAllocations)
.filter((key) => taxAllocations[key].sale.getAmount() > 0 || taxAllocations[key].cost.getAmount() > 0)
.map((key) => {
if (
key === "federal" &&
selectedDmsAllocationConfig.gst_override &&
selectedDmsAllocationConfig.gst_override !== ""
) {
const ret = { ...taxAllocations[key], tax: key };
ret.costCenter.dms_acctnumber = selectedDmsAllocationConfig.gst_override;
ret.profitCenter.dms_acctnumber = selectedDmsAllocationConfig.gst_override;
return ret;
} else {
return { ...taxAllocations[key], tax: key };
}
})
];
}

View File

@@ -211,6 +211,8 @@ const CreateRepairOrderTag = (job, errorCallback) => {
}
const repairCosts = CreateCosts(job);
const jobline = CreateJobLines(job.joblines);
const timeticket = CreateTimeTickets(job.timetickets);
try {
const ret = {
@@ -242,6 +244,7 @@ const CreateRepairOrderTag = (job, errorCallback) => {
},
InsuranceCompany: job.ins_co_nm || "",
Claim: job.clm_no || "",
DMSAllocation: job.dms_allocation || "",
Contacts: {
CSR: job.employee_csr_rel
? `${
@@ -276,8 +279,11 @@ const CreateRepairOrderTag = (job, errorCallback) => {
DateInvoiced:
(job.date_invoiced && moment(job.date_invoiced).tz(job.bodyshop.timezone).format(DateFormat)) || "",
DateExported:
(job.date_exported && moment(job.date_exported).tz(job.bodyshop.timezone).format(DateFormat)) || ""
(job.date_exported && moment(job.date_exported).tz(job.bodyshop.timezone).format(DateFormat)) || "",
DateVoid: (job.date_void && moment(job.date_void).tz(job.bodyshop.timezone).format(DateFormat)) || ""
},
JobLineDetails: { jobline },
TimeTicketDetails: { timeticket },
Sales: {
Labour: {
Aluminum: Dinero(job.job_totals.rates.laa.total).toFormat(DineroFormat),
@@ -625,3 +631,42 @@ const CreateCosts = (job) => {
}, 0)
};
};
const CreateJobLines = (joblines) => {
const repairLines = [];
joblines.forEach((jobline) => {
repairLines.push({
line_description: jobline.line_desc,
oem_part_no: jobline.oem_partno,
alt_part_no: jobline.alt_partno,
op_code_desc: jobline.op_code_desc,
part_type: jobline.part_type,
part_qty: jobline.part_qty,
part_price: jobline.act_price,
labor_type: jobline.mod_lbr_ty,
labor_hours: jobline.mod_lb_hrs,
labor_sale: jobline.lbr_amt
});
});
return repairLines;
};
const CreateTimeTickets = (timetickets) => {
const timeTickets = [];
timetickets.forEach((ticket) => {
timeTickets.push({
date: ticket.date,
employee: ticket.employee.employee_number
.trim()
.concat(" - ", ticket.employee.first_name.trim(), " ", ticket.employee.last_name.trim())
.trim(),
productive_hrs: ticket.productivehrs,
actual_hrs: ticket.actualhrs,
cost_center: ticket.cost_center,
flat_rate: ticket.flat_rate,
rate: ticket.rate,
ticket_cost: ticket.flat_rate ? ticket.rate * ticket.productive_hrs : ticket.rate * ticket.actual_hrs
});
});
return timeTickets;
};

View File

@@ -206,8 +206,13 @@ query QUERY_JOBS_FOR_RECEIVABLES_EXPORT($ids: [uuid!]!) {
adjustment_bottom_line
state_tax_rate
qb_multiple_payers
parts_tax_rates
tax_paint_mat_rt
tax_lbr_rt
tax_shop_mat_rt
tax_sub_rt
tax_tow_rt
tax_str_rt
owner {
accountingid
}
@@ -1108,7 +1113,7 @@ exports.KAIZEN_QUERY = `query KAIZEN_EXPORT($start: timestamptz, $bodyshopid: uu
use_paint_scale_data
timezone
}
jobs(where: {_and: [{updated_at: {_gt: $start}}, {updated_at: {_lte: $end}}, {shopid: {_eq: $bodyshopid}}]}) {
jobs(where: {_and: [{updated_at: {_gt: $start}}, {updated_at: {_lte: $end}}, {converted: {_eq: true}}, {shopid: {_eq: $bodyshopid}}]}) {
actual_completion
actual_delivery
actual_in
@@ -1133,6 +1138,8 @@ exports.KAIZEN_QUERY = `query KAIZEN_EXPORT($start: timestamptz, $bodyshopid: uu
date_invoiced
date_open
date_repairstarted
date_void
dms_allocation
employee_body_rel {
first_name
last_name
@@ -1163,6 +1170,7 @@ exports.KAIZEN_QUERY = `query KAIZEN_EXPORT($start: timestamptz, $bodyshopid: uu
ins_co_nm
joblines(where: {removed: {_eq: false}}) {
act_price
alt_partno
billlines(order_by: {bill: {date: desc_nulls_last}} limit: 1) {
actual_cost
actual_price
@@ -1177,12 +1185,15 @@ exports.KAIZEN_QUERY = `query KAIZEN_EXPORT($start: timestamptz, $bodyshopid: uu
}
db_price
id
lbr_amt
lbr_op
line_desc
line_ind
line_no
mod_lb_hrs
mod_lbr_ty
oem_partno
op_code_desc
parts_order_lines(order_by: {parts_order: {order_date: desc_nulls_last}} limit: 1){
parts_order{
id
@@ -1195,7 +1206,6 @@ exports.KAIZEN_QUERY = `query KAIZEN_EXPORT($start: timestamptz, $bodyshopid: uu
profitcenter_labor
prt_dsmk_m
prt_dsmk_p
oem_partno
status
}
job_totals
@@ -1246,12 +1256,18 @@ exports.KAIZEN_QUERY = `query KAIZEN_EXPORT($start: timestamptz, $bodyshopid: uu
scheduled_in
status
timetickets {
id
rate
cost_center
actualhrs
productivehrs
cost_center
date
employee {
employee_number
first_name
last_name
}
flat_rate
id
productivehrs
rate
}
tlos_ind
v_color
@@ -1534,6 +1550,7 @@ exports.QUERY_JOB_COSTING_DETAILS = ` query QUERY_JOB_COSTING_DETAILS($id: uuid!
op_code_desc
profitcenter_part
profitcenter_labor
act_price_before_ppc
}
bills {
id
@@ -2475,7 +2492,6 @@ query QUERY_TASK_BY_ID($id: uuid!) {
`;
exports.GET_JOBS_BY_PKS = `query GET_JOBS_BY_PKS($ids: [uuid!]!) {
jobs(where: {id: {_in: $ids}}) {
id

View File

@@ -269,7 +269,7 @@ function GenerateCostingData(job) {
job &&
job.joblines.reduce(
(acc, val) => {
//Parts Lines
//Shop or Paint Material Flags
if (val.db_ref === "936008") {
//If either of these DB REFs change, they also need to change in job-totals/job-costing calculations.
hasMapaLine = true;
@@ -277,6 +277,8 @@ function GenerateCostingData(job) {
if (val.db_ref === "936007") {
hasMashLine = true;
}
//Labor Profit Center
if (val.mod_lbr_ty) {
const laborProfitCenter = val.profitcenter_labor || defaultProfits[val.mod_lbr_ty] || "Unknown";
@@ -307,6 +309,7 @@ function GenerateCostingData(job) {
}
}
// Part Profit Center
if (val.part_type && val.part_type !== "PAE" && val.part_type !== "PAS" && val.part_type !== "PASL") {
const partsProfitCenter = val.profitcenter_part || defaultProfits[val.part_type] || "Unknown";
@@ -315,7 +318,9 @@ function GenerateCostingData(job) {
if (!partsProfitCenter)
console.log("Unknown cost/profit center mapping for parts.", val.line_desc, val.part_type);
const partsAmount = Dinero({
amount: Math.round((val.act_price || 0) * 100)
amount: val.act_price_before_ppc
? Math.round(val.act_price_before_ppc * 100)
: Math.round(val.act_price * 100)
})
.multiply(val.part_qty || 1)
.add(
@@ -324,7 +329,9 @@ function GenerateCostingData(job) {
? val.prt_dsmk_m
? Dinero({ amount: Math.round(val.prt_dsmk_m * 100) })
: Dinero({
amount: Math.round(val.act_price * 100)
amount: val.act_price_before_ppc
? Math.round(val.act_price_before_ppc * 100)
: Math.round(val.act_price * 100)
})
.multiply(val.part_qty || 0)
.percentage(Math.abs(val.prt_dsmk_p || 0))
@@ -334,6 +341,8 @@ function GenerateCostingData(job) {
if (!acc.parts[partsProfitCenter]) acc.parts[partsProfitCenter] = Dinero();
acc.parts[partsProfitCenter] = acc.parts[partsProfitCenter].add(partsAmount);
}
//Sublet Profit Center
if (val.part_type && val.part_type !== "PAE" && (val.part_type === "PAS" || val.part_type === "PASL")) {
const partsProfitCenter = val.profitcenter_part || defaultProfits[val.part_type] || "Unknown";
@@ -362,8 +371,11 @@ function GenerateCostingData(job) {
acc.sublet[partsProfitCenter] = acc.sublet[partsProfitCenter].add(partsAmount);
}
//To deal with additional costs.
if (!val.part_type && !val.mod_lbr_ty) {
//Additional Profit Center
if (
(!val.part_type && !val.mod_lbr_ty) ||
(!val.part_type && val.mod_lbr_ty && val.act_price > 0 && val.lbr_op !== "OP14")
) {
//Does it already have a defined profit center?
//If so, use it, otherwise try to use the same from the auto-allocate logic in IO app jobs-close-auto-allocate.
const partsProfitCenter = val.profitcenter_part || getAdditionalCostCenter(val, defaultProfits) || "Unknown";

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