diff --git a/package-lock.json b/package-lock.json index c9c999974..b9ed707fb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,7 @@ "": { "name": "@gusto/embedded-react-sdk", "version": "0.24.1", + "hasInstallScript": true, "license": "MIT", "dependencies": { "@gusto/embedded-api": "^0.11.4", @@ -58,6 +59,7 @@ "lint-staged": "^16.2.7", "msw": "^2.12.7", "npm-run-all": "^4.1.5", + "patch-package": "^8.0.1", "prettier": "^3.7.4", "react-dnd": "^16.0.1", "react-dnd-html5-backend": "^16.0.1", @@ -154,7 +156,6 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -458,6 +459,7 @@ "integrity": "sha512-7e8SScMocHxcAb8YhtkbMhGG+EKLRIficb1F5sjvhSYsWTZGxvg4KIDp8kgxnV2PUJ3ddPe6J9QESjKvBWRDkg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@cacheable/utils": "^2.3.2", "@keyv/bigmap": "^1.3.0", @@ -471,6 +473,7 @@ "integrity": "sha512-KT01GjzV6AQD5+IYrcpoYLkCu1Jod3nau1Z7EsEuViO3TZGRacSbO9MfHmbJ1WaOXFtWLxPVj169cn2WNKPkIg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "hashery": "^1.2.0", "hookified": "^1.13.0" @@ -499,6 +502,7 @@ "integrity": "sha512-8kGE2P+HjfY8FglaOiW+y8qxcaQAfAhVML+i66XJR3YX5FtyDqn6Txctr3K2FrbxLKixRRYYBWMbuGciOhYNDg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "hashery": "^1.2.0", "keyv": "^5.5.4" @@ -510,6 +514,7 @@ "integrity": "sha512-FA5LmZVF1VziNc0bIdCSA1IoSVnDCqE8HJIZZv2/W8YmoAM50+tnUgJR/gQZwEeIMleuIOnRnHA/UaZRNeV4iQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@keyv/serialize": "^1.1.1" } @@ -859,7 +864,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -883,6 +887,7 @@ } ], "license": "MIT-0", + "peer": true, "engines": { "node": ">=18" } @@ -903,7 +908,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -924,6 +928,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -948,6 +953,7 @@ } ], "license": "MIT-0", + "peer": true, "engines": { "node": ">=18" }, @@ -961,6 +967,7 @@ "integrity": "sha512-id+7YRUgoUX6CgV0DtuhirQWodeeA7Lf4i2x71JS/vtA5pRb/hIGWlw+G6MeXvsM+MXrz0VAydTGElX1rAfgPg==", "dev": true, "license": "MIT", + "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/JounQin" @@ -2247,7 +2254,8 @@ "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz", "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@ladle/react": { "version": "5.1.1", @@ -5820,6 +5828,7 @@ "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.12.tgz", "integrity": "sha512-T1/8t5DhV/SisWjDnaiU2drl6ySvsHj1bHBCWNXd+/T+Hh1cf6JodyEYMd5sgwm+b/mETT4EV3H+zCVczCU5hg==", "license": "MIT", + "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" @@ -5972,7 +5981,8 @@ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -6229,7 +6239,6 @@ "integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -6247,7 +6256,6 @@ "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -6342,7 +6350,6 @@ "integrity": "sha512-3xP4XzzDNQOIqBMWogftkwxhg5oMKApqY0BAflmLZiFYHqyhSOxv/cd/zPQLTcCXr4AkaKb25joocY0BD1WC6A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.51.0", "@typescript-eslint/types": "8.51.0", @@ -6888,6 +6895,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@yarnpkg/lockfile": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", + "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", + "dev": true, + "license": "BSD-2-Clause" + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -6908,7 +6922,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -7157,6 +7170,7 @@ "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=8" } @@ -7323,6 +7337,7 @@ "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=8" } @@ -7503,7 +7518,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -7571,6 +7585,7 @@ "integrity": "sha512-yr+FSHWn1ZUou5LkULX/S+jhfgfnLbuKQjE40tyEd4fxGZVMbBL5ifno0J0OauykS8UiCSgHi+DV/YD+rjFxFg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@cacheable/memory": "^2.0.6", "@cacheable/utils": "^2.3.2", @@ -7585,6 +7600,7 @@ "integrity": "sha512-FA5LmZVF1VziNc0bIdCSA1IoSVnDCqE8HJIZZv2/W8YmoAM50+tnUgJR/gQZwEeIMleuIOnRnHA/UaZRNeV4iQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@keyv/serialize": "^1.1.1" } @@ -8069,7 +8085,8 @@ "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/colorette": { "version": "2.0.20", @@ -8260,7 +8277,6 @@ "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", @@ -8321,6 +8337,7 @@ "integrity": "sha512-IQOkD3hbR5KrN93MtcYuad6YPuTSUhntLHDuLEbFWE+ff2/XSZNdZG+LcbbIW5AXKg/WFIfYItIzVoHngHXZzA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12 || >=16" } @@ -8355,6 +8372,7 @@ "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "mdn-data": "2.12.2", "source-map-js": "^1.0.1" @@ -8376,6 +8394,7 @@ "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "cssesc": "bin/cssesc" }, @@ -8751,6 +8770,7 @@ "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "path-type": "^4.0.0" }, @@ -8764,6 +8784,7 @@ "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=8" } @@ -8812,7 +8833,8 @@ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/dompurify": { "version": "3.3.1", @@ -9255,7 +9277,6 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -10024,6 +10045,7 @@ "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 4.9.1" } @@ -10113,6 +10135,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/find-yarn-workspace-root": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz", + "integrity": "sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "micromatch": "^4.0.2" + } + }, "node_modules/flat-cache": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", @@ -10487,6 +10519,7 @@ "integrity": "sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "global-prefix": "^3.0.0" }, @@ -10500,6 +10533,7 @@ "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ini": "^1.3.5", "kind-of": "^6.0.2", @@ -10514,7 +10548,8 @@ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "dev": true, - "license": "ISC" + "license": "ISC", + "peer": true }, "node_modules/global-prefix/node_modules/which": { "version": "1.3.1", @@ -10522,6 +10557,7 @@ "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", "dev": true, "license": "ISC", + "peer": true, "dependencies": { "isexe": "^2.0.0" }, @@ -10608,7 +10644,8 @@ "resolved": "https://registry.npmjs.org/globjoin/-/globjoin-0.1.4.tgz", "integrity": "sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/globrex": { "version": "0.1.2", @@ -10734,6 +10771,7 @@ "integrity": "sha512-fWltioiy5zsSAs9ouEnvhsVJeAXRybGCNNv0lvzpzNOSDbULXRy7ivFWwCCv4I5Am6kSo75hmbsCduOoc2/K4w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "hookified": "^1.13.0" }, @@ -11044,7 +11082,8 @@ "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.14.0.tgz", "integrity": "sha512-pi1ynXIMFx/uIIwpWJ/5CEtOHLGtnUB0WhGeeYT+fKcQ+WCQbm3/rrkAXnpfph++PgepNqPdTC2WTj8A6k6zoQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/hosted-git-info": { "version": "2.8.9", @@ -11088,6 +11127,7 @@ "integrity": "sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=8" }, @@ -11210,7 +11250,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.28.4" }, @@ -11761,6 +11800,7 @@ "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -12690,7 +12730,6 @@ "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "cssstyle": "^4.2.1", "data-urls": "^5.0.0", @@ -12780,6 +12819,26 @@ "dev": true, "license": "MIT" }, + "node_modules/json-stable-stringify": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.3.0.tgz", + "integrity": "sha512-qtYiSSFlwot9XHtF9bD9c7rwKjr+RecWT//ZnPvSmEjpV5mmPOCN4j8UjY5hbjNkOwZ/jQv3J6R1/pL7RwgMsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "isarray": "^2.0.5", + "jsonify": "^0.0.1", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", @@ -12813,6 +12872,16 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jsonify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.1.tgz", + "integrity": "sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==", + "dev": true, + "license": "Public Domain", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/jsonparse": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", @@ -12886,16 +12955,28 @@ "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } }, + "node_modules/klaw-sync": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/klaw-sync/-/klaw-sync-6.0.0.tgz", + "integrity": "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.11" + } + }, "node_modules/known-css-properties": { "version": "0.37.0", "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.37.0.tgz", "integrity": "sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/koa": { "version": "2.16.3", @@ -13179,7 +13260,8 @@ "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/lodash.uniq": { "version": "4.5.0", @@ -13272,6 +13354,7 @@ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -13354,6 +13437,7 @@ "integrity": "sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==", "dev": true, "license": "MIT", + "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -13680,7 +13764,8 @@ "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", "dev": true, - "license": "CC0-1.0" + "license": "CC0-1.0", + "peer": true }, "node_modules/media-typer": { "version": "0.3.0", @@ -14854,6 +14939,7 @@ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -15422,6 +15508,176 @@ "node": ">= 0.8" } }, + "node_modules/patch-package": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/patch-package/-/patch-package-8.0.1.tgz", + "integrity": "sha512-VsKRIA8f5uqHQ7NGhwIna6Bx6D9s/1iXlA1hthBVBEbkq+t4kXD0HHt+rJhf/Z+Ci0F/HCB2hvn0qLdLG+Qxlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@yarnpkg/lockfile": "^1.1.0", + "chalk": "^4.1.2", + "ci-info": "^3.7.0", + "cross-spawn": "^7.0.3", + "find-yarn-workspace-root": "^2.0.0", + "fs-extra": "^10.0.0", + "json-stable-stringify": "^1.0.2", + "klaw-sync": "^6.0.0", + "minimist": "^1.2.6", + "open": "^7.4.2", + "semver": "^7.5.3", + "slash": "^2.0.0", + "tmp": "^0.2.4", + "yaml": "^2.2.2" + }, + "bin": { + "patch-package": "index.js" + }, + "engines": { + "node": ">=14", + "npm": ">5" + } + }, + "node_modules/patch-package/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/patch-package/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/patch-package/node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/patch-package/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/patch-package/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/patch-package/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/patch-package/node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/patch-package/node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/patch-package/node_modules/open": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", + "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0", + "is-wsl": "^2.1.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/patch-package/node_modules/slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/path-browserify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", @@ -15602,7 +15858,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -15617,7 +15872,8 @@ "resolved": "https://registry.npmjs.org/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.6.tgz", "integrity": "sha512-0sglIs9Wmkzbr8lQwEyIzlDOOC9bGmfVKcJTaxv3vMmd3uo4o4DerC3En0bnmgceeql9BfC8hRkp7cg0fjdVqw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/postcss-safe-parser": { "version": "7.0.1", @@ -15639,6 +15895,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18.0" }, @@ -15666,7 +15923,8 @@ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/prelude-ls": { "version": "1.2.1", @@ -15700,6 +15958,7 @@ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -15715,6 +15974,7 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10" }, @@ -15782,6 +16042,7 @@ "integrity": "sha512-kXuQdQTB6oN3KhI6V4acnBSZx8D2I4xzZvn9+wFLLFCoBNQY/sFnCW6c43OL7pOQ2HvGV4lnWIXNmgfp7cTWhQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "hookified": "^1.13.0" }, @@ -16036,7 +16297,6 @@ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.69.0.tgz", "integrity": "sha512-yt6ZGME9f4F6WHwevrvpAjh42HMvocuSnSIHUGycBqXIJdhqGSPQzTpGF+1NLREk/58IdPxEMfPcFCjlMhclGw==", "license": "MIT", - "peer": true, "engines": { "node": ">=18.0.0" }, @@ -16091,7 +16351,8 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/react-refresh": { "version": "0.17.0", @@ -16580,8 +16841,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/robot3/-/robot3-1.2.0.tgz", "integrity": "sha512-Xin8KHqCKrD9Rqk1ZzZQYjsb6S9DRggcfwBqnVPeM3DLtNCJLxWWTrPJDYm3E+ZiTO7H3VMdgyPSkIbuYnYP2Q==", - "license": "BSD-2-Clause", - "peer": true + "license": "BSD-2-Clause" }, "node_modules/rollup": { "version": "4.53.5", @@ -16589,7 +16849,6 @@ "integrity": "sha512-iTNAbFSlRpcHeeWu73ywU/8KuU/LZmNCSxp6fjQkJBD3ivUb8tpDrXhIxEzA05HlYMEwmtaUnb3RP+YNv162OQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -17167,7 +17426,8 @@ "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/semver": { "version": "7.7.3", @@ -18006,14 +18266,16 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-2.0.0.tgz", "integrity": "sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/stylelint/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/stylelint/node_modules/file-entry-cache": { "version": "11.1.1", @@ -18021,6 +18283,7 @@ "integrity": "sha512-TPVFSDE7q91Dlk1xpFLvFllf8r0HyOMOlnWy7Z2HBku5H3KhIeOGInexrIeg2D64DosVB/JXkrrk6N/7Wriq4A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "flat-cache": "^6.1.19" } @@ -18031,6 +18294,7 @@ "integrity": "sha512-l/K33newPTZMTGAnnzaiqSl6NnH7Namh8jBNjrgjprWxGmZUuxx/sJNIRaijOh3n7q7ESbhNZC+pvVZMFdeU4A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "cacheable": "^2.2.0", "flatted": "^3.3.3", @@ -18043,6 +18307,7 @@ "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", @@ -18064,6 +18329,7 @@ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 4" } @@ -18074,6 +18340,7 @@ "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 4" } @@ -18084,6 +18351,7 @@ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=8" } @@ -18094,6 +18362,7 @@ "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=8" } @@ -18104,6 +18373,7 @@ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -18119,6 +18389,7 @@ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -18145,6 +18416,7 @@ "integrity": "sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "has-flag": "^4.0.0", "supports-color": "^7.0.0" @@ -18180,7 +18452,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/svg-tags/-/svg-tags-1.0.0.tgz", "integrity": "sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==", - "dev": true + "dev": true, + "peer": true }, "node_modules/symbol-tree": { "version": "3.2.4", @@ -18218,6 +18491,7 @@ "integrity": "sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==", "dev": true, "license": "BSD-3-Clause", + "peer": true, "dependencies": { "ajv": "^8.0.1", "lodash.truncate": "^4.4.2", @@ -18235,6 +18509,7 @@ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -18251,6 +18526,7 @@ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "color-name": "~1.1.4" }, @@ -18263,14 +18539,16 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/table/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/table/node_modules/is-fullwidth-code-point": { "version": "3.0.0", @@ -18278,6 +18556,7 @@ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=8" } @@ -18288,6 +18567,7 @@ "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ansi-styles": "^4.0.0", "astral-regex": "^2.0.0", @@ -18306,6 +18586,7 @@ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -18321,6 +18602,7 @@ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -18493,6 +18775,16 @@ "dev": true, "license": "MIT" }, + "node_modules/tmp": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -18565,9 +18857,9 @@ } }, "node_modules/ts-api-utils": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.3.0.tgz", - "integrity": "sha512-6eg3Y9SF7SsAvGzRHQvvc1skDAhwI4YQ32ui1scxD1Ccr0G5qIIbUBT3pFTKX8kmWIQClHobtUdNuaBgwdfdWg==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", "dev": true, "license": "MIT", "engines": { @@ -18657,7 +18949,6 @@ "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" @@ -19056,7 +19347,8 @@ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/validate-npm-package-license": { "version": "3.0.4", @@ -19137,7 +19429,6 @@ "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -19956,7 +20247,6 @@ "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", @@ -20403,6 +20693,7 @@ "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", "dev": true, "license": "ISC", + "peer": true, "dependencies": { "imurmurhash": "^0.1.4", "signal-exit": "^4.0.1" @@ -20614,7 +20905,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index 8b5795229..e16d09239 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "./CHANGELOG.md" ], "scripts": { + "postinstall": "patch-package", "build": "npm run build:clean && npm run i18n:generate && vite build", "build:ci": "npm run build && npm run lint:check && npm run format:check && npm run test:ci", "build:clean": "rm -rf ./dist && mkdir ./dist", @@ -42,7 +43,7 @@ "watch:vite": "vite build --watch --mode development", "watch:translations": "node ./build/translationWatcher.js", "dev": "node ./build/prompt.js && npm run i18n:generate && npm-run-all --parallel watch:vite watch:translations", - "dev:setup": "npm link ../gws-flows/node_modules/react && (cd ../gws-flows && yarn link -r ../embedded-react-sdk)", + "dev:setup": "npm link ../gws-flows/node_modules/react ../gws-flows/node_modules/react-dom && (cd ../gws-flows && yarn link -r ../embedded-react-sdk)", "docs:events": "npx tsx ./build/eventTypeDocsEmitter.ts", "docs:sync": "npx tsx .docs/src/sync/syncManager.ts", "docs": "npx tsx .docs/src/preview/previewGenerator.ts", @@ -94,6 +95,7 @@ "lint-staged": "^16.2.7", "msw": "^2.12.7", "npm-run-all": "^4.1.5", + "patch-package": "^8.0.1", "prettier": "^3.7.4", "react-dnd": "^16.0.1", "react-dnd-html5-backend": "^16.0.1", diff --git a/patches/@gusto+embedded-api+0.11.4.patch b/patches/@gusto+embedded-api+0.11.4.patch new file mode 100644 index 000000000..e95e0dbf3 --- /dev/null +++ b/patches/@gusto+embedded-api+0.11.4.patch @@ -0,0 +1,26 @@ +diff --git a/node_modules/@gusto/embedded-api/esm/models/components/payrollemployeecompensationstype.js b/node_modules/@gusto/embedded-api/esm/models/components/payrollemployeecompensationstype.js +index 5655c44..27f2bc2 100644 +--- a/node_modules/@gusto/embedded-api/esm/models/components/payrollemployeecompensationstype.js ++++ b/node_modules/@gusto/embedded-api/esm/models/components/payrollemployeecompensationstype.js +@@ -57,7 +57,7 @@ export function hourlyCompensationsFromJSON(jsonString) { + export const PayrollEmployeeCompensationsTypePaidTimeOff$inboundSchema = z.object({ + name: z.string().optional(), + hours: z.string().optional(), +- final_payout_unused_hours_input: z.string().optional(), ++ final_payout_unused_hours_input: z.nullable(z.string()).optional(), + }).transform((v) => { + return remap$(v, { + "final_payout_unused_hours_input": "finalPayoutUnusedHoursInput", +diff --git a/node_modules/@gusto/embedded-api/src/models/components/payrollemployeecompensationstype.ts b/node_modules/@gusto/embedded-api/src/models/components/payrollemployeecompensationstype.ts +index f96feb5..d1e539a 100644 +--- a/node_modules/@gusto/embedded-api/src/models/components/payrollemployeecompensationstype.ts ++++ b/node_modules/@gusto/embedded-api/src/models/components/payrollemployeecompensationstype.ts +@@ -270,7 +270,7 @@ export const PayrollEmployeeCompensationsTypePaidTimeOff$inboundSchema: + > = z.object({ + name: z.string().optional(), + hours: z.string().optional(), +- final_payout_unused_hours_input: z.string().optional(), ++ final_payout_unused_hours_input: z.nullable(z.string()).optional(), + }).transform((v) => { + return remap$(v, { + "final_payout_unused_hours_input": "finalPayoutUnusedHoursInput", diff --git a/src/components/Common/UI/DescriptionList/DescriptionList.module.scss b/src/components/Common/UI/DescriptionList/DescriptionList.module.scss index df8f59dac..8e1fb35f6 100644 --- a/src/components/Common/UI/DescriptionList/DescriptionList.module.scss +++ b/src/components/Common/UI/DescriptionList/DescriptionList.module.scss @@ -1,6 +1,16 @@ .root { width: 100%; + dt { + font-weight: var(--g-fontWeightMedium); + color: var(--g-colorBodyContent); + } + + dd { + font-weight: var(--g-fontWeightRegular); + color: var(--g-colorBodySubContent); + } + .item { &:not(:last-child) { padding-bottom: toRem(20); diff --git a/src/components/Terminations/TerminateEmployee/TerminateEmployee.test.tsx b/src/components/Terminations/TerminateEmployee/TerminateEmployee.test.tsx new file mode 100644 index 000000000..e4aeabe19 --- /dev/null +++ b/src/components/Terminations/TerminateEmployee/TerminateEmployee.test.tsx @@ -0,0 +1,220 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { screen, waitFor } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { http, HttpResponse } from 'msw' +import { TerminateEmployee } from './TerminateEmployee' +import { server } from '@/test/mocks/server' +import { componentEvents } from '@/shared/constants' +import { setupApiTestMocks } from '@/test/mocks/apiServer' +import { renderWithProviders } from '@/test-utils/renderWithProviders' +import { API_BASE_URL } from '@/test/constants' + +const mockEmployee = { + uuid: 'employee-123', + first_name: 'John', + last_name: 'Doe', + email: 'john.doe@example.com', + company_uuid: 'company-123', + terminated: false, + onboarded: true, +} + +const mockTermination = { + uuid: 'termination-123', + employee_uuid: 'employee-123', + effective_date: '2025-01-15', + run_termination_payroll: true, + active: false, + cancelable: true, +} + +const mockTerminationPayPeriods = [ + { + employee_uuid: 'employee-123', + employee_name: 'John Doe', + start_date: '2025-01-01', + end_date: '2025-01-15', + check_date: '2025-01-20', + pay_schedule_uuid: 'pay-schedule-123', + }, +] + +const mockPayrollPrepared = { + payroll_uuid: 'payroll-123', + company_uuid: 'company-123', + off_cycle: true, + off_cycle_reason: 'Dismissed employee', +} + +describe('TerminateEmployee', () => { + const onEvent = vi.fn() + const user = userEvent.setup() + const defaultProps = { + employeeId: 'employee-123', + companyId: 'company-123', + onEvent, + } + + beforeEach(() => { + setupApiTestMocks() + onEvent.mockClear() + + server.use( + http.get(`${API_BASE_URL}/v1/employees/:employee_id`, () => { + return HttpResponse.json(mockEmployee) + }), + http.post(`${API_BASE_URL}/v1/employees/:employee_id/terminations`, () => { + return HttpResponse.json(mockTermination) + }), + http.get( + `${API_BASE_URL}/v1/companies/:company_id/pay_periods/unprocessed_termination_pay_periods`, + () => { + return HttpResponse.json(mockTerminationPayPeriods) + }, + ), + http.post(`${API_BASE_URL}/v1/companies/:company_id/payrolls`, () => { + return HttpResponse.json(mockPayrollPrepared) + }), + ) + }) + + describe('rendering', () => { + it('renders the termination form with employee name', async () => { + renderWithProviders() + + await waitFor(() => { + expect(screen.getByRole('heading', { name: 'Terminate John Doe' })).toBeInTheDocument() + }) + + expect( + screen.getByText(/Set their last day of work and choose how to handle their final payroll/), + ).toBeInTheDocument() + }) + + it('renders the date picker for last day of work', async () => { + renderWithProviders() + + await waitFor(() => { + expect(screen.getByText('Last day of work')).toBeInTheDocument() + }) + }) + + it('renders all payroll options', async () => { + renderWithProviders() + + await waitFor(() => { + expect(screen.getByText('Dismissal payroll')).toBeInTheDocument() + }) + + expect(screen.getByText('Regular payroll')).toBeInTheDocument() + expect(screen.getByText('Another way')).toBeInTheDocument() + }) + + it('renders submit and cancel buttons', async () => { + renderWithProviders() + + await waitFor(() => { + expect(screen.getByRole('button', { name: 'Terminate employee' })).toBeInTheDocument() + }) + + expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument() + }) + }) + + describe('form validation', () => { + it('shows validation error when submitting without date', async () => { + renderWithProviders() + + await waitFor(() => { + expect(screen.getByRole('button', { name: 'Terminate employee' })).toBeInTheDocument() + }) + + await user.click(screen.getByRole('button', { name: 'Terminate employee' })) + + await waitFor(() => { + expect(screen.getByText('Last day of work is required')).toBeInTheDocument() + }) + }) + + it('has dismissal payroll selected by default', async () => { + renderWithProviders() + + await waitFor(() => { + expect(screen.getByLabelText('Dismissal payroll')).toBeChecked() + }) + }) + }) + + describe('cancel action', () => { + it('emits CANCEL event when cancel button is clicked', async () => { + renderWithProviders() + + await waitFor(() => { + expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument() + }) + + await user.click(screen.getByRole('button', { name: 'Cancel' })) + + expect(onEvent).toHaveBeenCalledWith(componentEvents.CANCEL) + }) + }) + + describe('payroll option selection', () => { + it('allows selecting dismissal payroll option', async () => { + renderWithProviders() + + await waitFor(() => { + expect(screen.getByText('Dismissal payroll')).toBeInTheDocument() + }) + + const dismissalRadio = screen.getByLabelText('Dismissal payroll') + await user.click(dismissalRadio) + + expect(dismissalRadio).toBeChecked() + }) + + it('allows selecting regular payroll option', async () => { + renderWithProviders() + + await waitFor(() => { + expect(screen.getByText('Regular payroll')).toBeInTheDocument() + }) + + const regularRadio = screen.getByLabelText('Regular payroll') + await user.click(regularRadio) + + expect(regularRadio).toBeChecked() + }) + + it('allows selecting another way option', async () => { + renderWithProviders() + + await waitFor(() => { + expect(screen.getByText('Another way')).toBeInTheDocument() + }) + + const anotherWayRadio = screen.getByLabelText('Another way') + await user.click(anotherWayRadio) + + expect(anotherWayRadio).toBeChecked() + }) + + it('shows option descriptions', async () => { + renderWithProviders() + + await waitFor(() => { + expect( + screen.getByText(/Runs a final payroll that automatically pays out unused PTO/), + ).toBeInTheDocument() + }) + + expect( + screen.getByText(/Same as dismissal payrolls, except there won.t be a separate record/), + ).toBeInTheDocument() + + expect( + screen.getByText(/You can run an off-cycle payroll to manually calculate/), + ).toBeInTheDocument() + }) + }) +}) diff --git a/src/components/Terminations/TerminateEmployee/TerminateEmployee.tsx b/src/components/Terminations/TerminateEmployee/TerminateEmployee.tsx new file mode 100644 index 000000000..ab87f375a --- /dev/null +++ b/src/components/Terminations/TerminateEmployee/TerminateEmployee.tsx @@ -0,0 +1,174 @@ +import { useState } from 'react' +import { useQueryClient } from '@tanstack/react-query' +import { useEmployeesGetSuspense } from '@gusto/embedded-api/react-query/employeesGet' +import { useEmployeeEmploymentsCreateTerminationMutation } from '@gusto/embedded-api/react-query/employeeEmploymentsCreateTermination' +import { usePayrollsCreateOffCycleMutation } from '@gusto/embedded-api/react-query/payrollsCreateOffCycle' +import { + usePaySchedulesGetUnprocessedTerminationPeriods, + invalidateAllPaySchedulesGetUnprocessedTerminationPeriods, +} from '@gusto/embedded-api/react-query/paySchedulesGetUnprocessedTerminationPeriods' +import { invalidateAllPayrollsList } from '@gusto/embedded-api/react-query/payrollsList' +import { OffCycleReason } from '@gusto/embedded-api/models/operations/postv1companiescompanyidpayrolls' +import { RFCDate } from '@gusto/embedded-api/types/rfcdate' +import { TerminateEmployeePresentation, type PayrollOption } from './TerminateEmployeePresentation' +import type { BaseComponentInterface } from '@/components/Base/Base' +import { BaseComponent } from '@/components/Base/Base' +import { useBase } from '@/components/Base/useBase' +import { componentEvents } from '@/shared/constants' +import { useComponentDictionary, useI18n } from '@/i18n' + +export interface TerminateEmployeeProps extends BaseComponentInterface<'Terminations.TerminateEmployee'> { + employeeId: string + companyId: string +} + +export function TerminateEmployee(props: TerminateEmployeeProps) { + return ( + + {props.children} + + ) +} + +const Root = ({ employeeId, companyId, dictionary }: TerminateEmployeeProps) => { + useComponentDictionary('Terminations.TerminateEmployee', dictionary) + useI18n('Terminations.TerminateEmployee') + + const queryClient = useQueryClient() + const { onEvent, baseSubmitHandler } = useBase() + + const [lastDayOfWork, setLastDayOfWork] = useState(null) + const [payrollOption, setPayrollOption] = useState('dismissalPayroll') + const [lastDayError, setLastDayError] = useState() + + const { + data: { employee }, + } = useEmployeesGetSuspense({ employeeId }) + + const { mutateAsync: createTermination, isPending: isCreatingTermination } = + useEmployeeEmploymentsCreateTerminationMutation() + + const { mutateAsync: createOffCyclePayroll, isPending: isCreatingPayroll } = + usePayrollsCreateOffCycleMutation() + + const { refetch: fetchTerminationPeriods } = usePaySchedulesGetUnprocessedTerminationPeriods( + { companyId }, + { enabled: false }, + ) + + const employeeName = [employee?.firstName, employee?.lastName].filter(Boolean).join(' ') + + const validateForm = (): boolean => { + if (!lastDayOfWork) { + setLastDayError('Last day of work is required') + return false + } + setLastDayError(undefined) + return true + } + + const handleSubmit = async () => { + if (!validateForm()) { + return + } + + const effectiveDate = lastDayOfWork!.toISOString().split('T')[0]! + + await baseSubmitHandler({ effectiveDate, payrollOption }, async () => { + const runTerminationPayroll = payrollOption === 'dismissalPayroll' + + const result = await createTermination({ + request: { + employeeId, + requestBody: { + effectiveDate, + runTerminationPayroll, + }, + }, + }) + + if (runTerminationPayroll) { + try { + const { data: terminationPeriodsData } = await fetchTerminationPeriods() + + const employeePeriods = + terminationPeriodsData?.unprocessedTerminationPayPeriodList?.filter( + period => period.employeeUuid === employeeId, + ) ?? [] + + const createdPayrolls = [] + + for (const terminationPeriod of employeePeriods) { + if (terminationPeriod.startDate && terminationPeriod.endDate) { + const payrollResult = await createOffCyclePayroll({ + request: { + companyId, + requestBody: { + offCycle: true, + offCycleReason: OffCycleReason.DismissedEmployee, + startDate: new RFCDate(terminationPeriod.startDate), + endDate: new RFCDate(terminationPeriod.endDate), + employeeUuids: [employeeId], + checkDate: terminationPeriod.checkDate + ? new RFCDate(terminationPeriod.checkDate) + : undefined, + }, + }, + }) + + createdPayrolls.push(payrollResult.payrollPrepared) + } + } + + if (createdPayrolls.length > 0) { + await invalidateAllPayrollsList(queryClient) + await invalidateAllPaySchedulesGetUnprocessedTerminationPeriods(queryClient) + + onEvent(componentEvents.EMPLOYEE_TERMINATION_PAYROLL_CREATED, { + payrolls: createdPayrolls, + }) + } + } catch (payrollError) { + onEvent(componentEvents.EMPLOYEE_TERMINATION_PAYROLL_FAILED, { + error: payrollError, + employeeId, + }) + } + } + + onEvent(componentEvents.EMPLOYEE_TERMINATION_CREATED, { + termination: result.termination, + payrollOption, + runTerminationPayroll, + }) + + onEvent(componentEvents.EMPLOYEE_TERMINATION_DONE, { + employeeId, + effectiveDate, + payrollOption, + termination: result.termination, + ...(payrollOption === 'anotherWay' && { manualHandling: true }), + }) + }) + } + + const handleCancel = () => { + onEvent(componentEvents.CANCEL) + } + + const isPending = isCreatingTermination || isCreatingPayroll + + return ( + + ) +} diff --git a/src/components/Terminations/TerminateEmployee/TerminateEmployeePresentation.tsx b/src/components/Terminations/TerminateEmployee/TerminateEmployeePresentation.tsx new file mode 100644 index 000000000..7a8ff7f39 --- /dev/null +++ b/src/components/Terminations/TerminateEmployee/TerminateEmployeePresentation.tsx @@ -0,0 +1,95 @@ +import { useTranslation } from 'react-i18next' +import { Flex, ActionsLayout } from '@/components/Common' +import { useComponentContext } from '@/contexts/ComponentAdapter/useComponentContext' +import { useI18n } from '@/i18n' + +export type PayrollOption = 'dismissalPayroll' | 'regularPayroll' | 'anotherWay' + +interface TerminateEmployeePresentationProps { + employeeName: string + lastDayOfWork: Date | null + onLastDayOfWorkChange: (date: Date | null) => void + payrollOption: PayrollOption + onPayrollOptionChange: (option: PayrollOption) => void + onSubmit: () => void + onCancel: () => void + isLoading: boolean + lastDayError?: string +} + +export function TerminateEmployeePresentation({ + employeeName, + lastDayOfWork, + onLastDayOfWorkChange, + payrollOption, + onPayrollOptionChange, + onSubmit, + onCancel, + isLoading, + lastDayError, +}: TerminateEmployeePresentationProps) { + const { Alert, Heading, Text, DatePicker, RadioGroup, Button } = useComponentContext() + useI18n('Terminations.TerminateEmployee') + const { t } = useTranslation('Terminations.TerminateEmployee') + + const payrollOptions = [ + { + value: 'dismissalPayroll' as const, + label: t('form.payrollOption.options.dismissalPayroll.label'), + description: t('form.payrollOption.options.dismissalPayroll.description'), + }, + { + value: 'regularPayroll' as const, + label: t('form.payrollOption.options.regularPayroll.label'), + description: t('form.payrollOption.options.regularPayroll.description'), + }, + { + value: 'anotherWay' as const, + label: t('form.payrollOption.options.anotherWay.label'), + description: t('form.payrollOption.options.anotherWay.description'), + }, + ] + + return ( + + + {t('title', { employeeName })} + {t('subtitle')} + + + + + + { + onPayrollOptionChange(value as PayrollOption) + }} + options={payrollOptions} + /> + + {t(`alert.${payrollOption}.text`, { employeeName })} + + + + + + + + + ) +} diff --git a/src/components/Terminations/TerminationFlow/TerminationFlow.tsx b/src/components/Terminations/TerminationFlow/TerminationFlow.tsx new file mode 100644 index 000000000..eb1845e2d --- /dev/null +++ b/src/components/Terminations/TerminationFlow/TerminationFlow.tsx @@ -0,0 +1,27 @@ +import { createMachine } from 'robot3' +import { useMemo } from 'react' +import { terminationMachine } from './terminationStateMachine' +import type { + TerminationFlowProps, + TerminationFlowContextInterface, +} from './TerminationFlowComponents' +import { TerminateEmployeeContextual } from './TerminationFlowComponents' +import { Flow } from '@/components/Flow/Flow' + +export const TerminationFlow = ({ companyId, employeeId, onEvent }: TerminationFlowProps) => { + const terminationFlow = useMemo( + () => + createMachine( + 'form', + terminationMachine, + (initialContext: TerminationFlowContextInterface) => ({ + ...initialContext, + component: TerminateEmployeeContextual, + companyId, + employeeId, + }), + ), + [companyId, employeeId], + ) + return +} diff --git a/src/components/Terminations/TerminationFlow/TerminationFlowComponents.tsx b/src/components/Terminations/TerminationFlow/TerminationFlowComponents.tsx new file mode 100644 index 000000000..a66eeb376 --- /dev/null +++ b/src/components/Terminations/TerminationFlow/TerminationFlowComponents.tsx @@ -0,0 +1,81 @@ +import type { ReactNode } from 'react' +import { useTranslation } from 'react-i18next' +import { TerminateEmployee } from '../TerminateEmployee/TerminateEmployee' +import { TerminationSummary } from '../TerminationSummary/TerminationSummary' +import type { PayrollOption } from '../TerminateEmployee/TerminateEmployeePresentation' +import { useFlow, type FlowContextInterface } from '@/components/Flow/useFlow' +import type { BaseComponentInterface } from '@/components/Base' +import { Flex } from '@/components/Common' +import { ensureRequired } from '@/helpers/ensureRequired' +import { useComponentContext } from '@/contexts/ComponentAdapter/useComponentContext' +import { useI18n } from '@/i18n' +import { componentEvents, type EventType } from '@/shared/constants' + +export interface TerminationFlowProps extends BaseComponentInterface { + companyId: string + employeeId: string +} + +export type TerminationFlowAlert = { + type: 'error' | 'info' | 'success' + title: string + content?: ReactNode +} + +export interface TerminationFlowContextInterface extends FlowContextInterface { + companyId: string + employeeId: string + payrollOption?: PayrollOption + alerts?: TerminationFlowAlert[] +} + +export function TerminateEmployeeContextual() { + const { companyId, employeeId, onEvent, alerts } = useFlow() + const { Alert } = useComponentContext() + useI18n('Terminations.TerminationFlow') + + return ( + + {alerts?.map((alert, index) => ( + + {alert.content} + + ))} + + + ) +} + +export function TerminationSummaryContextual() { + const { companyId, employeeId, payrollOption, onEvent } = + useFlow() + useI18n('Terminations.TerminationFlow') + const { t } = useTranslation('Terminations.TerminationFlow') + + const handleEvent = (event: EventType, data?: unknown) => { + if (event === componentEvents.EMPLOYEE_TERMINATION_CANCELLED) { + onEvent(event, { + ...(data as object), + alert: { + type: 'success', + title: t('cancelSuccess'), + }, + }) + return + } + onEvent(event, data) + } + + return ( + + ) +} diff --git a/src/components/Terminations/TerminationFlow/index.ts b/src/components/Terminations/TerminationFlow/index.ts new file mode 100644 index 000000000..a9d4fde48 --- /dev/null +++ b/src/components/Terminations/TerminationFlow/index.ts @@ -0,0 +1,2 @@ +export { TerminationFlow } from './TerminationFlow' +export type { TerminationFlowProps } from './TerminationFlowComponents' diff --git a/src/components/Terminations/TerminationFlow/terminationStateMachine.ts b/src/components/Terminations/TerminationFlow/terminationStateMachine.ts new file mode 100644 index 000000000..f8c9f61e6 --- /dev/null +++ b/src/components/Terminations/TerminationFlow/terminationStateMachine.ts @@ -0,0 +1,90 @@ +import { transition, reduce, state } from 'robot3' +import type { PayrollOption } from '../TerminateEmployee/TerminateEmployeePresentation' +import type { + TerminationFlowContextInterface, + TerminationFlowAlert, +} from './TerminationFlowComponents' +import { + TerminateEmployeeContextual, + TerminationSummaryContextual, +} from './TerminationFlowComponents' +import { componentEvents } from '@/shared/constants' +import type { MachineEventType, MachineTransition } from '@/types/Helpers' + +type EventPayloads = { + [componentEvents.EMPLOYEE_TERMINATION_DONE]: { + employeeId: string + effectiveDate: string + payrollOption: PayrollOption + } + [componentEvents.EMPLOYEE_TERMINATION_EDIT]: { + employeeId: string + } + [componentEvents.EMPLOYEE_TERMINATION_CANCELLED]: { + employeeId: string + alert?: TerminationFlowAlert + } + [componentEvents.EMPLOYEE_TERMINATION_RUN_PAYROLL]: { + employeeId: string + companyId: string + } + [componentEvents.EMPLOYEE_TERMINATION_RUN_OFF_CYCLE_PAYROLL]: { + employeeId: string + companyId: string + } +} + +export const terminationMachine = { + form: state( + transition( + componentEvents.EMPLOYEE_TERMINATION_DONE, + 'summary', + reduce( + ( + ctx: TerminationFlowContextInterface, + ev: MachineEventType, + ): TerminationFlowContextInterface => { + return { + ...ctx, + component: TerminationSummaryContextual, + payrollOption: ev.payload.payrollOption, + alerts: undefined, + } + }, + ), + ), + ), + summary: state( + transition( + componentEvents.EMPLOYEE_TERMINATION_EDIT, + 'form', + reduce((ctx: TerminationFlowContextInterface): TerminationFlowContextInterface => { + return { + ...ctx, + component: TerminateEmployeeContextual, + alerts: undefined, + } + }), + ), + transition( + componentEvents.EMPLOYEE_TERMINATION_CANCELLED, + 'form', + reduce( + ( + ctx: TerminationFlowContextInterface, + ev: MachineEventType< + EventPayloads, + typeof componentEvents.EMPLOYEE_TERMINATION_CANCELLED + >, + ): TerminationFlowContextInterface => { + return { + ...ctx, + component: TerminateEmployeeContextual, + alerts: ev.payload.alert ? [ev.payload.alert] : undefined, + payrollOption: undefined, + } + }, + ), + ), + ), +} diff --git a/src/components/Terminations/TerminationSummary/TerminationSummary.test.tsx b/src/components/Terminations/TerminationSummary/TerminationSummary.test.tsx new file mode 100644 index 000000000..a1a84414e --- /dev/null +++ b/src/components/Terminations/TerminationSummary/TerminationSummary.test.tsx @@ -0,0 +1,273 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { screen, waitFor } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { http, HttpResponse } from 'msw' +import { TerminationSummary } from './TerminationSummary' +import { server } from '@/test/mocks/server' +import { componentEvents } from '@/shared/constants' +import { setupApiTestMocks } from '@/test/mocks/apiServer' +import { renderWithProviders } from '@/test-utils/renderWithProviders' +import { API_BASE_URL } from '@/test/constants' + +const mockEmployee = { + uuid: 'employee-123', + first_name: 'John', + last_name: 'Doe', + email: 'john.doe@example.com', + company_uuid: 'company-123', + terminated: true, + onboarded: true, +} + +const getFutureDate = () => { + const date = new Date() + date.setDate(date.getDate() + 7) + return date.toISOString().split('T')[0] +} + +const getPastDate = () => { + const date = new Date() + date.setDate(date.getDate() - 7) + return date.toISOString().split('T')[0] +} + +const mockTerminationCancelable = { + uuid: 'termination-123', + employee_uuid: 'employee-123', + effective_date: getFutureDate(), + run_termination_payroll: false, + active: true, + cancelable: true, +} + +const mockTerminationWithPayroll = { + uuid: 'termination-456', + employee_uuid: 'employee-123', + effective_date: getFutureDate(), + run_termination_payroll: true, + active: true, + cancelable: false, +} + +const mockTerminationPast = { + uuid: 'termination-789', + employee_uuid: 'employee-123', + effective_date: getPastDate(), + run_termination_payroll: false, + active: true, + cancelable: false, +} + +describe('TerminationSummary', () => { + const onEvent = vi.fn() + const user = userEvent.setup() + const defaultProps = { + employeeId: 'employee-123', + companyId: 'company-123', + onEvent, + } + + beforeEach(() => { + setupApiTestMocks() + onEvent.mockClear() + + server.use( + http.get(`${API_BASE_URL}/v1/employees/:employee_id`, () => { + return HttpResponse.json(mockEmployee) + }), + ) + }) + + describe('rendering', () => { + it('renders success alert with employee name', async () => { + server.use( + http.get(`${API_BASE_URL}/v1/employees/:employee_id/terminations`, () => { + return HttpResponse.json([mockTerminationCancelable]) + }), + ) + + renderWithProviders() + + await waitFor(() => { + expect(screen.getByText('John Doe has been successfully terminated')).toBeInTheDocument() + }) + }) + + it('displays termination dates correctly', async () => { + server.use( + http.get(`${API_BASE_URL}/v1/employees/:employee_id/terminations`, () => { + return HttpResponse.json([mockTerminationCancelable]) + }), + ) + + renderWithProviders() + + await waitFor(() => { + expect(screen.getByText('Last day of work')).toBeInTheDocument() + }) + + expect(screen.getByText('Last pay day')).toBeInTheDocument() + }) + }) + + describe('conditional buttons', () => { + it('shows cancel button when termination is cancelable', async () => { + server.use( + http.get(`${API_BASE_URL}/v1/employees/:employee_id/terminations`, () => { + return HttpResponse.json([mockTerminationCancelable]) + }), + ) + + renderWithProviders() + + await waitFor(() => { + expect(screen.getByRole('button', { name: 'Cancel termination' })).toBeInTheDocument() + }) + }) + + it('shows edit button when effective date is in the future', async () => { + server.use( + http.get(`${API_BASE_URL}/v1/employees/:employee_id/terminations`, () => { + return HttpResponse.json([mockTerminationCancelable]) + }), + ) + + renderWithProviders() + + await waitFor(() => { + expect(screen.getByRole('button', { name: 'Edit termination' })).toBeInTheDocument() + }) + }) + + it('hides edit button when effective date is in the past', async () => { + server.use( + http.get(`${API_BASE_URL}/v1/employees/:employee_id/terminations`, () => { + return HttpResponse.json([mockTerminationPast]) + }), + ) + + renderWithProviders() + + await waitFor(() => { + expect(screen.getByText('John Doe has been successfully terminated')).toBeInTheDocument() + }) + + expect(screen.queryByRole('button', { name: 'Edit termination' })).not.toBeInTheDocument() + }) + + it('shows run payroll button when dismissal payroll was selected', async () => { + server.use( + http.get(`${API_BASE_URL}/v1/employees/:employee_id/terminations`, () => { + return HttpResponse.json([mockTerminationWithPayroll]) + }), + ) + + renderWithProviders() + + await waitFor(() => { + expect(screen.getByRole('button', { name: 'Run termination payroll' })).toBeInTheDocument() + }) + }) + + it('hides run payroll button when dismissal payroll was not selected', async () => { + server.use( + http.get(`${API_BASE_URL}/v1/employees/:employee_id/terminations`, () => { + return HttpResponse.json([mockTerminationCancelable]) + }), + ) + + renderWithProviders() + + await waitFor(() => { + expect(screen.getByText('John Doe has been successfully terminated')).toBeInTheDocument() + }) + + expect( + screen.queryByRole('button', { name: 'Run termination payroll' }), + ).not.toBeInTheDocument() + }) + }) + + describe('actions', () => { + it('emits EMPLOYEE_TERMINATION_CANCELLED event when cancel button is clicked', async () => { + server.use( + http.get(`${API_BASE_URL}/v1/employees/:employee_id/terminations`, () => { + return HttpResponse.json([mockTerminationCancelable]) + }), + http.delete(`${API_BASE_URL}/v1/employees/:employee_id/terminations`, () => { + return new HttpResponse(null, { status: 204 }) + }), + ) + + renderWithProviders() + + await waitFor(() => { + expect(screen.getByRole('button', { name: 'Cancel termination' })).toBeInTheDocument() + }) + + await user.click(screen.getByRole('button', { name: 'Cancel termination' })) + + await waitFor(() => { + expect(screen.getByRole('button', { name: 'Yes, cancel termination' })).toBeInTheDocument() + }) + + await user.click(screen.getByRole('button', { name: 'Yes, cancel termination' })) + + await waitFor(() => { + expect(onEvent).toHaveBeenCalledWith( + componentEvents.EMPLOYEE_TERMINATION_CANCELLED, + expect.objectContaining({ + employeeId: 'employee-123', + }), + ) + }) + }) + + it('emits EMPLOYEE_TERMINATION_EDIT event when edit button is clicked', async () => { + server.use( + http.get(`${API_BASE_URL}/v1/employees/:employee_id/terminations`, () => { + return HttpResponse.json([mockTerminationCancelable]) + }), + ) + + renderWithProviders() + + await waitFor(() => { + expect(screen.getByRole('button', { name: 'Edit termination' })).toBeInTheDocument() + }) + + await user.click(screen.getByRole('button', { name: 'Edit termination' })) + + expect(onEvent).toHaveBeenCalledWith( + componentEvents.EMPLOYEE_TERMINATION_EDIT, + expect.objectContaining({ + employeeId: 'employee-123', + }), + ) + }) + + it('emits EMPLOYEE_TERMINATION_RUN_PAYROLL event when run payroll button is clicked', async () => { + server.use( + http.get(`${API_BASE_URL}/v1/employees/:employee_id/terminations`, () => { + return HttpResponse.json([mockTerminationWithPayroll]) + }), + ) + + renderWithProviders() + + await waitFor(() => { + expect(screen.getByRole('button', { name: 'Run termination payroll' })).toBeInTheDocument() + }) + + await user.click(screen.getByRole('button', { name: 'Run termination payroll' })) + + expect(onEvent).toHaveBeenCalledWith( + componentEvents.EMPLOYEE_TERMINATION_RUN_PAYROLL, + expect.objectContaining({ + employeeId: 'employee-123', + companyId: 'company-123', + }), + ) + }) + }) +}) diff --git a/src/components/Terminations/TerminationSummary/TerminationSummary.tsx b/src/components/Terminations/TerminationSummary/TerminationSummary.tsx new file mode 100644 index 000000000..2dcd6ad4f --- /dev/null +++ b/src/components/Terminations/TerminationSummary/TerminationSummary.tsx @@ -0,0 +1,144 @@ +import { useState } from 'react' +import { useQueryClient } from '@tanstack/react-query' +import { useEmployeesGetSuspense } from '@gusto/embedded-api/react-query/employeesGet' +import { + useEmployeeEmploymentsGetTerminationsSuspense, + invalidateAllEmployeeEmploymentsGetTerminations, +} from '@gusto/embedded-api/react-query/employeeEmploymentsGetTerminations' +import { useEmployeeEmploymentsDeleteTerminationMutation } from '@gusto/embedded-api/react-query/employeeEmploymentsDeleteTermination' +import { invalidateAllEmployeesList } from '@gusto/embedded-api/react-query/employeesList' +import { TerminationSummaryPresentation } from './TerminationSummaryPresentation' +import { normalizeToDate } from '@/helpers/dateFormatting' +import type { BaseComponentInterface } from '@/components/Base/Base' +import { BaseComponent } from '@/components/Base/Base' +import { useBase } from '@/components/Base/useBase' +import { componentEvents } from '@/shared/constants' +import { useComponentDictionary, useI18n } from '@/i18n' + +export type PayrollOption = 'dismissalPayroll' | 'regularPayroll' | 'anotherWay' + +export interface TerminationSummaryProps extends BaseComponentInterface<'Terminations.TerminationSummary'> { + employeeId: string + companyId: string + payrollOption?: PayrollOption +} + +export function TerminationSummary(props: TerminationSummaryProps) { + return ( + + {props.children} + + ) +} + +const Root = ({ employeeId, companyId, payrollOption, dictionary }: TerminationSummaryProps) => { + useComponentDictionary('Terminations.TerminationSummary', dictionary) + useI18n('Terminations.TerminationSummary') + + const queryClient = useQueryClient() + const { onEvent, baseSubmitHandler } = useBase() + + const [isCancelDialogOpen, setIsCancelDialogOpen] = useState(false) + + const { + data: { employee }, + } = useEmployeesGetSuspense({ employeeId }) + + const { data: terminationsData } = useEmployeeEmploymentsGetTerminationsSuspense({ employeeId }) + + const { mutateAsync: deleteTermination, isPending: isDeleting } = + useEmployeeEmploymentsDeleteTerminationMutation() + + const employeeName = [employee?.firstName, employee?.lastName].filter(Boolean).join(' ') + + const terminations = terminationsData.terminationList ?? [] + const termination = terminations.find(t => t.active) ?? terminations[0] + + const effectiveDate = termination?.effectiveDate + const canCancel = termination?.cancelable === true + const effectiveDateLocal = normalizeToDate(effectiveDate) + const todayMidnight = new Date(new Date().toDateString()) + const canEdit = effectiveDateLocal ? effectiveDateLocal >= todayMidnight : false + + const showRunOffCyclePayroll = payrollOption === 'anotherWay' + const showRunPayroll = + !showRunOffCyclePayroll && + (termination?.runTerminationPayroll === true || payrollOption === 'dismissalPayroll') + + const handleCancelClick = () => { + setIsCancelDialogOpen(true) + } + + const handleDialogClose = () => { + setIsCancelDialogOpen(false) + } + + const handleConfirmCancel = async () => { + if (!termination) return + + await baseSubmitHandler({ terminationId: termination.uuid }, async () => { + await deleteTermination({ + request: { + employeeId, + }, + }) + + await invalidateAllEmployeeEmploymentsGetTerminations(queryClient) + await invalidateAllEmployeesList(queryClient) + + setIsCancelDialogOpen(false) + + onEvent(componentEvents.EMPLOYEE_TERMINATION_CANCELLED, { + employeeId, + termination, + }) + }) + } + + const handleEditDismissal = () => { + onEvent(componentEvents.EMPLOYEE_TERMINATION_EDIT, { + employeeId, + termination, + }) + } + + const handleRunDismissalPayroll = () => { + onEvent(componentEvents.EMPLOYEE_TERMINATION_RUN_PAYROLL, { + employeeId, + companyId, + termination, + }) + } + + const handleRunOffCyclePayroll = () => { + onEvent(componentEvents.EMPLOYEE_TERMINATION_RUN_OFF_CYCLE_PAYROLL, { + employeeId, + companyId, + termination, + }) + } + + if (!termination) { + return null + } + + return ( + + ) +} diff --git a/src/components/Terminations/TerminationSummary/TerminationSummaryPresentation.tsx b/src/components/Terminations/TerminationSummary/TerminationSummaryPresentation.tsx new file mode 100644 index 000000000..f3399f399 --- /dev/null +++ b/src/components/Terminations/TerminationSummary/TerminationSummaryPresentation.tsx @@ -0,0 +1,111 @@ +import { useTranslation } from 'react-i18next' +import { ActionsLayout, Flex } from '@/components/Common' +import { useComponentContext } from '@/contexts/ComponentAdapter/useComponentContext' +import { useDateFormatter } from '@/hooks/useDateFormatter' +import { useI18n } from '@/i18n' + +interface TerminationSummaryPresentationProps { + employeeName: string + effectiveDate: string | undefined + canCancel: boolean + canEdit: boolean + showRunPayroll: boolean + showRunOffCyclePayroll: boolean + onCancelClick: () => void + onEditDismissal: () => void + onRunDismissalPayroll: () => void + onRunOffCyclePayroll: () => void + isLoading: boolean + isCancelDialogOpen: boolean + onDialogClose: () => void + onDialogConfirm: () => void + isCancelling: boolean +} + +export function TerminationSummaryPresentation({ + employeeName, + effectiveDate, + canCancel, + canEdit, + showRunPayroll, + showRunOffCyclePayroll, + onCancelClick, + onEditDismissal, + onRunDismissalPayroll, + onRunOffCyclePayroll, + isLoading, + isCancelDialogOpen, + onDialogClose, + onDialogConfirm, + isCancelling, +}: TerminationSummaryPresentationProps) { + const { Alert, Heading, Text, Button, DescriptionList, Dialog } = useComponentContext() + const { formatLongWithYear } = useDateFormatter() + useI18n('Terminations.TerminationSummary') + const { t } = useTranslation('Terminations.TerminationSummary') + + const formattedDate = formatLongWithYear(effectiveDate) || 'N/A' + + const dateItems = [ + { + term: t('dates.lastDayOfWork'), + description: formattedDate, + }, + { + term: t('dates.lastPayDay'), + description: formattedDate, + }, + ] + + const hasActions = canCancel || canEdit || showRunPayroll || showRunOffCyclePayroll + + return ( + + + + {t('title')} + {t('subtitle')} + + + + + {hasActions && ( + + {canCancel && ( + + )} + {canEdit && ( + + )} + {showRunPayroll && ( + + )} + {showRunOffCyclePayroll && ( + + )} + + )} + + + {t('cancelDialog.body')} + + + ) +} diff --git a/src/components/Terminations/TerminationsData.tsx b/src/components/Terminations/TerminationsData.tsx new file mode 100644 index 000000000..1a8d4ec46 --- /dev/null +++ b/src/components/Terminations/TerminationsData.tsx @@ -0,0 +1,1255 @@ +import { Suspense, useState } from 'react' +import { useQueryClient } from '@tanstack/react-query' +import { usePayrollsListSuspense } from '@gusto/embedded-api/react-query/payrollsList' +import { usePaySchedulesGetUnprocessedTerminationPeriodsSuspense } from '@gusto/embedded-api/react-query/paySchedulesGetUnprocessedTerminationPeriods' +import { + useEmployeesListSuspense, + invalidateAllEmployeesList, +} from '@gusto/embedded-api/react-query/employeesList' +import { useGustoEmbeddedContext } from '@gusto/embedded-api/react-query/_context' +import { payrollsPrepare } from '@gusto/embedded-api/funcs/payrollsPrepare' +import { + ProcessingStatuses, + PayrollTypes, +} from '@gusto/embedded-api/models/operations/getv1companiescompanyidpayrolls' +import { OffCycleReasonType } from '@gusto/embedded-api/models/components/offcyclereasontype' +import type { Payroll } from '@gusto/embedded-api/models/components/payroll' +import type { UnprocessedTerminationPayPeriod } from '@gusto/embedded-api/models/components/unprocessedterminationpayperiod' +import type { PayrollPrepared } from '@gusto/embedded-api/models/components/payrollprepared' +import type { ShowEmployees } from '@gusto/embedded-api/models/components/showemployees' +import { TerminateEmployee } from './TerminateEmployee/TerminateEmployee' +import { TerminationSummary } from './TerminationSummary/TerminationSummary' + +interface TerminationsDataProps { + companyId: string + useMockData?: boolean +} + +const MOCK_TERMINATION_PERIODS: UnprocessedTerminationPayPeriod[] = [ + { + employeeUuid: 'mock-employee-past-123', + employeeName: 'John Doe (Past Termination - 3 weeks ago)', + startDate: '2024-12-01', + endDate: '2024-12-07', + checkDate: '2024-12-10', + debitDate: '2024-12-08', + payScheduleUuid: 'mock-pay-schedule-1', + }, + { + employeeUuid: 'mock-employee-past-123', + employeeName: 'John Doe (Past Termination - 3 weeks ago)', + startDate: '2024-12-08', + endDate: '2024-12-14', + checkDate: '2024-12-17', + debitDate: '2024-12-15', + payScheduleUuid: 'mock-pay-schedule-1', + }, + { + employeeUuid: 'mock-employee-past-123', + employeeName: 'John Doe (Past Termination - 3 weeks ago)', + startDate: '2024-12-15', + endDate: '2024-12-21', + checkDate: '2024-12-24', + debitDate: '2024-12-22', + payScheduleUuid: 'mock-pay-schedule-1', + }, + { + employeeUuid: 'mock-employee-recent-456', + employeeName: 'Jane Smith (Recent Termination)', + startDate: '2024-12-22', + endDate: '2024-12-28', + checkDate: '2024-12-31', + debitDate: '2024-12-29', + payScheduleUuid: 'mock-pay-schedule-2', + }, +] + +const MOCK_TERMINATED_EMPLOYEES: ShowEmployees[] = [ + { + uuid: 'mock-employee-past-123', + firstName: 'John', + lastName: 'Doe', + terminated: true, + terminations: [ + { + effectiveDate: '2024-12-01', + active: true, + runTerminationPayroll: true, + cancelable: false, + }, + ], + } as ShowEmployees, + { + uuid: 'mock-employee-recent-456', + firstName: 'Jane', + lastName: 'Smith', + terminated: true, + terminations: [ + { + effectiveDate: '2024-12-22', + active: true, + runTerminationPayroll: true, + cancelable: true, + }, + ], + } as ShowEmployees, +] + +const MOCK_DISMISSAL_PAYROLLS: Payroll[] = [ + { + payrollUuid: 'mock-payroll-dismissal-1', + companyUuid: 'mock-company', + processed: false, + offCycle: true, + offCycleReason: OffCycleReasonType.DismissedEmployee, + checkDate: '2024-12-10', + payPeriod: { + startDate: '2024-12-01', + endDate: '2024-12-07', + }, + finalTerminationPayroll: true, + } as Payroll, + { + payrollUuid: 'mock-payroll-dismissal-2', + companyUuid: 'mock-company', + processed: false, + offCycle: true, + offCycleReason: OffCycleReasonType.DismissedEmployee, + checkDate: '2024-12-17', + payPeriod: { + startDate: '2024-12-08', + endDate: '2024-12-14', + }, + finalTerminationPayroll: true, + } as Payroll, + { + payrollUuid: 'mock-payroll-dismissal-3', + companyUuid: 'mock-company', + processed: false, + offCycle: true, + offCycleReason: OffCycleReasonType.DismissedEmployee, + checkDate: '2024-12-24', + payPeriod: { + startDate: '2024-12-15', + endDate: '2024-12-21', + }, + finalTerminationPayroll: true, + } as Payroll, + { + payrollUuid: 'mock-payroll-dismissal-4', + companyUuid: 'mock-company', + processed: false, + offCycle: true, + offCycleReason: OffCycleReasonType.DismissedEmployee, + checkDate: '2024-12-31', + payPeriod: { + startDate: '2024-12-22', + endDate: '2024-12-28', + }, + finalTerminationPayroll: true, + } as Payroll, +] + +type EmployeeInfo = { + uuid: string + firstName: string | null | undefined + lastName: string | null | undefined + excluded?: boolean +} + +const MOCK_EMPLOYEES_BY_PAYROLL: Record = { + 'mock-payroll-dismissal-1': [ + { uuid: 'mock-employee-past-123', firstName: 'John', lastName: 'Doe', excluded: false }, + ], + 'mock-payroll-dismissal-2': [ + { uuid: 'mock-employee-past-123', firstName: 'John', lastName: 'Doe', excluded: false }, + ], + 'mock-payroll-dismissal-3': [ + { uuid: 'mock-employee-past-123', firstName: 'John', lastName: 'Doe', excluded: false }, + ], + 'mock-payroll-dismissal-4': [ + { uuid: 'mock-employee-recent-456', firstName: 'Jane', lastName: 'Smith', excluded: false }, + ], +} + +const mockDataBannerStyles: React.CSSProperties = { + backgroundColor: '#fef3c7', + border: '2px dashed #f59e0b', + borderRadius: '8px', + padding: '12px 16px', + marginBottom: '24px', + display: 'flex', + alignItems: 'center', + gap: '12px', +} + +const mockBadgeStyles: React.CSSProperties = { + display: 'inline-block', + padding: '2px 8px', + borderRadius: '4px', + fontSize: '11px', + fontWeight: 600, + backgroundColor: '#f59e0b', + color: 'white', + marginLeft: '8px', + textTransform: 'uppercase', + letterSpacing: '0.5px', +} + +type PayrollCategory = 'Regular' | 'Off-Cycle' | 'Dismissal' + +const getPayrollCategory = (payroll: Payroll): PayrollCategory => { + if ( + payroll.offCycleReason === OffCycleReasonType.DismissedEmployee || + payroll.finalTerminationPayroll + ) { + return 'Dismissal' + } + if (payroll.offCycle) { + return 'Off-Cycle' + } + return 'Regular' +} + +const tableStyles: React.CSSProperties = { + width: '100%', + borderCollapse: 'collapse', + marginBottom: '24px', + fontSize: '14px', +} + +const thStyles: React.CSSProperties = { + textAlign: 'left', + padding: '12px 8px', + borderBottom: '2px solid #e5e7eb', + backgroundColor: '#f9fafb', + fontWeight: 600, +} + +const tdStyles: React.CSSProperties = { + padding: '12px 8px', + borderBottom: '1px solid #e5e7eb', +} + +const sectionStyles: React.CSSProperties = { + marginBottom: '32px', +} + +const headingStyles: React.CSSProperties = { + fontSize: '18px', + fontWeight: 600, + marginBottom: '16px', + color: '#111827', +} + +const badgeStyles: Record = { + Regular: { + display: 'inline-block', + padding: '2px 8px', + borderRadius: '4px', + fontSize: '12px', + fontWeight: 500, + backgroundColor: '#dbeafe', + color: '#1e40af', + }, + 'Off-Cycle': { + display: 'inline-block', + padding: '2px 8px', + borderRadius: '4px', + fontSize: '12px', + fontWeight: 500, + backgroundColor: '#fef3c7', + color: '#92400e', + }, + Dismissal: { + display: 'inline-block', + padding: '2px 8px', + borderRadius: '4px', + fontSize: '12px', + fontWeight: 500, + backgroundColor: '#fee2e2', + color: '#991b1b', + }, +} + +const boolDisplayValue = (value: boolean | null | undefined): string => { + if (value === true) return 'Yes' + if (value === false) return 'No' + return 'N/A' +} + +const tooltipStyles: React.CSSProperties = { + position: 'absolute', + bottom: '100%', + left: '50%', + transform: 'translateX(-50%)', + backgroundColor: '#1f2937', + color: 'white', + padding: '8px 12px', + borderRadius: '6px', + fontSize: '12px', + whiteSpace: 'normal', + zIndex: 1000, + marginBottom: '4px', + boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)', + maxWidth: '250px', + minWidth: '150px', +} + +const modalOverlayStyles: React.CSSProperties = { + position: 'fixed', + top: 0, + left: 0, + right: 0, + bottom: 0, + backgroundColor: 'rgba(0, 0, 0, 0.5)', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + zIndex: 9999, +} + +const modalContentStyles: React.CSSProperties = { + backgroundColor: 'white', + borderRadius: '12px', + padding: '24px', + maxWidth: '600px', + width: '90%', + maxHeight: '90vh', + overflow: 'auto', + boxShadow: '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)', +} + +const selectStyles: React.CSSProperties = { + width: '100%', + padding: '10px 12px', + fontSize: '14px', + border: '1px solid #d1d5db', + borderRadius: '6px', + backgroundColor: 'white', + cursor: 'pointer', + marginBottom: '20px', +} + +type PayrollEmployeeData = { + loading: boolean + loaded: boolean + employees: EmployeeInfo[] + error?: string +} + +function EmployeeCountCell({ + payroll, + companyId, + isMockData, +}: { + payroll: Payroll + companyId: string + isMockData?: boolean +}) { + const gustoEmbedded = useGustoEmbeddedContext() + const payrollId = payroll.payrollUuid || payroll.uuid + const isMockPayroll = payrollId?.startsWith('mock-') + + const mockEmployees = isMockPayroll && payrollId ? MOCK_EMPLOYEES_BY_PAYROLL[payrollId] || [] : [] + + const [employeeData, setEmployeeData] = useState(() => { + if (isMockPayroll) { + return { + loading: false, + loaded: true, + employees: mockEmployees, + } + } + return { + loading: false, + loaded: false, + employees: [], + } + }) + const [showTooltip, setShowTooltip] = useState(false) + + const loadEmployees = async () => { + if (isMockPayroll || employeeData.loaded || employeeData.loading) return + + setEmployeeData(prev => ({ ...prev, loading: true })) + + if (!payrollId) { + setEmployeeData({ loading: false, loaded: true, employees: [], error: 'No payroll ID' }) + return + } + + const result = await payrollsPrepare(gustoEmbedded, { companyId, payrollId }) + + if (!result.ok) { + setEmployeeData({ + loading: false, + loaded: true, + employees: [], + error: 'Failed to load', + }) + return + } + + const preparedPayroll = result.value as { payrollPrepared?: PayrollPrepared } + const employeeCompensations = preparedPayroll.payrollPrepared?.employeeCompensations || [] + + const employees: EmployeeInfo[] = employeeCompensations.map(ec => ({ + uuid: ec.employeeUuid || '', + firstName: ec.firstName, + lastName: ec.lastName, + excluded: ec.excluded, + })) + + setEmployeeData({ + loading: false, + loaded: true, + employees, + }) + } + + const getDisplayText = () => { + if (employeeData.loading) return 'Loading...' + if (employeeData.error) return 'Error' + if (employeeData.loaded) { + const active = employeeData.employees.filter(e => !e.excluded).length + const total = employeeData.employees.length + return active === total ? `${total}` : `${active}/${total}` + } + return 'Click to load' + } + + const renderEmployeeList = () => { + return ( +
    + {employeeData.employees.map((e, idx) => { + const name = `${e.firstName || ''} ${e.lastName || ''}`.trim() || 'Unknown' + return ( +
  • + {e.excluded ? `${name} (excluded)` : name} +
  • + ) + })} +
+ ) + } + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault() + void loadEmployees() + } + } + + return ( + + + + ) +} + +function PayrollTable({ + payrolls, + companyId, + isMockData, +}: { + payrolls: Payroll[] + companyId: string + isMockData?: boolean +}) { + if (payrolls.length === 0) { + return

No payrolls found.

+ } + + return ( + + + + + + + + + + + + + {isMockData && } + + + + {payrolls.map(payroll => { + const category = getPayrollCategory(payroll) + const payPeriodDisplay = payroll.payPeriod + ? `${payroll.payPeriod.startDate} - ${payroll.payPeriod.endDate}` + : 'N/A' + const isMockRow = payroll.payrollUuid?.startsWith('mock-') + + return ( + + + + + + + + + + + {isMockData && ( + + )} + + ) + })} + +
Pay PeriodCheck DateTypeEmployeesOff-CycleOff-Cycle ReasonFinal TermStatusPayroll IDSource
+ {payPeriodDisplay} + {isMockRow && Mock} + {payroll.checkDate || 'N/A'} + {category} + {boolDisplayValue(payroll.offCycle)}{payroll.offCycleReason || '-'}{boolDisplayValue(payroll.finalTerminationPayroll)}{payroll.processed ? 'Processed' : 'Unprocessed'} + {payroll.payrollUuid || payroll.uuid} + + + {isMockRow ? 'Mock' : 'Real'} + +
+ ) +} + +function TerminationPayPeriodsTable({ + periods, + isMockData, +}: { + periods: UnprocessedTerminationPayPeriod[] + isMockData?: boolean +}) { + if (periods.length === 0) { + return ( +

+ No unprocessed termination pay periods found. +

+ ) + } + + return ( + + + + + + + + + {isMockData && } + + + + {periods.map((period, index) => { + const payPeriodDisplay = + period.startDate && period.endDate ? `${period.startDate} - ${period.endDate}` : 'N/A' + const isMockRow = period.employeeUuid?.startsWith('mock-') + + return ( + + + + + + + {isMockData && ( + + )} + + ) + })} + +
EmployeePay PeriodCheck DateDebit DateEmployee IDSource
+ {period.employeeName || 'Unknown'} + {isMockRow && Mock} + {payPeriodDisplay}{period.checkDate || 'N/A'}{period.debitDate || 'N/A'} + {period.employeeUuid} + + + {isMockRow ? 'Mock' : 'Real'} + +
+ ) +} + +function TerminatedEmployeesTable({ + employees, + isMockData, + onViewSummary, +}: { + employees: ShowEmployees[] + isMockData?: boolean + onViewSummary: (employeeId: string) => void +}) { + if (employees.length === 0) { + return

No terminated employees found.

+ } + + return ( + + + + + + + + + + {isMockData && } + + + + + {employees.map(employee => { + const termination = employee.terminations?.[0] + const employeeName = + `${employee.firstName || ''} ${employee.lastName || ''}`.trim() || 'Unknown' + const isMockRow = employee.uuid.startsWith('mock-') + + return ( + + + + + + + + {isMockData && ( + + )} + + + ) + })} + +
EmployeeEffective DateActiveDismissal PayrollCancelableEmployee IDSourceActions
+ {employeeName} + {isMockRow && Mock} + {termination?.effectiveDate || 'N/A'} + + {termination?.active ? 'Terminated' : 'Scheduled'} + + {boolDisplayValue(termination?.runTerminationPayroll)}{boolDisplayValue(termination?.cancelable)} + {employee.uuid} + + + {isMockRow ? 'Mock' : 'Real'} + + + +
+ ) +} + +interface TerminationModalProps { + isOpen: boolean + onClose: () => void + companyId: string + activeEmployees: ShowEmployees[] + onTerminationComplete: () => void +} + +function TerminationModal({ + isOpen, + onClose, + companyId, + activeEmployees, + onTerminationComplete, +}: TerminationModalProps) { + const [selectedEmployeeId, setSelectedEmployeeId] = useState('') + + if (!isOpen) return null + + const handleClose = () => { + onClose() + setSelectedEmployeeId('') + } + + const handleTerminationEvent = (event: string) => { + if (event === 'employee/termination/done' || event === 'cancel') { + onTerminationComplete() + handleClose() + } + } + + return ( +
+ +
+ +
+ + +
+ + {selectedEmployeeId ? ( +
+ + Loading employee data... +
+ } + > + + + + ) : ( +

+ Select an employee to begin the termination process. +

+ )} + + + ) +} + +interface TerminationSummaryModalProps { + isOpen: boolean + onClose: () => void + companyId: string + employeeId: string | null +} + +function TerminationSummaryModal({ + isOpen, + onClose, + companyId, + employeeId, +}: TerminationSummaryModalProps) { + if (!isOpen || !employeeId) return null + + const handleEvent = (event: string) => { + if ( + event === 'employee/termination/cancelled' || + event === 'employee/termination/edit' || + event === 'employee/termination/runPayroll' || + event === 'employee/termination/runOffCyclePayroll' + ) { + onClose() + } + } + + return ( +
+ +
+ + + Loading termination summary... + + } + > + + + + + ) +} + +function TerminationsDataContent({ companyId, useMockData }: TerminationsDataProps) { + const queryClient = useQueryClient() + const [isModalOpen, setIsModalOpen] = useState(false) + const [showMockData, setShowMockData] = useState(useMockData ?? false) + const [summaryEmployeeId, setSummaryEmployeeId] = useState(null) + + const today = new Date() + const threeMonthsAgo = new Date(today.getFullYear(), today.getMonth() - 3, today.getDate()) + const threeMonthsFromNow = new Date(today.getFullYear(), today.getMonth() + 3, today.getDate()) + const formatDate = (date: Date) => date.toISOString().split('T')[0] + + const { data: payrollsData, refetch: refetchPayrolls } = usePayrollsListSuspense({ + companyId, + processingStatuses: [ProcessingStatuses.Unprocessed], + payrollTypes: [PayrollTypes.Regular, PayrollTypes.OffCycle], + includeOffCycle: true, + startDate: formatDate(threeMonthsAgo), + endDate: formatDate(threeMonthsFromNow), + }) + + const { data: terminationPeriodsData } = usePaySchedulesGetUnprocessedTerminationPeriodsSuspense({ + companyId, + }) + + const { data: terminatedEmployeesData } = useEmployeesListSuspense({ + companyId, + terminated: true, + }) + + const { data: activeEmployeesData } = useEmployeesListSuspense({ + companyId, + terminated: false, + onboarded: true, + }) + + const handleRefresh = () => { + void refetchPayrolls() + } + + const handleTerminationComplete = () => { + void invalidateAllEmployeesList(queryClient) + void refetchPayrolls() + } + + const realPayrollList = payrollsData.payrollList || [] + const realTerminationPeriods = terminationPeriodsData.unprocessedTerminationPayPeriodList || [] + const realTerminatedEmployees = terminatedEmployeesData.showEmployees || [] + const activeEmployees = activeEmployeesData.showEmployees || [] + + const payrollList = showMockData + ? [...MOCK_DISMISSAL_PAYROLLS, ...realPayrollList] + : realPayrollList + + const terminationPeriods = showMockData + ? [...MOCK_TERMINATION_PERIODS, ...realTerminationPeriods] + : realTerminationPeriods + + const terminatedEmployees = showMockData + ? [...MOCK_TERMINATED_EMPLOYEES, ...realTerminatedEmployees] + : realTerminatedEmployees + + const regularPayrolls = payrollList.filter(p => getPayrollCategory(p) === 'Regular') + const offCyclePayrolls = payrollList.filter(p => getPayrollCategory(p) === 'Off-Cycle') + const dismissalPayrolls = payrollList.filter(p => getPayrollCategory(p) === 'Dismissal') + + const mockPeriodCount = showMockData ? MOCK_TERMINATION_PERIODS.length : 0 + const mockEmployeeCount = showMockData ? MOCK_TERMINATED_EMPLOYEES.length : 0 + const mockPayrollCount = showMockData ? MOCK_DISMISSAL_PAYROLLS.length : 0 + + return ( +
+
+

+ Terminations Data - Payroll Overview +

+
+ + + +
+
+ + {showMockData && ( +
+ 🧪 +
+ Mock Data Mode Enabled +

+ Simulating a past-dated termination scenario: John Doe was terminated 3 weeks ago with{' '} + {mockPeriodCount} unprocessed pay periods and{' '} + {mockPayrollCount} corresponding dismissal payrolls (one for each + period). This demonstrates how our EmployeeTerminations component creates separate + payrolls for each unprocessed period. Mock rows are highlighted in yellow with a + "MOCK" badge. +

+
+
+ )} + +
+
+
{payrollList.length}
+
Total Unprocessed
+
+
+
{regularPayrolls.length}
+
Regular
+
+
+
{offCyclePayrolls.length}
+
Off-Cycle
+
+
+
{dismissalPayrolls.length}
+
Dismissal
+
+
+ +
+

+ All Unprocessed Payrolls + {showMockData && ( + + +{mockPayrollCount} Mock Dismissal + + )} +

+ +
+ +
+

Regular Payrolls

+ +
+ +
+

Off-Cycle Payrolls

+ +
+ +
+

+ Dismissal Payrolls + {showMockData && ( + +{mockPayrollCount} Mock + )} +

+

+ Off-cycle payrolls created for dismissed employees. + {showMockData && ( + + {' '} + These 3 mock payrolls correspond to John Doe's 3 unprocessed pay periods - each + created by our EmployeeTerminations component. + + )} +

+ +
+ +
+

+ Unprocessed Termination Pay Periods + {showMockData && ( + +{mockPeriodCount} Mock + )} +

+

+ These are pay periods for employees who selected "Dismissal Payroll" as their + final payroll option. Match the pay period dates with the payrolls above to identify which + payroll corresponds to each terminated employee. + {showMockData && ( + + {' '} + Note: John Doe has 3 unprocessed pay periods (simulating a past-dated termination). + + )} +

+ +
+ +
+

+ All Terminated Employees ({terminatedEmployees.length}) + {showMockData && ( + +{mockEmployeeCount} Mock + )} +

+

+ All employees who have been terminated or are scheduled to be terminated. +

+ +
+ + { + setIsModalOpen(false) + }} + companyId={companyId} + activeEmployees={activeEmployees} + onTerminationComplete={handleTerminationComplete} + /> + + { + setSummaryEmployeeId(null) + }} + companyId={companyId} + employeeId={summaryEmployeeId} + /> +
+ ) +} + +export function TerminationsData({ companyId, useMockData }: TerminationsDataProps) { + return ( + Loading payroll data...} + > + + + ) +} diff --git a/src/components/Terminations/index.tsx b/src/components/Terminations/index.tsx new file mode 100644 index 000000000..f93544b6d --- /dev/null +++ b/src/components/Terminations/index.tsx @@ -0,0 +1,4 @@ +export { TerminateEmployee } from './TerminateEmployee/TerminateEmployee' +export { TerminationSummary } from './TerminationSummary/TerminationSummary' +export { TerminationsData } from './TerminationsData' +export { TerminationFlow } from './TerminationFlow/TerminationFlow' diff --git a/src/components/index.ts b/src/components/index.ts index 65177cfb3..dc942db67 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -3,3 +3,4 @@ export * as Company from './Company' export * as Contractor from './Contractor' export * as Employee from './Employee' export * as Payroll from './Payroll' +export * as Terminations from './Terminations' diff --git a/src/i18n/en/Terminations.TerminateEmployee.json b/src/i18n/en/Terminations.TerminateEmployee.json new file mode 100644 index 000000000..17e040ed5 --- /dev/null +++ b/src/i18n/en/Terminations.TerminateEmployee.json @@ -0,0 +1,50 @@ +{ + "title": "Terminate {{employeeName}}", + "subtitle": "Set their last day of work and choose how to handle their final payroll.", + "form": { + "lastDayOfEmployment": { + "label": "Last day of work", + "description": "The last day must be after their most recent regular payroll period (even if they were paid $0). If they never started working, the dismissal date has to be on or after the hire date." + }, + "payrollOption": { + "label": "How should the final payroll be handled?", + "description": "We recommend running a dismissal payroll. Make sure you meet your state's requirements about employees' final payment (deadline to pay, unused PTO, etc).", + "options": { + "dismissalPayroll": { + "label": "Dismissal payroll", + "description": "Runs a final payroll that automatically pays out unused PTO, lets you include severance, keep a separate record for audits, and choose a custom payday based on when you run it." + }, + "regularPayroll": { + "label": "Regular payroll", + "description": "Same as dismissal payrolls, except there won’t be a separate record of final payment and you can’t customize the final payday (they’ll be paid on the regular payday)." + }, + "anotherWay": { + "label": "Another way", + "description": "You can run an off-cycle payroll to manually calculate final amounts, or you can pay them outside of the app (but make sure to report this payroll so the amounts are recorded on tax forms). You can also select this option if you’ve already paid them." + } + } + } + }, + "alert": { + "dismissalPayroll": { + "label": "After submitting, you won't be able to undo this dismissal", + "text": "Make sure you want to end {{employeeName}}'s employment. You won't be able to cancel this dismissal and you'll need to rehire them if they return." + }, + "regularPayroll": { + "label": "After their last day, you won't be able to undo this dismissal", + "text": "Make sure you want to end {{employeeName}}'s employment. You will be able to cancel the dismissal or make changes until their last day. After their last working day, you won't be able to cancel this dismissal and you'll need to rehire them if they return." + }, + "anotherWay": { + "label": "After their last day, you won't be able to undo this dismissal", + "text": "Make sure you want to end {{employeeName}}'s employment. You will be able to cancel the dismissal or make changes until their last day. After their last working day, you won't be able to cancel this dismissal and you'll need to rehire them if they return." + } + }, + "actions": { + "submit": "Terminate employee", + "cancel": "Cancel" + }, + "validation": { + "lastDayRequired": "Last day of work is required", + "payrollOptionRequired": "Please select how to handle the final payroll" + } +} diff --git a/src/i18n/en/Terminations.TerminationFlow.json b/src/i18n/en/Terminations.TerminationFlow.json new file mode 100644 index 000000000..7ed05d1c7 --- /dev/null +++ b/src/i18n/en/Terminations.TerminationFlow.json @@ -0,0 +1,3 @@ +{ + "cancelSuccess": "Termination has been cancelled successfully" +} diff --git a/src/i18n/en/Terminations.TerminationSummary.json b/src/i18n/en/Terminations.TerminationSummary.json new file mode 100644 index 000000000..186268945 --- /dev/null +++ b/src/i18n/en/Terminations.TerminationSummary.json @@ -0,0 +1,25 @@ +{ + "title": "Termination summary", + "subtitle": "The termination has been submitted. Here's the timeline and what to expect.", + "alert": { + "success": { + "label": "{{employeeName}} has been successfully terminated" + } + }, + "dates": { + "lastDayOfWork": "Last day of work", + "lastPayDay": "Last pay day" + }, + "actions": { + "cancelTermination": "Cancel termination", + "editDismissal": "Edit termination", + "runDismissalPayroll": "Run termination payroll", + "runOffCyclePayroll": "Run off-cycle payroll" + }, + "cancelDialog": { + "title": "Cancel termination?", + "body": "Are you sure you want to cancel this termination? The employee will remain active.", + "confirm": "Yes, cancel termination", + "cancel": "No, go back" + } +} diff --git a/src/shared/constants.ts b/src/shared/constants.ts index 6f59167d6..c429a2fb7 100644 --- a/src/shared/constants.ts +++ b/src/shared/constants.ts @@ -121,6 +121,17 @@ export const contractorPaymentEvents = { CONTRACTOR_PAYMENT_VIEW: 'contractor/payments/view', } as const +export const terminationEvents = { + EMPLOYEE_TERMINATION_CREATED: 'employee/termination/created', + EMPLOYEE_TERMINATION_PAYROLL_CREATED: 'employee/termination/payroll/created', + EMPLOYEE_TERMINATION_PAYROLL_FAILED: 'employee/termination/payroll/failed', + EMPLOYEE_TERMINATION_DONE: 'employee/termination/done', + EMPLOYEE_TERMINATION_CANCELLED: 'employee/termination/cancelled', + EMPLOYEE_TERMINATION_EDIT: 'employee/termination/edit', + EMPLOYEE_TERMINATION_RUN_PAYROLL: 'employee/termination/runPayroll', + EMPLOYEE_TERMINATION_RUN_OFF_CYCLE_PAYROLL: 'employee/termination/runOffCyclePayroll', +} as const + export const payScheduleEvents = { PAY_SCHEDULE_CREATE: 'paySchedule/create', PAY_SCHEDULE_CREATED: 'paySchedule/created', @@ -177,6 +188,7 @@ export const componentEvents = { ...runPayrollEvents, ...payrollWireEvents, ...contractorPaymentEvents, + ...terminationEvents, } as const export type EventType = (typeof componentEvents)[keyof typeof componentEvents] diff --git a/src/test/setup.ts b/src/test/setup.ts index 96da1d4e0..4f6b0de61 100644 --- a/src/test/setup.ts +++ b/src/test/setup.ts @@ -50,6 +50,14 @@ afterAll(() => { // Mock scrollIntoView Element.prototype.scrollIntoView = vi.fn() +// Mock HTMLDialogElement methods (jsdom doesn't support them) +HTMLDialogElement.prototype.showModal = vi.fn(function (this: HTMLDialogElement) { + this.open = true +}) +HTMLDialogElement.prototype.close = vi.fn(function (this: HTMLDialogElement) { + this.open = false +}) + expect.extend(toHaveNoViolations) // Make accessibility testing utilities globally available diff --git a/src/types/i18next.d.ts b/src/types/i18next.d.ts index 8ac01ea75..9018ae5dc 100644 --- a/src/types/i18next.d.ts +++ b/src/types/i18next.d.ts @@ -1681,6 +1681,84 @@ export interface PayrollWireInstructions{ "confirm":string; }; }; +export interface TerminationsTerminateEmployee{ +"title":string; +"subtitle":string; +"form":{ +"lastDayOfEmployment":{ +"label":string; +"description":string; +}; +"payrollOption":{ +"label":string; +"description":string; +"options":{ +"dismissalPayroll":{ +"label":string; +"description":string; +}; +"regularPayroll":{ +"label":string; +"description":string; +}; +"anotherWay":{ +"label":string; +"description":string; +}; +}; +}; +}; +"alert":{ +"dismissalPayroll":{ +"label":string; +"text":string; +}; +"regularPayroll":{ +"label":string; +"text":string; +}; +"anotherWay":{ +"label":string; +"text":string; +}; +}; +"actions":{ +"submit":string; +"cancel":string; +}; +"validation":{ +"lastDayRequired":string; +"payrollOptionRequired":string; +}; +}; +export interface TerminationsTerminationFlow{ +"cancelSuccess":string; +}; +export interface TerminationsTerminationSummary{ +"title":string; +"subtitle":string; +"alert":{ +"success":{ +"label":string; +}; +}; +"dates":{ +"lastDayOfWork":string; +"lastPayDay":string; +}; +"actions":{ +"cancelTermination":string; +"editDismissal":string; +"runDismissalPayroll":string; +"runOffCyclePayroll":string; +}; +"cancelDialog":{ +"title":string; +"body":string; +"confirm":string; +"cancel":string; +}; +}; export interface common{ "status":{ "loading":string; @@ -1773,6 +1851,7 @@ export interface common{ "CO":string; "CT":string; "DE":string; +"DC":string; "FL":string; "GA":string; "HI":string; @@ -1855,6 +1934,6 @@ export interface common{ interface CustomTypeOptions { defaultNS: 'common'; - resources: { 'Company.Addresses': CompanyAddresses, 'Company.AssignSignatory': CompanyAssignSignatory, 'Company.BankAccount': CompanyBankAccount, 'Company.DocumentList': CompanyDocumentList, 'Company.FederalTaxes': CompanyFederalTaxes, 'Company.Industry': CompanyIndustry, 'Company.Locations': CompanyLocations, 'Company.OnboardingOverview': CompanyOnboardingOverview, 'Company.PaySchedule': CompanyPaySchedule, 'Company.SignatureForm': CompanySignatureForm, 'Company.StateTaxes': CompanyStateTaxes, 'Contractor.Address': ContractorAddress, 'Contractor.ContractorList': ContractorContractorList, 'Contractor.NewHireReport': ContractorNewHireReport, 'Contractor.PaymentMethod': ContractorPaymentMethod, 'Contractor.Payments.CreatePayment': ContractorPaymentsCreatePayment, 'Contractor.Payments.PaymentHistory': ContractorPaymentsPaymentHistory, 'Contractor.Payments.PaymentsList': ContractorPaymentsPaymentsList, 'Contractor.Profile': ContractorProfile, 'Contractor.Submit': ContractorSubmit, 'Employee.BankAccount': EmployeeBankAccount, 'Employee.Compensation': EmployeeCompensation, 'Employee.Deductions': EmployeeDeductions, 'Employee.DocumentSigner': EmployeeDocumentSigner, 'Employee.EmployeeList': EmployeeEmployeeList, 'Employee.FederalTaxes': EmployeeFederalTaxes, 'Employee.HomeAddress': EmployeeHomeAddress, 'Employee.Landing': EmployeeLanding, 'Employee.OnboardingSummary': EmployeeOnboardingSummary, 'Employee.PaySchedules': EmployeePaySchedules, 'Employee.PaymentMethod': EmployeePaymentMethod, 'Employee.Profile': EmployeeProfile, 'Employee.SplitPaycheck': EmployeeSplitPaycheck, 'Employee.StateTaxes': EmployeeStateTaxes, 'Employee.Taxes': EmployeeTaxes, 'Payroll.Common': PayrollCommon, 'Payroll.ConfirmWireDetailsBanner': PayrollConfirmWireDetailsBanner, 'Payroll.ConfirmWireDetailsForm': PayrollConfirmWireDetailsForm, 'Payroll.PayrollBlocker': PayrollPayrollBlocker, 'Payroll.PayrollConfiguration': PayrollPayrollConfiguration, 'Payroll.PayrollEditEmployee': PayrollPayrollEditEmployee, 'Payroll.PayrollFlow': PayrollPayrollFlow, 'Payroll.PayrollHistory': PayrollPayrollHistory, 'Payroll.PayrollLanding': PayrollPayrollLanding, 'Payroll.PayrollList': PayrollPayrollList, 'Payroll.PayrollOverview': PayrollPayrollOverview, 'Payroll.PayrollReceipts': PayrollPayrollReceipts, 'Payroll.WireInstructions': PayrollWireInstructions, 'common': common, } + resources: { 'Company.Addresses': CompanyAddresses, 'Company.AssignSignatory': CompanyAssignSignatory, 'Company.BankAccount': CompanyBankAccount, 'Company.DocumentList': CompanyDocumentList, 'Company.FederalTaxes': CompanyFederalTaxes, 'Company.Industry': CompanyIndustry, 'Company.Locations': CompanyLocations, 'Company.OnboardingOverview': CompanyOnboardingOverview, 'Company.PaySchedule': CompanyPaySchedule, 'Company.SignatureForm': CompanySignatureForm, 'Company.StateTaxes': CompanyStateTaxes, 'Contractor.Address': ContractorAddress, 'Contractor.ContractorList': ContractorContractorList, 'Contractor.NewHireReport': ContractorNewHireReport, 'Contractor.PaymentMethod': ContractorPaymentMethod, 'Contractor.Payments.CreatePayment': ContractorPaymentsCreatePayment, 'Contractor.Payments.PaymentHistory': ContractorPaymentsPaymentHistory, 'Contractor.Payments.PaymentsList': ContractorPaymentsPaymentsList, 'Contractor.Profile': ContractorProfile, 'Contractor.Submit': ContractorSubmit, 'Employee.BankAccount': EmployeeBankAccount, 'Employee.Compensation': EmployeeCompensation, 'Employee.Deductions': EmployeeDeductions, 'Employee.DocumentSigner': EmployeeDocumentSigner, 'Employee.EmployeeList': EmployeeEmployeeList, 'Employee.FederalTaxes': EmployeeFederalTaxes, 'Employee.HomeAddress': EmployeeHomeAddress, 'Employee.Landing': EmployeeLanding, 'Employee.OnboardingSummary': EmployeeOnboardingSummary, 'Employee.PaySchedules': EmployeePaySchedules, 'Employee.PaymentMethod': EmployeePaymentMethod, 'Employee.Profile': EmployeeProfile, 'Employee.SplitPaycheck': EmployeeSplitPaycheck, 'Employee.StateTaxes': EmployeeStateTaxes, 'Employee.Taxes': EmployeeTaxes, 'Payroll.Common': PayrollCommon, 'Payroll.ConfirmWireDetailsBanner': PayrollConfirmWireDetailsBanner, 'Payroll.ConfirmWireDetailsForm': PayrollConfirmWireDetailsForm, 'Payroll.PayrollBlocker': PayrollPayrollBlocker, 'Payroll.PayrollConfiguration': PayrollPayrollConfiguration, 'Payroll.PayrollEditEmployee': PayrollPayrollEditEmployee, 'Payroll.PayrollFlow': PayrollPayrollFlow, 'Payroll.PayrollHistory': PayrollPayrollHistory, 'Payroll.PayrollLanding': PayrollPayrollLanding, 'Payroll.PayrollList': PayrollPayrollList, 'Payroll.PayrollOverview': PayrollPayrollOverview, 'Payroll.PayrollReceipts': PayrollPayrollReceipts, 'Payroll.WireInstructions': PayrollWireInstructions, 'Terminations.TerminateEmployee': TerminationsTerminateEmployee, 'Terminations.TerminationFlow': TerminationsTerminationFlow, 'Terminations.TerminationSummary': TerminationsTerminationSummary, 'common': common, } }; } \ No newline at end of file