Compare commits
	
		
			38 Commits
		
	
	
		
			zelda.by-p
			...
			main
		
	
	| Author | SHA1 | Date | 
|---|---|---|
| 
							
							
								 | 
						acc2058bad | |
| 
							
							
								 | 
						85f252cac7 | |
| 
							
							
								 | 
						aeebe9a43d | |
| 
							
							
								 | 
						cd0de7331b | |
| 
							
							
								 | 
						05d87c367e | |
| 
							
							
								 | 
						69610cbbb5 | |
| 
							
							
								 | 
						1ac558fe75 | |
| 
							
							
								 | 
						024b4cd8c8 | |
| 
							
							
								 | 
						3a1a63bf2a | |
| 
							
							
								 | 
						41e174f545 | |
| 
							
							
								 | 
						52167a360b | |
| 
							
							
								 | 
						5af2f6e548 | |
| 
							
							
								 | 
						3e8e7ae7d7 | |
| 
							
							
								 | 
						41a3baddd8 | |
| 
							
							
								 | 
						5fea257b68 | |
| 
							
							
								 | 
						58d4809548 | |
| 
							
							
								 | 
						d9ff36297a | |
| 
							
							
								 | 
						7b2f5ecdec | |
| 
							
							
								 | 
						aac8861eb5 | |
| 
							
							
								 | 
						aea4169a50 | |
| 
							
							
								 | 
						43dcfc78bb | |
| 
							
							
								 | 
						5f1c7c793c | |
| 
							
							
								 | 
						4f9edf80d0 | |
| 
							
							
								 | 
						168d458009 | |
| 
							
							
								 | 
						00cc23a46d | |
| 
							
							
								 | 
						16ddb88095 | |
| 
							
							
								 | 
						f66c9d6c26 | |
| 
							
							
								 | 
						cb3bfef759 | |
| 
							
							
								 | 
						d929366810 | |
| 
							
							
								 | 
						42f0a881f4 | |
| 
							
							
								 | 
						4d2c39c053 | |
| 
							
							
								 | 
						fe4d22f361 | |
| 
							
							
								
								 | 
						44c3934daa | |
| 
							
							
								
								 | 
						7f4f7569e5 | |
| 
							
							
								
								 | 
						c33aab0d5e | |
| 
							
							
								
								 | 
						c19400ef66 | |
| 
							
							
								
								 | 
						384f6d8b99 | |
| 
							
							
								
								 | 
						838e025274 | 
| 
						 | 
					@ -1,13 +1,13 @@
 | 
				
			||||||
<!DOCTYPE html>
 | 
					<!DOCTYPE html>
 | 
				
			||||||
<html lang="en">
 | 
					<html lang="en">
 | 
				
			||||||
    <head>
 | 
					  <head>
 | 
				
			||||||
        <meta charset="UTF-8" />
 | 
					    <meta charset="UTF-8" />
 | 
				
			||||||
        <link rel="icon" href="public/favicon.ico" />
 | 
					    <link rel="icon" href="public/favicon.ico" />
 | 
				
			||||||
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
 | 
					    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
 | 
				
			||||||
        <title>Vite + React + TS</title>
 | 
					    <title>Bid System</title>
 | 
				
			||||||
    </head>
 | 
					  </head>
 | 
				
			||||||
    <body>
 | 
					  <body>
 | 
				
			||||||
        <div id="root"></div>
 | 
					    <div id="root"></div>
 | 
				
			||||||
        <script type="module" src="/src/main.tsx"></script>
 | 
					    <script type="module" src="/src/main.tsx"></script>
 | 
				
			||||||
    </body>
 | 
					  </body>
 | 
				
			||||||
</html>
 | 
					</html>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -29,6 +29,8 @@
 | 
				
			||||||
        "react": "^19.0.0",
 | 
					        "react": "^19.0.0",
 | 
				
			||||||
        "react-cropper": "^2.3.3",
 | 
					        "react-cropper": "^2.3.3",
 | 
				
			||||||
        "react-dom": "^19.0.0",
 | 
					        "react-dom": "^19.0.0",
 | 
				
			||||||
 | 
					        "react-json-view": "^1.21.3",
 | 
				
			||||||
 | 
					        "react-player": "^2.16.0",
 | 
				
			||||||
        "react-router": "^6.29.0",
 | 
					        "react-router": "^6.29.0",
 | 
				
			||||||
        "react-router-dom": "^6.29.0",
 | 
					        "react-router-dom": "^6.29.0",
 | 
				
			||||||
        "react-toastify": "^11.0.3",
 | 
					        "react-toastify": "^11.0.3",
 | 
				
			||||||
| 
						 | 
					@ -2106,6 +2108,12 @@
 | 
				
			||||||
        "punycode": "^2.1.0"
 | 
					        "punycode": "^2.1.0"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/asap": {
 | 
				
			||||||
 | 
					      "version": "2.0.6",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==",
 | 
				
			||||||
 | 
					      "license": "MIT"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/asynckit": {
 | 
					    "node_modules/asynckit": {
 | 
				
			||||||
      "version": "0.4.0",
 | 
					      "version": "0.4.0",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
 | 
				
			||||||
| 
						 | 
					@ -2133,6 +2141,12 @@
 | 
				
			||||||
        "url": "https://github.com/sponsors/wooorm"
 | 
					        "url": "https://github.com/sponsors/wooorm"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/base16": {
 | 
				
			||||||
 | 
					      "version": "1.0.0",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/base16/-/base16-1.0.0.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-pNdYkNPiJUnEhnfXV56+sQy8+AaPcG3POZAUnwr4EeqCUZFz4u2PePbo3e5Gj4ziYPCWGUZT9RHisvJKnwFuBQ==",
 | 
				
			||||||
 | 
					      "license": "MIT"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/bcp-47-match": {
 | 
					    "node_modules/bcp-47-match": {
 | 
				
			||||||
      "version": "2.0.3",
 | 
					      "version": "2.0.3",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/bcp-47-match/-/bcp-47-match-2.0.3.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/bcp-47-match/-/bcp-47-match-2.0.3.tgz",
 | 
				
			||||||
| 
						 | 
					@ -2393,6 +2407,15 @@
 | 
				
			||||||
      "integrity": "sha512-nhymn9GdnV3CqiEHJVai54TULFAE3VshJTXSqSJKa8yXAKyBKDWdhHarnlIPrshJ0WMFTGuFvG02YjLXfPiuOA==",
 | 
					      "integrity": "sha512-nhymn9GdnV3CqiEHJVai54TULFAE3VshJTXSqSJKa8yXAKyBKDWdhHarnlIPrshJ0WMFTGuFvG02YjLXfPiuOA==",
 | 
				
			||||||
      "license": "MIT"
 | 
					      "license": "MIT"
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/cross-fetch": {
 | 
				
			||||||
 | 
					      "version": "3.2.0",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.2.0.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==",
 | 
				
			||||||
 | 
					      "license": "MIT",
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "node-fetch": "^2.7.0"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/cross-spawn": {
 | 
					    "node_modules/cross-spawn": {
 | 
				
			||||||
      "version": "7.0.6",
 | 
					      "version": "7.0.6",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
 | 
				
			||||||
| 
						 | 
					@ -2675,6 +2698,15 @@
 | 
				
			||||||
        "url": "https://github.com/sponsors/wooorm"
 | 
					        "url": "https://github.com/sponsors/wooorm"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/deepmerge": {
 | 
				
			||||||
 | 
					      "version": "4.3.1",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
 | 
				
			||||||
 | 
					      "license": "MIT",
 | 
				
			||||||
 | 
					      "engines": {
 | 
				
			||||||
 | 
					        "node": ">=0.10.0"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/delayed-stream": {
 | 
					    "node_modules/delayed-stream": {
 | 
				
			||||||
      "version": "1.0.0",
 | 
					      "version": "1.0.0",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
 | 
				
			||||||
| 
						 | 
					@ -3138,6 +3170,36 @@
 | 
				
			||||||
        "reusify": "^1.0.4"
 | 
					        "reusify": "^1.0.4"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/fbemitter": {
 | 
				
			||||||
 | 
					      "version": "3.0.0",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/fbemitter/-/fbemitter-3.0.0.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-KWKaceCwKQU0+HPoop6gn4eOHk50bBv/VxjJtGMfwmJt3D29JpN4H4eisCtIPA+a8GVBam+ldMMpMjJUvpDyHw==",
 | 
				
			||||||
 | 
					      "license": "BSD-3-Clause",
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "fbjs": "^3.0.0"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/fbjs": {
 | 
				
			||||||
 | 
					      "version": "3.0.5",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-3.0.5.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-ztsSx77JBtkuMrEypfhgc3cI0+0h+svqeie7xHbh1k/IKdcydnvadp/mUaGgjAOXQmQSxsqgaRhS3q9fy+1kxg==",
 | 
				
			||||||
 | 
					      "license": "MIT",
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "cross-fetch": "^3.1.5",
 | 
				
			||||||
 | 
					        "fbjs-css-vars": "^1.0.0",
 | 
				
			||||||
 | 
					        "loose-envify": "^1.0.0",
 | 
				
			||||||
 | 
					        "object-assign": "^4.1.0",
 | 
				
			||||||
 | 
					        "promise": "^7.1.1",
 | 
				
			||||||
 | 
					        "setimmediate": "^1.0.5",
 | 
				
			||||||
 | 
					        "ua-parser-js": "^1.0.35"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/fbjs-css-vars": {
 | 
				
			||||||
 | 
					      "version": "1.0.2",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/fbjs-css-vars/-/fbjs-css-vars-1.0.2.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ==",
 | 
				
			||||||
 | 
					      "license": "MIT"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/file-entry-cache": {
 | 
					    "node_modules/file-entry-cache": {
 | 
				
			||||||
      "version": "8.0.0",
 | 
					      "version": "8.0.0",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
 | 
				
			||||||
| 
						 | 
					@ -4077,12 +4139,30 @@
 | 
				
			||||||
        "url": "https://opencollective.com/parcel"
 | 
					        "url": "https://opencollective.com/parcel"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/load-script": {
 | 
				
			||||||
 | 
					      "version": "1.0.0",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/load-script/-/load-script-1.0.0.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-kPEjMFtZvwL9TaZo0uZ2ml+Ye9HUMmPwbYRJ324qF9tqMejwykJ5ggTyvzmrbBeapCAbk98BSbTeovHEEP1uCA==",
 | 
				
			||||||
 | 
					      "license": "MIT"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/lodash": {
 | 
					    "node_modules/lodash": {
 | 
				
			||||||
      "version": "4.17.21",
 | 
					      "version": "4.17.21",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
 | 
				
			||||||
      "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
 | 
					      "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
 | 
				
			||||||
      "license": "MIT"
 | 
					      "license": "MIT"
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/lodash.curry": {
 | 
				
			||||||
 | 
					      "version": "4.1.1",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/lodash.curry/-/lodash.curry-4.1.1.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-/u14pXGviLaweY5JI0IUzgzF2J6Ne8INyzAZjImcryjgkZ+ebruBxy2/JaOOkTqScddcYtakjhSaeemV8lR0tA==",
 | 
				
			||||||
 | 
					      "license": "MIT"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/lodash.flow": {
 | 
				
			||||||
 | 
					      "version": "3.5.0",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/lodash.flow/-/lodash.flow-3.5.0.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-ff3BX/tSioo+XojX4MOsOMhJw0nZoUEF011LX8g8d3gvjVbxd89cCio4BCXronjxcTUIJUoqKEUA+n4CqvvRPw==",
 | 
				
			||||||
 | 
					      "license": "MIT"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/lodash.merge": {
 | 
					    "node_modules/lodash.merge": {
 | 
				
			||||||
      "version": "4.6.2",
 | 
					      "version": "4.6.2",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
 | 
				
			||||||
| 
						 | 
					@ -4413,6 +4493,12 @@
 | 
				
			||||||
        "url": "https://opencollective.com/unified"
 | 
					        "url": "https://opencollective.com/unified"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/memoize-one": {
 | 
				
			||||||
 | 
					      "version": "5.2.1",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==",
 | 
				
			||||||
 | 
					      "license": "MIT"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/merge2": {
 | 
					    "node_modules/merge2": {
 | 
				
			||||||
      "version": "1.4.1",
 | 
					      "version": "1.4.1",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
 | 
				
			||||||
| 
						 | 
					@ -5099,6 +5185,26 @@
 | 
				
			||||||
      "dev": true,
 | 
					      "dev": true,
 | 
				
			||||||
      "license": "MIT"
 | 
					      "license": "MIT"
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/node-fetch": {
 | 
				
			||||||
 | 
					      "version": "2.7.0",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
 | 
				
			||||||
 | 
					      "license": "MIT",
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "whatwg-url": "^5.0.0"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "engines": {
 | 
				
			||||||
 | 
					        "node": "4.x || >=6.0.0"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "peerDependencies": {
 | 
				
			||||||
 | 
					        "encoding": "^0.1.0"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "peerDependenciesMeta": {
 | 
				
			||||||
 | 
					        "encoding": {
 | 
				
			||||||
 | 
					          "optional": true
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/node-releases": {
 | 
					    "node_modules/node-releases": {
 | 
				
			||||||
      "version": "2.0.19",
 | 
					      "version": "2.0.19",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
 | 
				
			||||||
| 
						 | 
					@ -5409,6 +5515,15 @@
 | 
				
			||||||
        "postcss": "^8.2.1"
 | 
					        "postcss": "^8.2.1"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/promise": {
 | 
				
			||||||
 | 
					      "version": "7.3.1",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==",
 | 
				
			||||||
 | 
					      "license": "MIT",
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "asap": "~2.0.3"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/prop-types": {
 | 
					    "node_modules/prop-types": {
 | 
				
			||||||
      "version": "15.8.1",
 | 
					      "version": "15.8.1",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
 | 
				
			||||||
| 
						 | 
					@ -5436,6 +5551,12 @@
 | 
				
			||||||
      "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
 | 
					      "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
 | 
				
			||||||
      "license": "MIT"
 | 
					      "license": "MIT"
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/pure-color": {
 | 
				
			||||||
 | 
					      "version": "1.3.0",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/pure-color/-/pure-color-1.3.0.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-QFADYnsVoBMw1srW7OVKEYjG+MbIa49s54w1MA1EDY6r2r/sTcKKYqRX1f4GYvnXP7eN/Pe9HFcX+hwzmrXRHA==",
 | 
				
			||||||
 | 
					      "license": "MIT"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/queue-microtask": {
 | 
					    "node_modules/queue-microtask": {
 | 
				
			||||||
      "version": "1.2.3",
 | 
					      "version": "1.2.3",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
 | 
				
			||||||
| 
						 | 
					@ -5466,6 +5587,18 @@
 | 
				
			||||||
        "node": ">=0.10.0"
 | 
					        "node": ">=0.10.0"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/react-base16-styling": {
 | 
				
			||||||
 | 
					      "version": "0.6.0",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/react-base16-styling/-/react-base16-styling-0.6.0.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-yvh/7CArceR/jNATXOKDlvTnPKPmGZz7zsenQ3jUwLzHkNUR0CvY3yGYJbWJ/nnxsL8Sgmt5cO3/SILVuPO6TQ==",
 | 
				
			||||||
 | 
					      "license": "MIT",
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "base16": "^1.0.0",
 | 
				
			||||||
 | 
					        "lodash.curry": "^4.0.1",
 | 
				
			||||||
 | 
					        "lodash.flow": "^3.3.0",
 | 
				
			||||||
 | 
					        "pure-color": "^1.2.0"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/react-cropper": {
 | 
					    "node_modules/react-cropper": {
 | 
				
			||||||
      "version": "2.3.3",
 | 
					      "version": "2.3.3",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/react-cropper/-/react-cropper-2.3.3.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/react-cropper/-/react-cropper-2.3.3.tgz",
 | 
				
			||||||
| 
						 | 
					@ -5505,12 +5638,53 @@
 | 
				
			||||||
        "react": ">= 16.8 || 18.0.0"
 | 
					        "react": ">= 16.8 || 18.0.0"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/react-fast-compare": {
 | 
				
			||||||
 | 
					      "version": "3.2.2",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==",
 | 
				
			||||||
 | 
					      "license": "MIT"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/react-is": {
 | 
					    "node_modules/react-is": {
 | 
				
			||||||
      "version": "16.13.1",
 | 
					      "version": "16.13.1",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
 | 
				
			||||||
      "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
 | 
					      "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
 | 
				
			||||||
      "license": "MIT"
 | 
					      "license": "MIT"
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/react-json-view": {
 | 
				
			||||||
 | 
					      "version": "1.21.3",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/react-json-view/-/react-json-view-1.21.3.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-13p8IREj9/x/Ye4WI/JpjhoIwuzEgUAtgJZNBJckfzJt1qyh24BdTm6UQNGnyTq9dapQdrqvquZTo3dz1X6Cjw==",
 | 
				
			||||||
 | 
					      "license": "MIT",
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "flux": "^4.0.1",
 | 
				
			||||||
 | 
					        "react-base16-styling": "^0.6.0",
 | 
				
			||||||
 | 
					        "react-lifecycles-compat": "^3.0.4",
 | 
				
			||||||
 | 
					        "react-textarea-autosize": "^8.3.2"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "peerDependencies": {
 | 
				
			||||||
 | 
					        "react": "^17.0.0 || ^16.3.0 || ^15.5.4",
 | 
				
			||||||
 | 
					        "react-dom": "^17.0.0 || ^16.3.0 || ^15.5.4"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/react-json-view/node_modules/flux": {
 | 
				
			||||||
 | 
					      "version": "4.0.4",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/flux/-/flux-4.0.4.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-NCj3XlayA2UsapRpM7va6wU1+9rE5FIL7qoMcmxWHRzbp0yujihMBm9BBHZ1MDIk5h5o2Bl6eGiCe8rYELAmYw==",
 | 
				
			||||||
 | 
					      "license": "BSD-3-Clause",
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "fbemitter": "^3.0.0",
 | 
				
			||||||
 | 
					        "fbjs": "^3.0.1"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "peerDependencies": {
 | 
				
			||||||
 | 
					        "react": "^15.0.2 || ^16.0.0 || ^17.0.0"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/react-lifecycles-compat": {
 | 
				
			||||||
 | 
					      "version": "3.0.4",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==",
 | 
				
			||||||
 | 
					      "license": "MIT"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/react-markdown": {
 | 
					    "node_modules/react-markdown": {
 | 
				
			||||||
      "version": "9.0.3",
 | 
					      "version": "9.0.3",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-9.0.3.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-9.0.3.tgz",
 | 
				
			||||||
| 
						 | 
					@ -5547,6 +5721,22 @@
 | 
				
			||||||
        "react-dom": "^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
 | 
					        "react-dom": "^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/react-player": {
 | 
				
			||||||
 | 
					      "version": "2.16.0",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/react-player/-/react-player-2.16.0.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-mAIPHfioD7yxO0GNYVFD1303QFtI3lyyQZLY229UEAp/a10cSW+hPcakg0Keq8uWJxT2OiT/4Gt+Lc9bD6bJmQ==",
 | 
				
			||||||
 | 
					      "license": "MIT",
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "deepmerge": "^4.0.0",
 | 
				
			||||||
 | 
					        "load-script": "^1.0.0",
 | 
				
			||||||
 | 
					        "memoize-one": "^5.1.1",
 | 
				
			||||||
 | 
					        "prop-types": "^15.7.2",
 | 
				
			||||||
 | 
					        "react-fast-compare": "^3.0.1"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "peerDependencies": {
 | 
				
			||||||
 | 
					        "react": ">=16.6.0"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/react-refresh": {
 | 
					    "node_modules/react-refresh": {
 | 
				
			||||||
      "version": "0.14.2",
 | 
					      "version": "0.14.2",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",
 | 
				
			||||||
| 
						 | 
					@ -6139,6 +6329,12 @@
 | 
				
			||||||
        "semver": "bin/semver.js"
 | 
					        "semver": "bin/semver.js"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/setimmediate": {
 | 
				
			||||||
 | 
					      "version": "1.0.5",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
 | 
				
			||||||
 | 
					      "license": "MIT"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/socket.io-client": {
 | 
					    "node_modules/socket.io-client": {
 | 
				
			||||||
      "version": "4.8.1",
 | 
					      "version": "4.8.1",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz",
 | 
				
			||||||
| 
						 | 
					@ -6311,6 +6507,12 @@
 | 
				
			||||||
        "node": ">=8.0"
 | 
					        "node": ">=8.0"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/tr46": {
 | 
				
			||||||
 | 
					      "version": "0.0.3",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
 | 
				
			||||||
 | 
					      "license": "MIT"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/trim-lines": {
 | 
					    "node_modules/trim-lines": {
 | 
				
			||||||
      "version": "3.0.1",
 | 
					      "version": "3.0.1",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
 | 
				
			||||||
| 
						 | 
					@ -6386,6 +6588,32 @@
 | 
				
			||||||
        "typescript": ">=4.8.4 <5.8.0"
 | 
					        "typescript": ">=4.8.4 <5.8.0"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/ua-parser-js": {
 | 
				
			||||||
 | 
					      "version": "1.0.40",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.40.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-z6PJ8Lml+v3ichVojCiB8toQJBuwR42ySM4ezjXIqXK3M0HczmKQ3LF4rhU55PfD99KEEXQG6yb7iOMyvYuHew==",
 | 
				
			||||||
 | 
					      "funding": [
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          "type": "opencollective",
 | 
				
			||||||
 | 
					          "url": "https://opencollective.com/ua-parser-js"
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          "type": "paypal",
 | 
				
			||||||
 | 
					          "url": "https://paypal.me/faisalman"
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          "type": "github",
 | 
				
			||||||
 | 
					          "url": "https://github.com/sponsors/faisalman"
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "license": "MIT",
 | 
				
			||||||
 | 
					      "bin": {
 | 
				
			||||||
 | 
					        "ua-parser-js": "script/cli.js"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "engines": {
 | 
				
			||||||
 | 
					        "node": "*"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/unified": {
 | 
					    "node_modules/unified": {
 | 
				
			||||||
      "version": "11.0.5",
 | 
					      "version": "11.0.5",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz",
 | 
				
			||||||
| 
						 | 
					@ -7507,6 +7735,22 @@
 | 
				
			||||||
        "url": "https://github.com/sponsors/wooorm"
 | 
					        "url": "https://github.com/sponsors/wooorm"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/webidl-conversions": {
 | 
				
			||||||
 | 
					      "version": "3.0.1",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
 | 
				
			||||||
 | 
					      "license": "BSD-2-Clause"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/whatwg-url": {
 | 
				
			||||||
 | 
					      "version": "5.0.0",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
 | 
				
			||||||
 | 
					      "license": "MIT",
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "tr46": "~0.0.3",
 | 
				
			||||||
 | 
					        "webidl-conversions": "^3.0.0"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/ws": {
 | 
					    "node_modules/ws": {
 | 
				
			||||||
      "version": "8.17.1",
 | 
					      "version": "8.17.1",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -31,6 +31,8 @@
 | 
				
			||||||
    "react": "^19.0.0",
 | 
					    "react": "^19.0.0",
 | 
				
			||||||
    "react-cropper": "^2.3.3",
 | 
					    "react-cropper": "^2.3.3",
 | 
				
			||||||
    "react-dom": "^19.0.0",
 | 
					    "react-dom": "^19.0.0",
 | 
				
			||||||
 | 
					    "react-json-view": "^1.21.3",
 | 
				
			||||||
 | 
					    "react-player": "^2.16.0",
 | 
				
			||||||
    "react-router": "^6.29.0",
 | 
					    "react-router": "^6.29.0",
 | 
				
			||||||
    "react-router-dom": "^6.29.0",
 | 
					    "react-router-dom": "^6.29.0",
 | 
				
			||||||
    "react-toastify": "^11.0.3",
 | 
					    "react-toastify": "^11.0.3",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,120 +1,187 @@
 | 
				
			||||||
import { generateNestParams, handleError, handleSuccess } from '.';
 | 
					import { generateNestParams, handleError, handleSuccess } from ".";
 | 
				
			||||||
import axios from '../lib/axios';
 | 
					import axios from "../lib/axios";
 | 
				
			||||||
import { IBid, IWebBid } from '../system/type';
 | 
					import { IBid, IMetadata, IWebBid } from "../system/type";
 | 
				
			||||||
import { removeFalsyValues } from '../utils';
 | 
					import { removeFalsyValues } from "../utils";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const getBids = async (params: Record<string, string | number>) => {
 | 
					export const getBids = async (params: Record<string, string | number>) => {
 | 
				
			||||||
    return await axios({
 | 
					  return await axios({
 | 
				
			||||||
        url: 'bids',
 | 
					    url: "bids",
 | 
				
			||||||
        params: generateNestParams(params),
 | 
					    params: generateNestParams(params),
 | 
				
			||||||
        withCredentials: true,
 | 
					    withCredentials: true,
 | 
				
			||||||
        method: 'GET',
 | 
					    method: "GET",
 | 
				
			||||||
    });
 | 
					  });
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const createBid = async (bid: Omit<IBid, 'id' | 'created_at' | 'updated_at' | 'is_system_account'>) => {
 | 
					export const createBid = async (
 | 
				
			||||||
    const newData = removeFalsyValues(bid);
 | 
					  bid: Omit<IBid, "id" | "created_at" | "updated_at" | "is_system_account">
 | 
				
			||||||
 | 
					) => {
 | 
				
			||||||
 | 
					  const newData = removeFalsyValues(bid);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    try {
 | 
					  try {
 | 
				
			||||||
        const { data } = await axios({
 | 
					    const { data } = await axios({
 | 
				
			||||||
            url: 'bids',
 | 
					      url: "bids",
 | 
				
			||||||
            withCredentials: true,
 | 
					      withCredentials: true,
 | 
				
			||||||
            method: 'POST',
 | 
					      method: "POST",
 | 
				
			||||||
            data: newData,
 | 
					      data: newData,
 | 
				
			||||||
        });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        handleSuccess(data);
 | 
					    handleSuccess(data);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return data;
 | 
					    return data;
 | 
				
			||||||
    } catch (error) {
 | 
					  } catch (error) {
 | 
				
			||||||
        handleError(error);
 | 
					    handleError(error);
 | 
				
			||||||
    }
 | 
					  }
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const updateBid = async (bid: Partial<IBid>) => {
 | 
					export const updateBid = async (bid: Partial<IBid>) => {
 | 
				
			||||||
    const { plus_price, max_price, quantity } = removeFalsyValues(bid, ['plus_price']);
 | 
					  const { plus_price, max_price, quantity, metadata } = removeFalsyValues(bid, [
 | 
				
			||||||
 | 
					    "plus_price",
 | 
				
			||||||
 | 
					  ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    try {
 | 
					  try {
 | 
				
			||||||
        const { data } = await axios({
 | 
					    const { data } = await axios({
 | 
				
			||||||
            url: 'bids/' + bid.id,
 | 
					      url: "bids/" + bid.id,
 | 
				
			||||||
            withCredentials: true,
 | 
					      withCredentials: true,
 | 
				
			||||||
            method: 'PUT',
 | 
					      method: "PUT",
 | 
				
			||||||
            data: { plus_price, max_price, quantity },
 | 
					      data: { plus_price, max_price, quantity, metadata },
 | 
				
			||||||
        });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        handleSuccess(data);
 | 
					    handleSuccess(data);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return data;
 | 
					    return data;
 | 
				
			||||||
    } catch (error) {
 | 
					  } catch (error) {
 | 
				
			||||||
        handleError(error);
 | 
					    handleError(error);
 | 
				
			||||||
    }
 | 
					  }
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const toggleBid = async (bid: Partial<IBid>) => {
 | 
					export const toggleBid = async (bid: Partial<IBid>) => {
 | 
				
			||||||
    try {
 | 
					  try {
 | 
				
			||||||
        const { data } = await axios({
 | 
					    const { data } = await axios({
 | 
				
			||||||
            url: 'bids/toggle/' + bid.id,
 | 
					      url: "bids/toggle/" + bid.id,
 | 
				
			||||||
            withCredentials: true,
 | 
					      withCredentials: true,
 | 
				
			||||||
            method: 'POST',
 | 
					      method: "POST",
 | 
				
			||||||
        });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        handleSuccess(data);
 | 
					    handleSuccess(data);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return data;
 | 
					    return data;
 | 
				
			||||||
    } catch (error) {
 | 
					  } catch (error) {
 | 
				
			||||||
        handleError(error);
 | 
					    handleError(error);
 | 
				
			||||||
    }
 | 
					  }
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const deleteBid = async (bid: IBid) => {
 | 
					export const deleteBid = async (bid: IBid) => {
 | 
				
			||||||
    try {
 | 
					  try {
 | 
				
			||||||
        const { data } = await axios({
 | 
					    const { data } = await axios({
 | 
				
			||||||
            url: 'bids/' + bid.id,
 | 
					      url: "bids/" + bid.id,
 | 
				
			||||||
            withCredentials: true,
 | 
					      withCredentials: true,
 | 
				
			||||||
            method: 'DELETE',
 | 
					      method: "DELETE",
 | 
				
			||||||
        });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        handleSuccess(data);
 | 
					    handleSuccess(data);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return data;
 | 
					    return data;
 | 
				
			||||||
    } catch (error) {
 | 
					  } catch (error) {
 | 
				
			||||||
        handleError(error);
 | 
					    handleError(error);
 | 
				
			||||||
    }
 | 
					  }
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const deletesBid = async (bids: IBid[]) => {
 | 
					export const deletesBid = async (bids: IBid[]) => {
 | 
				
			||||||
    const ids = bids.reduce((prev, cur) => {
 | 
					  const ids = bids.reduce((prev, cur) => {
 | 
				
			||||||
        prev.push(cur.id);
 | 
					    prev.push(cur.id);
 | 
				
			||||||
        return prev;
 | 
					    return prev;
 | 
				
			||||||
    }, [] as number[]);
 | 
					  }, [] as number[]);
 | 
				
			||||||
    try {
 | 
					  try {
 | 
				
			||||||
        const { data } = await axios({
 | 
					    const { data } = await axios({
 | 
				
			||||||
            url: 'bids/deletes',
 | 
					      url: "bids/deletes",
 | 
				
			||||||
            withCredentials: true,
 | 
					      withCredentials: true,
 | 
				
			||||||
            method: 'POST',
 | 
					      method: "POST",
 | 
				
			||||||
            data: {
 | 
					      data: {
 | 
				
			||||||
                ids,
 | 
					        ids,
 | 
				
			||||||
            },
 | 
					      },
 | 
				
			||||||
        });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        handleSuccess(data);
 | 
					    handleSuccess(data);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return data;
 | 
					    return data;
 | 
				
			||||||
    } catch (error) {
 | 
					  } catch (error) {
 | 
				
			||||||
        handleError(error);
 | 
					    handleError(error);
 | 
				
			||||||
    }
 | 
					  }
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const getImagesWorking = async (values: (IBid | IWebBid) & { type: string }) => {
 | 
					export const getImagesWorking = async (
 | 
				
			||||||
    try {
 | 
					  values: (IBid | IWebBid) & { type: string }
 | 
				
			||||||
        const { data } = await axios({
 | 
					) => {
 | 
				
			||||||
            url: `bids/images-working/${values.type.toLocaleLowerCase().replace('_', '-')}/${values.id}`,
 | 
					  try {
 | 
				
			||||||
            withCredentials: true,
 | 
					    const { data } = await axios({
 | 
				
			||||||
            method: 'GET',
 | 
					      url: `bids/images-working/${values.type
 | 
				
			||||||
        });
 | 
					        .toLocaleLowerCase()
 | 
				
			||||||
 | 
					        .replace("_", "-")}/${values.id}`,
 | 
				
			||||||
 | 
					      withCredentials: true,
 | 
				
			||||||
 | 
					      method: "GET",
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return data;
 | 
					    return data;
 | 
				
			||||||
    } catch (error) {
 | 
					  } catch (error) {
 | 
				
			||||||
        console.log('%csrc/apis/bid.ts:118 error', 'color: #007acc;', error);
 | 
					    console.log("%csrc/apis/bid.ts:118 error", "color: #007acc;", error);
 | 
				
			||||||
    }
 | 
					  }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const getRecords = async (values: IBid) => {
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const { data } = await axios({
 | 
				
			||||||
 | 
					      url: `bids/records/${values.id}`,
 | 
				
			||||||
 | 
					      withCredentials: true,
 | 
				
			||||||
 | 
					      method: "GET",
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return data;
 | 
				
			||||||
 | 
					  } catch (error) {
 | 
				
			||||||
 | 
					    console.log("%csrc/apis/bid.ts:118 error", "color: #007acc;", error);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const deleteRecord = async (name: string) => {
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const { data } = await axios({
 | 
				
			||||||
 | 
					      url: `bids/record/${name}`,
 | 
				
			||||||
 | 
					      withCredentials: true,
 | 
				
			||||||
 | 
					      method: "DELETE",
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return data;
 | 
				
			||||||
 | 
					  } catch (error) {
 | 
				
			||||||
 | 
					    console.log("%csrc/apis/bid.ts:154 error", "color: #007acc;", error);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const deleteMetadata = async (id: IMetadata["id"]) => {
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const { data } = await axios({
 | 
				
			||||||
 | 
					      url: `bid-metadatas/${id}`,
 | 
				
			||||||
 | 
					      withCredentials: true,
 | 
				
			||||||
 | 
					      method: "DELETE",
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return data;
 | 
				
			||||||
 | 
					  } catch (error) {
 | 
				
			||||||
 | 
					    console.log("%csrc/apis/bid.ts:154 error", "color: #007acc;", error);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const updateMetadata = async (id: IMetadata["id"], value: string) => {
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const { data } = await axios({
 | 
				
			||||||
 | 
					      url: `bid-metadatas/${id}`,
 | 
				
			||||||
 | 
					      withCredentials: true,
 | 
				
			||||||
 | 
					      method: "PUT",
 | 
				
			||||||
 | 
					      data: {
 | 
				
			||||||
 | 
					        value,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return data;
 | 
				
			||||||
 | 
					  } catch (error) {
 | 
				
			||||||
 | 
					    console.log("%csrc/apis/bid.ts:154 error", "color: #007acc;", error);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,33 @@
 | 
				
			||||||
 | 
					import { handleError } from ".";
 | 
				
			||||||
 | 
					import axios from "../lib/axios";
 | 
				
			||||||
 | 
					import { IConfig } from "../system/type";
 | 
				
			||||||
 | 
					import { removeFalsyValues } from "../utils";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const CONFIG_KEYS = {
 | 
				
			||||||
 | 
					  MAIL_SCRAP_REPORT: "MAIL_SCRAP_REPORT",
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const getConfig = async (key: keyof typeof CONFIG_KEYS) => {
 | 
				
			||||||
 | 
					  return await axios({
 | 
				
			||||||
 | 
					    url: "configs/" + key,
 | 
				
			||||||
 | 
					    withCredentials: true,
 | 
				
			||||||
 | 
					    method: "GET",
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const upsertConfig = async (data: IConfig) => {
 | 
				
			||||||
 | 
					  const { key_name, value, type } = removeFalsyValues(data, ["value"]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const { data } = await axios({
 | 
				
			||||||
 | 
					      url: "configs/upsert",
 | 
				
			||||||
 | 
					      withCredentials: true,
 | 
				
			||||||
 | 
					      method: "POST",
 | 
				
			||||||
 | 
					      data: { key_name, value: value || null, type },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return data;
 | 
				
			||||||
 | 
					  } catch (error) {
 | 
				
			||||||
 | 
					    handleError(error);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,50 @@
 | 
				
			||||||
 | 
					import { handleError, handleSuccess } from ".";
 | 
				
			||||||
 | 
					import axios from "../lib/axios";
 | 
				
			||||||
 | 
					import { IScrapConfig, IWebBid } from "../system/type";
 | 
				
			||||||
 | 
					import { removeFalsyValues } from "../utils";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const createScrapConfig = async (
 | 
				
			||||||
 | 
					  bid: Omit<
 | 
				
			||||||
 | 
					    IScrapConfig,
 | 
				
			||||||
 | 
					    "id" | "created_at" | "updated_at" | "scrap_items"
 | 
				
			||||||
 | 
					  > & { web_id: IWebBid["id"] }
 | 
				
			||||||
 | 
					) => {
 | 
				
			||||||
 | 
					  const newData = removeFalsyValues(bid);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const { data } = await axios({
 | 
				
			||||||
 | 
					      url: "scrap-configs",
 | 
				
			||||||
 | 
					      withCredentials: true,
 | 
				
			||||||
 | 
					      method: "POST",
 | 
				
			||||||
 | 
					      data: {
 | 
				
			||||||
 | 
					        ...newData,
 | 
				
			||||||
 | 
					        enable: newData.enable === "1",
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    handleSuccess(data);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return data;
 | 
				
			||||||
 | 
					  } catch (error) {
 | 
				
			||||||
 | 
					    handleError(error);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const updateScrapConfig = async (scrapConfig: Partial<IScrapConfig>) => {
 | 
				
			||||||
 | 
					  const { search_url, keywords, id, enable } = removeFalsyValues(scrapConfig);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const { data } = await axios({
 | 
				
			||||||
 | 
					      url: "scrap-configs/" + id,
 | 
				
			||||||
 | 
					      withCredentials: true,
 | 
				
			||||||
 | 
					      method: "PUT",
 | 
				
			||||||
 | 
					      data: { search_url, keywords, enable: enable === "1" },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    handleSuccess(data);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return data;
 | 
				
			||||||
 | 
					  } catch (error) {
 | 
				
			||||||
 | 
					    handleError(error);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
| 
						 | 
					@ -1,117 +1,357 @@
 | 
				
			||||||
 | 
					/* eslint-disable @typescript-eslint/no-explicit-any */
 | 
				
			||||||
/* eslint-disable @typescript-eslint/no-unused-vars */
 | 
					/* eslint-disable @typescript-eslint/no-unused-vars */
 | 
				
			||||||
import { Button, LoadingOverlay, Modal, ModalProps, NumberInput, TextInput } from '@mantine/core';
 | 
					import {
 | 
				
			||||||
import { useForm, zodResolver } from '@mantine/form';
 | 
					  Button,
 | 
				
			||||||
import _ from 'lodash';
 | 
					  LoadingOverlay,
 | 
				
			||||||
import { useEffect, useRef, useState } from 'react';
 | 
					  Modal,
 | 
				
			||||||
import { z } from 'zod';
 | 
					  ModalProps,
 | 
				
			||||||
import { createBid, updateBid } from '../../apis/bid';
 | 
					  NumberInput,
 | 
				
			||||||
import { useConfirmStore } from '../../lib/zustand/use-confirm';
 | 
					  Select,
 | 
				
			||||||
import { IBid } from '../../system/type';
 | 
					  TextInput,
 | 
				
			||||||
 | 
					} from "@mantine/core";
 | 
				
			||||||
 | 
					import { useForm, zodResolver } from "@mantine/form";
 | 
				
			||||||
 | 
					import _ from "lodash";
 | 
				
			||||||
 | 
					import { useEffect, useRef, useState } from "react";
 | 
				
			||||||
 | 
					import { z } from "zod";
 | 
				
			||||||
 | 
					import { createBid, updateBid } from "../../apis/bid";
 | 
				
			||||||
 | 
					import { useConfirmStore } from "../../lib/zustand/use-confirm";
 | 
				
			||||||
 | 
					import { IBid, IMetadata } from "../../system/type";
 | 
				
			||||||
 | 
					import { formatTimeFromMinutes } from "../../utils";
 | 
				
			||||||
export interface IBidModelProps extends ModalProps {
 | 
					export interface IBidModelProps extends ModalProps {
 | 
				
			||||||
    data: IBid | null;
 | 
					  data: IBid | null;
 | 
				
			||||||
    onUpdated?: () => void;
 | 
					  onUpdated?: () => void;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const schema = {
 | 
					const schema = {
 | 
				
			||||||
    url: z.string({ message: 'Url is required' }).url('Invalid url format'),
 | 
					  url: z.string({ message: "Url is required" }).url("Invalid url format"),
 | 
				
			||||||
    max_price: z.number({ message: 'Max price is required' }).min(1, 'Max price must be at least 1'),
 | 
					  max_price: z
 | 
				
			||||||
    plus_price: z.number().min(0, 'Plus price must be at least 1').optional(),
 | 
					    .number({ message: "Max price is required" })
 | 
				
			||||||
    quantity: z.number().min(1, 'Quantity must be at least 1').optional(),
 | 
					    .min(1, "Max price must be at least 1"),
 | 
				
			||||||
 | 
					  plus_price: z.number().min(0, "Plus price must be at least 1").optional(),
 | 
				
			||||||
 | 
					  quantity: z.number().min(1, "Quantity must be at least 1").optional(),
 | 
				
			||||||
 | 
					  arrival_offset_seconds_live: z
 | 
				
			||||||
 | 
					    .number({ message: "Arrival offset seconds is required" })
 | 
				
			||||||
 | 
					    .refine((val) => val >= 60, {
 | 
				
			||||||
 | 
					      message: "Arrival offset seconds must be at least 60 seconds (1 minute)",
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    .optional(),
 | 
				
			||||||
 | 
					  early_tracking_seconds_live: z
 | 
				
			||||||
 | 
					    .number({ message: "Early login seconds is required" })
 | 
				
			||||||
 | 
					    .refine((val) => val >= 600, {
 | 
				
			||||||
 | 
					      message: "Early login seconds must be at least 600 seconds (10 minute)",
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    .optional(),
 | 
				
			||||||
 | 
					  arrival_offset_seconds_sandbox: z
 | 
				
			||||||
 | 
					    .number({ message: "Arrival offset seconds is required" })
 | 
				
			||||||
 | 
					    .refine((val) => val >= 60, {
 | 
				
			||||||
 | 
					      message: "Arrival offset seconds must be at least 60 seconds (1 minute)",
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    .optional(),
 | 
				
			||||||
 | 
					  early_tracking_seconds_sandbox: z
 | 
				
			||||||
 | 
					    .number({ message: "Early login seconds is required" })
 | 
				
			||||||
 | 
					    .refine((val) => val >= 600, {
 | 
				
			||||||
 | 
					      message: "Early login seconds must be at least 600 seconds (10 minute)",
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    .optional(),
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default function BidModal({ data, onUpdated, ...props }: IBidModelProps) {
 | 
					export default function BidModal({
 | 
				
			||||||
    const form = useForm({
 | 
					  data,
 | 
				
			||||||
        validate: zodResolver(z.object(schema)),
 | 
					  onUpdated,
 | 
				
			||||||
 | 
					  ...props
 | 
				
			||||||
 | 
					}: IBidModelProps) {
 | 
				
			||||||
 | 
					  const form = useForm({
 | 
				
			||||||
 | 
					    validate: zodResolver(z.object(schema)),
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const prevData = useRef<IBid | null>(data);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const { setConfirm } = useConfirmStore();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const [loading, setLoading] = useState(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleSubmit = async (values: typeof form.values) => {
 | 
				
			||||||
 | 
					    if (data) {
 | 
				
			||||||
 | 
					      setConfirm({
 | 
				
			||||||
 | 
					        title: "Update ?",
 | 
				
			||||||
 | 
					        message: `This product will be update`,
 | 
				
			||||||
 | 
					        handleOk: async () => {
 | 
				
			||||||
 | 
					          setLoading(true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          const metadata = valuesToMetadata(
 | 
				
			||||||
 | 
					            values as IBid & Record<string, any>
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          const result = await updateBid({ ...values, metadata });
 | 
				
			||||||
 | 
					          setLoading(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          if (!result) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          props.onClose();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          if (onUpdated) {
 | 
				
			||||||
 | 
					            onUpdated();
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        okButton: {
 | 
				
			||||||
 | 
					          color: "blue",
 | 
				
			||||||
 | 
					          value: "Update",
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      const { url, max_price, plus_price } = values;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      setLoading(true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const metadata = valuesToMetadata(values as IBid & Record<string, any>);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const result = await createBid({
 | 
				
			||||||
 | 
					        url,
 | 
				
			||||||
 | 
					        max_price,
 | 
				
			||||||
 | 
					        plus_price,
 | 
				
			||||||
 | 
					        metadata,
 | 
				
			||||||
 | 
					      } as IBid);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      setLoading(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (!result) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      props.onClose();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (onUpdated) {
 | 
				
			||||||
 | 
					        onUpdated();
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const mappingValues = (ignore: string[] = []) => {
 | 
				
			||||||
 | 
					    if (!data) return {};
 | 
				
			||||||
 | 
					    let values: IBid & Record<string, any> = data;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const followKey = ["arrival_offset_seconds", "early_tracking_seconds"];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (
 | 
				
			||||||
 | 
					      data.metadata.length &&
 | 
				
			||||||
 | 
					      data.metadata.some((item) => item.key_name === "mode_key")
 | 
				
			||||||
 | 
					    ) {
 | 
				
			||||||
 | 
					      data.metadata.reduce((prev, cur) => {
 | 
				
			||||||
 | 
					        if (ignore.includes(cur.key_name)) {
 | 
				
			||||||
 | 
					          prev[cur.key_name as string] = form.values[cur.key_name];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          return prev;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (cur.key_name === "mode_key") {
 | 
				
			||||||
 | 
					          prev[cur.key_name as string] = cur.value;
 | 
				
			||||||
 | 
					          return prev;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        prev[cur.key_name as string] = cur.value;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return prev;
 | 
				
			||||||
 | 
					      }, values);
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      const metadata = Object.assign(
 | 
				
			||||||
 | 
					        { mode_key: "live" },
 | 
				
			||||||
 | 
					        ...followKey.map((item) => ({
 | 
				
			||||||
 | 
					          [`${item}_live`]: data.web_bid[item as keyof typeof data.web_bid],
 | 
				
			||||||
 | 
					          [`${item}_sandbox`]: data.web_bid[item as keyof typeof data.web_bid],
 | 
				
			||||||
 | 
					        }))
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      values = { ...values, ...metadata };
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return values;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const valuesToMetadata = (values: IBid & Record<string, any>) => {
 | 
				
			||||||
 | 
					    const keys = [
 | 
				
			||||||
 | 
					      "mode_key",
 | 
				
			||||||
 | 
					      "arrival_offset_seconds_live",
 | 
				
			||||||
 | 
					      "arrival_offset_seconds_sandbox",
 | 
				
			||||||
 | 
					      "early_tracking_seconds_live",
 | 
				
			||||||
 | 
					      "early_tracking_seconds_sandbox",
 | 
				
			||||||
 | 
					    ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!values?.metadata) {
 | 
				
			||||||
 | 
					      const mode_key = values.mode_key;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (!mode_key) return [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const newValues = Object.entries(values)
 | 
				
			||||||
 | 
					        .map(([key, value]) => {
 | 
				
			||||||
 | 
					          if (keys.includes(key)) {
 | 
				
			||||||
 | 
					            return {
 | 
				
			||||||
 | 
					              key_name: key,
 | 
				
			||||||
 | 
					              value,
 | 
				
			||||||
 | 
					            } as IMetadata;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          return null;
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        .filter((i) => i !== null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return newValues;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (values.metadata.length <= 0) {
 | 
				
			||||||
 | 
					      return keys.map((item) => {
 | 
				
			||||||
 | 
					        return {
 | 
				
			||||||
 | 
					          key_name: item,
 | 
				
			||||||
 | 
					          value: values[item],
 | 
				
			||||||
 | 
					        } as IMetadata;
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return values.metadata.map((item) => {
 | 
				
			||||||
 | 
					      return {
 | 
				
			||||||
 | 
					        ...item,
 | 
				
			||||||
 | 
					        value: values[item.key_name],
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const prevData = useRef<IBid | null>(data);
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    form.reset();
 | 
				
			||||||
 | 
					    if (!data) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const { setConfirm } = useConfirmStore();
 | 
					    const values = mappingValues();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const [loading, setLoading] = useState(false);
 | 
					    form.setValues(values);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const handleSubmit = async (values: typeof form.values) => {
 | 
					    prevData.current = data;
 | 
				
			||||||
        if (data) {
 | 
					    // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
				
			||||||
            setConfirm({
 | 
					  }, [data]);
 | 
				
			||||||
                title: 'Update ?',
 | 
					 | 
				
			||||||
                message: `This product will be update`,
 | 
					 | 
				
			||||||
                handleOk: async () => {
 | 
					 | 
				
			||||||
                    setLoading(true);
 | 
					 | 
				
			||||||
                    const result = await updateBid(values);
 | 
					 | 
				
			||||||
                    setLoading(false);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    if (!result) return;
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    if (!props.opened) {
 | 
				
			||||||
 | 
					      form.reset();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
				
			||||||
 | 
					  }, [props.opened]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    props.onClose();
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    if (!data && !form.values.mode_key) {
 | 
				
			||||||
 | 
					      form.setValues({ mode_key: "live" });
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      const values = mappingValues(["mode_key"]);
 | 
				
			||||||
 | 
					      form.setValues(values);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    if (onUpdated) {
 | 
					    prevData.current = data;
 | 
				
			||||||
                        onUpdated();
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                },
 | 
					 | 
				
			||||||
                okButton: {
 | 
					 | 
				
			||||||
                    color: 'blue',
 | 
					 | 
				
			||||||
                    value: 'Update',
 | 
					 | 
				
			||||||
                },
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            const { url, max_price, plus_price } = values;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
            setLoading(true);
 | 
					    // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
				
			||||||
            const result = await createBid({ url, max_price, plus_price } as IBid);
 | 
					  }, [form.values["mode_key"]]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            setLoading(false);
 | 
					  return (
 | 
				
			||||||
 | 
					    <Modal
 | 
				
			||||||
 | 
					      className="relative"
 | 
				
			||||||
 | 
					      classNames={{
 | 
				
			||||||
 | 
					        header: "!flex !item-center !justify-center w-full",
 | 
				
			||||||
 | 
					      }}
 | 
				
			||||||
 | 
					      {...props}
 | 
				
			||||||
 | 
					      size={"xl"}
 | 
				
			||||||
 | 
					      title={<span className="text-xl font-bold">Bid</span>}
 | 
				
			||||||
 | 
					      centered
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <form
 | 
				
			||||||
 | 
					        onSubmit={form.onSubmit(handleSubmit)}
 | 
				
			||||||
 | 
					        className="grid grid-cols-2 gap-2.5"
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <Select
 | 
				
			||||||
 | 
					          className="col-span-2"
 | 
				
			||||||
 | 
					          label="Mode"
 | 
				
			||||||
 | 
					          data={[
 | 
				
			||||||
 | 
					            { label: "Live", value: "live" },
 | 
				
			||||||
 | 
					            { label: "Sandbox", value: "sandbox" },
 | 
				
			||||||
 | 
					          ]}
 | 
				
			||||||
 | 
					          defaultValue="live"
 | 
				
			||||||
 | 
					          checkIconPosition="right"
 | 
				
			||||||
 | 
					          allowDeselect={false}
 | 
				
			||||||
 | 
					          {...form.getInputProps("mode_key")}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (!result) return;
 | 
					        {data && data.name && (
 | 
				
			||||||
 | 
					          <TextInput
 | 
				
			||||||
 | 
					            className="col-span-2"
 | 
				
			||||||
 | 
					            readOnly={!!data}
 | 
				
			||||||
 | 
					            size="sm"
 | 
				
			||||||
 | 
					            label="Name"
 | 
				
			||||||
 | 
					            value={data.name}
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
 | 
					        <TextInput
 | 
				
			||||||
 | 
					          className="col-span-2"
 | 
				
			||||||
 | 
					          readOnly={!!data}
 | 
				
			||||||
 | 
					          size="sm"
 | 
				
			||||||
 | 
					          label="Url"
 | 
				
			||||||
 | 
					          {...form.getInputProps("url")}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					        <NumberInput
 | 
				
			||||||
 | 
					          className="col-span-2"
 | 
				
			||||||
 | 
					          size="sm"
 | 
				
			||||||
 | 
					          label="Max price"
 | 
				
			||||||
 | 
					          {...form.getInputProps("max_price")}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					        <NumberInput
 | 
				
			||||||
 | 
					          size="sm"
 | 
				
			||||||
 | 
					          label="Plus price"
 | 
				
			||||||
 | 
					          {...form.getInputProps("plus_price")}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					        <NumberInput
 | 
				
			||||||
 | 
					          size="sm"
 | 
				
			||||||
 | 
					          label="Quantity"
 | 
				
			||||||
 | 
					          {...form.getInputProps("quantity")}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            props.onClose();
 | 
					        <NumberInput
 | 
				
			||||||
 | 
					          description="Note: that only integer minutes are accepted."
 | 
				
			||||||
 | 
					          className="col-span-1"
 | 
				
			||||||
 | 
					          size="sm"
 | 
				
			||||||
 | 
					          label={`Arrival offset seconds (${formatTimeFromMinutes(
 | 
				
			||||||
 | 
					            form.getValues()[
 | 
				
			||||||
 | 
					              `arrival_offset_seconds_${form.getValues()["mode_key"]}`
 | 
				
			||||||
 | 
					            ] / 60
 | 
				
			||||||
 | 
					          )})`}
 | 
				
			||||||
 | 
					          placeholder="msg: 300"
 | 
				
			||||||
 | 
					          {...form.getInputProps(
 | 
				
			||||||
 | 
					            `arrival_offset_seconds_${form.getValues()["mode_key"]}`
 | 
				
			||||||
 | 
					          )}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (onUpdated) {
 | 
					        <NumberInput
 | 
				
			||||||
                onUpdated();
 | 
					          description="Note: that only integer minutes are accepted."
 | 
				
			||||||
            }
 | 
					          className="col-span-1"
 | 
				
			||||||
        }
 | 
					          size="sm"
 | 
				
			||||||
    };
 | 
					          label={`Early tracking seconds (${formatTimeFromMinutes(
 | 
				
			||||||
 | 
					            form.getValues()[
 | 
				
			||||||
 | 
					              `early_tracking_seconds_${form.getValues()["mode_key"]}`
 | 
				
			||||||
 | 
					            ] / 60
 | 
				
			||||||
 | 
					          )})`}
 | 
				
			||||||
 | 
					          placeholder="msg: 600"
 | 
				
			||||||
 | 
					          {...form.getInputProps(
 | 
				
			||||||
 | 
					            `early_tracking_seconds_${form.getValues()["mode_key"]}`
 | 
				
			||||||
 | 
					          )}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    useEffect(() => {
 | 
					        <Button
 | 
				
			||||||
        form.reset();
 | 
					          //   disabled={_.isEqual(form.values, prevData.current)}
 | 
				
			||||||
        if (!data) return;
 | 
					          className="col-span-2"
 | 
				
			||||||
 | 
					          type="submit"
 | 
				
			||||||
        form.setValues(data);
 | 
					          fullWidth
 | 
				
			||||||
 | 
					          size="sm"
 | 
				
			||||||
        prevData.current = data;
 | 
					          mt="md"
 | 
				
			||||||
        // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
					 | 
				
			||||||
    }, [data]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    useEffect(() => {
 | 
					 | 
				
			||||||
        if (!props.opened) {
 | 
					 | 
				
			||||||
            form.reset();
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
					 | 
				
			||||||
    }, [props.opened]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return (
 | 
					 | 
				
			||||||
        <Modal
 | 
					 | 
				
			||||||
            className="relative"
 | 
					 | 
				
			||||||
            classNames={{
 | 
					 | 
				
			||||||
                header: '!flex !item-center !justify-center w-full',
 | 
					 | 
				
			||||||
            }}
 | 
					 | 
				
			||||||
            {...props}
 | 
					 | 
				
			||||||
            size={'xl'}
 | 
					 | 
				
			||||||
            title={<span className="text-xl font-bold">Bid</span>}
 | 
					 | 
				
			||||||
            centered
 | 
					 | 
				
			||||||
        >
 | 
					        >
 | 
				
			||||||
            <form onSubmit={form.onSubmit(handleSubmit)} className="grid grid-cols-2 gap-2.5">
 | 
					          {data ? "Update" : "Create"}
 | 
				
			||||||
                {data && data.name && <TextInput className="col-span-2" readOnly={!!data} size="sm" label="Name" value={data.name} />}
 | 
					        </Button>
 | 
				
			||||||
                <TextInput className="col-span-2" readOnly={!!data} size="sm" label="Url" {...form.getInputProps('url')} />
 | 
					      </form>
 | 
				
			||||||
                <NumberInput className="col-span-2" size="sm" label="Max price" {...form.getInputProps('max_price')} />
 | 
					 | 
				
			||||||
                <NumberInput size="sm" label="Plus price" {...form.getInputProps('plus_price')} />
 | 
					 | 
				
			||||||
                <NumberInput size="sm" label="Quantity" {...form.getInputProps('quantity')} />
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                <Button disabled={_.isEqual(form.getValues(), prevData.current)} className="col-span-2" type="submit" fullWidth size="sm" mt="md">
 | 
					      <LoadingOverlay
 | 
				
			||||||
                    {data ? 'Update' : 'Create'}
 | 
					        visible={loading}
 | 
				
			||||||
                </Button>
 | 
					        zIndex={1000}
 | 
				
			||||||
            </form>
 | 
					        overlayProps={{ blur: 2 }}
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
            <LoadingOverlay visible={loading} zIndex={1000} overlayProps={{ blur: 2 }} />
 | 
					    </Modal>
 | 
				
			||||||
        </Modal>
 | 
					  );
 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -17,11 +17,13 @@ export default function DeleteRowAction({
 | 
				
			||||||
    setConfirm({
 | 
					    setConfirm({
 | 
				
			||||||
      handleOk: async () => {
 | 
					      handleOk: async () => {
 | 
				
			||||||
        const result = await deletesBid(chooses);
 | 
					        const result = await deletesBid(chooses);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        console.log({ result });
 | 
				
			||||||
        if (!result) return;
 | 
					        if (!result) return;
 | 
				
			||||||
        onDeleted?.();
 | 
					        onDeleted?.();
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      title: 'Delete',
 | 
					      title: "Delete",
 | 
				
			||||||
      message: `This action will remove ${chooses.length} products.`
 | 
					      message: `This action will remove ${chooses.length} products.`,
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,4 +1,4 @@
 | 
				
			||||||
export { default as ShowHistoriesModal } from './show-histories-modal';
 | 
					export { default as ShowHistoriesModal } from "./show-histories-modal";
 | 
				
			||||||
export { default as ShowHistoriesBidGraysApiModal } from './show-histories-bid-grays-api-modal';
 | 
					export { default as BidModal } from "./bid-modal";
 | 
				
			||||||
export { default as ShowHistoriesBidPicklesApiModal } from './show-histories-bid-pickles-api-modal';
 | 
					export { default as RecordModal } from "./record-modal";
 | 
				
			||||||
export { default as BidModal } from './bid-modal';
 | 
					export { default as ResponseDemoModal } from "./response-demo-modal";
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,168 @@
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  ActionIcon,
 | 
				
			||||||
 | 
					  Box,
 | 
				
			||||||
 | 
					  Card,
 | 
				
			||||||
 | 
					  Group,
 | 
				
			||||||
 | 
					  Menu,
 | 
				
			||||||
 | 
					  Modal,
 | 
				
			||||||
 | 
					  ModalProps,
 | 
				
			||||||
 | 
					  ScrollArea,
 | 
				
			||||||
 | 
					  Text,
 | 
				
			||||||
 | 
					  Title,
 | 
				
			||||||
 | 
					} from "@mantine/core";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  IconDots,
 | 
				
			||||||
 | 
					  IconPlayerRecordFilled,
 | 
				
			||||||
 | 
					  IconTrash,
 | 
				
			||||||
 | 
					  IconVideoFilled,
 | 
				
			||||||
 | 
					} from "@tabler/icons-react";
 | 
				
			||||||
 | 
					import moment from "moment";
 | 
				
			||||||
 | 
					import { useEffect, useState } from "react";
 | 
				
			||||||
 | 
					import ReactPlayer from "react-player";
 | 
				
			||||||
 | 
					import { deleteRecord, getRecords } from "../../apis/bid";
 | 
				
			||||||
 | 
					import { useConfirmStore } from "../../lib/zustand/use-confirm";
 | 
				
			||||||
 | 
					import { IBid } from "../../system/type";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface IRecordModalProps extends ModalProps {
 | 
				
			||||||
 | 
					  data: IBid | null;
 | 
				
			||||||
 | 
					  onUpdated?: () => void;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface IRecord {
 | 
				
			||||||
 | 
					  bid_id: number;
 | 
				
			||||||
 | 
					  name: string;
 | 
				
			||||||
 | 
					  original_name: string;
 | 
				
			||||||
 | 
					  timestamp: number;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function RecordModal({ data, ...props }: IRecordModalProps) {
 | 
				
			||||||
 | 
					  const [records, setRecords] = useState<IRecord[]>([]);
 | 
				
			||||||
 | 
					  const [selected, setSelected] = useState<IRecord | null>(null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const { setConfirm } = useConfirmStore();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const fetch = async () => {
 | 
				
			||||||
 | 
					    if (!data) return;
 | 
				
			||||||
 | 
					    const response = await getRecords(data);
 | 
				
			||||||
 | 
					    if (response?.data) {
 | 
				
			||||||
 | 
					      setRecords(response.data);
 | 
				
			||||||
 | 
					      setSelected(response.data[0]);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleDelete = async (item: IRecord) => {
 | 
				
			||||||
 | 
					    setConfirm({
 | 
				
			||||||
 | 
					      handleOk: async () => {
 | 
				
			||||||
 | 
					        const result = await deleteRecord(item.original_name);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (!result?.data) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        fetch();
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    fetch();
 | 
				
			||||||
 | 
					    // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
				
			||||||
 | 
					  }, [data]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <Modal
 | 
				
			||||||
 | 
					      classNames={{
 | 
				
			||||||
 | 
					        header: "!flex !justify-center w-full",
 | 
				
			||||||
 | 
					      }}
 | 
				
			||||||
 | 
					      {...props}
 | 
				
			||||||
 | 
					      size={"xl"}
 | 
				
			||||||
 | 
					      title={<Title order={3}>Video Records</Title>}
 | 
				
			||||||
 | 
					      centered
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <Box className="grid grid-cols-1 md:grid-cols-3 gap-4">
 | 
				
			||||||
 | 
					        {/* Sidebar - Record list */}
 | 
				
			||||||
 | 
					        {records.length > 0 && (
 | 
				
			||||||
 | 
					          <ScrollArea className="md:col-span-1 h-[400px] pr-2">
 | 
				
			||||||
 | 
					            {records.map((item) => (
 | 
				
			||||||
 | 
					              <Card
 | 
				
			||||||
 | 
					                key={item.original_name}
 | 
				
			||||||
 | 
					                withBorder
 | 
				
			||||||
 | 
					                shadow="xs"
 | 
				
			||||||
 | 
					                radius="md"
 | 
				
			||||||
 | 
					                className={`mb-3 cursor-pointer transition-all ${
 | 
				
			||||||
 | 
					                  selected?.original_name === item.original_name
 | 
				
			||||||
 | 
					                    ? "border-blue-500 bg-blue-50"
 | 
				
			||||||
 | 
					                    : "hover:bg-gray-50"
 | 
				
			||||||
 | 
					                }`}
 | 
				
			||||||
 | 
					                onClick={() => setSelected(item)}
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                <Card.Section withBorder inheritPadding py="xs">
 | 
				
			||||||
 | 
					                  <Group justify="space-between">
 | 
				
			||||||
 | 
					                    <Group>
 | 
				
			||||||
 | 
					                      <IconVideoFilled />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                      {selected &&
 | 
				
			||||||
 | 
					                        selected.original_name === item.original_name && (
 | 
				
			||||||
 | 
					                          <IconPlayerRecordFilled
 | 
				
			||||||
 | 
					                            size={14}
 | 
				
			||||||
 | 
					                            color="red"
 | 
				
			||||||
 | 
					                            className="animate-pulse"
 | 
				
			||||||
 | 
					                          />
 | 
				
			||||||
 | 
					                        )}
 | 
				
			||||||
 | 
					                    </Group>
 | 
				
			||||||
 | 
					                    <Menu withinPortal position="bottom-end" shadow="sm">
 | 
				
			||||||
 | 
					                      <Menu.Target>
 | 
				
			||||||
 | 
					                        <ActionIcon variant="subtle" color="gray">
 | 
				
			||||||
 | 
					                          <IconDots size={16} />
 | 
				
			||||||
 | 
					                        </ActionIcon>
 | 
				
			||||||
 | 
					                      </Menu.Target>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                      <Menu.Dropdown>
 | 
				
			||||||
 | 
					                        <Menu.Item
 | 
				
			||||||
 | 
					                          onClick={() => handleDelete(item)}
 | 
				
			||||||
 | 
					                          leftSection={<IconTrash size={14} />}
 | 
				
			||||||
 | 
					                          color="red"
 | 
				
			||||||
 | 
					                        >
 | 
				
			||||||
 | 
					                          Delete
 | 
				
			||||||
 | 
					                        </Menu.Item>
 | 
				
			||||||
 | 
					                      </Menu.Dropdown>
 | 
				
			||||||
 | 
					                    </Menu>
 | 
				
			||||||
 | 
					                  </Group>
 | 
				
			||||||
 | 
					                </Card.Section>
 | 
				
			||||||
 | 
					                <Text mt={"xs"} size="sm" fw={500}>
 | 
				
			||||||
 | 
					                  {item.name}
 | 
				
			||||||
 | 
					                </Text>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <Text mt="xs" c="dimmed" size="xs">
 | 
				
			||||||
 | 
					                  {moment(Number(item.timestamp)).fromNow()}
 | 
				
			||||||
 | 
					                </Text>
 | 
				
			||||||
 | 
					              </Card>
 | 
				
			||||||
 | 
					            ))}
 | 
				
			||||||
 | 
					          </ScrollArea>
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        {/* Player - Main content */}
 | 
				
			||||||
 | 
					        {records.length > 0 && (
 | 
				
			||||||
 | 
					          <Box className="md:col-span-2">
 | 
				
			||||||
 | 
					            {selected && (
 | 
				
			||||||
 | 
					              <ReactPlayer
 | 
				
			||||||
 | 
					                url={`${import.meta.env.VITE_BASE_URL}bids/record/${
 | 
				
			||||||
 | 
					                  selected.original_name
 | 
				
			||||||
 | 
					                }`}
 | 
				
			||||||
 | 
					                controls
 | 
				
			||||||
 | 
					                width="100%"
 | 
				
			||||||
 | 
					                height="auto"
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
 | 
					            )}
 | 
				
			||||||
 | 
					          </Box>
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        {records.length <= 0 && (
 | 
				
			||||||
 | 
					          <Box className="col-span-3">
 | 
				
			||||||
 | 
					            <Text className="text-center" c="dimmed">
 | 
				
			||||||
 | 
					              No video
 | 
				
			||||||
 | 
					            </Text>
 | 
				
			||||||
 | 
					          </Box>
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
 | 
					      </Box>
 | 
				
			||||||
 | 
					    </Modal>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,189 @@
 | 
				
			||||||
 | 
					/* eslint-disable @typescript-eslint/no-explicit-any */
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  ActionIcon,
 | 
				
			||||||
 | 
					  Box,
 | 
				
			||||||
 | 
					  Card,
 | 
				
			||||||
 | 
					  Group,
 | 
				
			||||||
 | 
					  Menu,
 | 
				
			||||||
 | 
					  Modal,
 | 
				
			||||||
 | 
					  ModalProps,
 | 
				
			||||||
 | 
					  ScrollArea,
 | 
				
			||||||
 | 
					  Text,
 | 
				
			||||||
 | 
					  Title,
 | 
				
			||||||
 | 
					} from "@mantine/core";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  IconCodeCircle,
 | 
				
			||||||
 | 
					  IconDots,
 | 
				
			||||||
 | 
					  IconPlayerRecordFilled,
 | 
				
			||||||
 | 
					  IconTrash,
 | 
				
			||||||
 | 
					} from "@tabler/icons-react";
 | 
				
			||||||
 | 
					import moment from "moment";
 | 
				
			||||||
 | 
					import { useEffect, useRef, useState } from "react";
 | 
				
			||||||
 | 
					import ReactJson from "react-json-view";
 | 
				
			||||||
 | 
					import { updateMetadata } from "../../apis/bid";
 | 
				
			||||||
 | 
					import { useConfirmStore } from "../../lib/zustand/use-confirm";
 | 
				
			||||||
 | 
					import { IBid, IMetadata } from "../../system/type";
 | 
				
			||||||
 | 
					import { getResponseDemo } from "../../utils";
 | 
				
			||||||
 | 
					export interface IResponseDemoProps extends ModalProps {
 | 
				
			||||||
 | 
					  data: IBid | null;
 | 
				
			||||||
 | 
					  onUpdated?: () => void;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function ResponseDemoModal({
 | 
				
			||||||
 | 
					  data,
 | 
				
			||||||
 | 
					  ...props
 | 
				
			||||||
 | 
					}: IResponseDemoProps) {
 | 
				
			||||||
 | 
					  const { setConfirm } = useConfirmStore();
 | 
				
			||||||
 | 
					  const [selected, setSelected] = useState<Record<string, any> | null>(null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const [responseDemo, setResponseDemo] = useState<null | IMetadata>(null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const dataRef = useRef<IBid | null>(data);
 | 
				
			||||||
 | 
					  // const responseDemo = useMemo(() => {
 | 
				
			||||||
 | 
					  //   if (!data) return null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  //   const values = getResponseDemo(data);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  //   if (Array.isArray(values?.value) && values?.value[0]) {
 | 
				
			||||||
 | 
					  //     setSelected(values?.value[0]);
 | 
				
			||||||
 | 
					  //   }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  //   return values;
 | 
				
			||||||
 | 
					  // }, [data]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    if (!data) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const value = getResponseDemo(data);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (Array.isArray(value?.value) && value?.value[0]) {
 | 
				
			||||||
 | 
					      setSelected(value?.value[0]);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    setResponseDemo(value);
 | 
				
			||||||
 | 
					    dataRef.current = data;
 | 
				
			||||||
 | 
					  }, [data]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleDelete = async () => {
 | 
				
			||||||
 | 
					    if (!responseDemo || !selected) return;
 | 
				
			||||||
 | 
					    console.log(selected);
 | 
				
			||||||
 | 
					    setConfirm({
 | 
				
			||||||
 | 
					      handleOk: async () => {
 | 
				
			||||||
 | 
					        const prevData = responseDemo.value as Record<string, any>[];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const newData = prevData.filter(
 | 
				
			||||||
 | 
					          (item) => item?.uuid !== selected?.uuid
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const result = await updateMetadata(
 | 
				
			||||||
 | 
					          responseDemo.id,
 | 
				
			||||||
 | 
					          JSON.stringify(newData)
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (!result?.data) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        props.onUpdated?.();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        setResponseDemo(result.data);
 | 
				
			||||||
 | 
					        if (result?.data?.value[0]) {
 | 
				
			||||||
 | 
					          setSelected(result?.data?.value[0]);
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          setSelected(null);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        // props.onClose();
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <Modal
 | 
				
			||||||
 | 
					      classNames={{
 | 
				
			||||||
 | 
					        header: "!flex !justify-center w-full",
 | 
				
			||||||
 | 
					      }}
 | 
				
			||||||
 | 
					      {...props}
 | 
				
			||||||
 | 
					      size={"xl"}
 | 
				
			||||||
 | 
					      title={<Title order={3}>Response Demo</Title>}
 | 
				
			||||||
 | 
					      centered
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <Box className="grid grid-cols-1 md:grid-cols-3 gap-4">
 | 
				
			||||||
 | 
					        {/* Sidebar - Record list */}
 | 
				
			||||||
 | 
					        {responseDemo && responseDemo?.value.length > 0 && (
 | 
				
			||||||
 | 
					          <ScrollArea className="md:col-span-1 h-[600px] pr-2">
 | 
				
			||||||
 | 
					            {Array.isArray(responseDemo.value) &&
 | 
				
			||||||
 | 
					              responseDemo.value.map((item) => {
 | 
				
			||||||
 | 
					                return (
 | 
				
			||||||
 | 
					                  <Card
 | 
				
			||||||
 | 
					                    onClick={() => setSelected(item)}
 | 
				
			||||||
 | 
					                    key={item.uuid}
 | 
				
			||||||
 | 
					                    withBorder
 | 
				
			||||||
 | 
					                    shadow="xs"
 | 
				
			||||||
 | 
					                    radius="md"
 | 
				
			||||||
 | 
					                    className={`mb-3 cursor-pointer transition-all ${"border-blue-500 bg-blue-50"}`}
 | 
				
			||||||
 | 
					                  >
 | 
				
			||||||
 | 
					                    <Card.Section withBorder inheritPadding py="xs">
 | 
				
			||||||
 | 
					                      <Group justify="space-between">
 | 
				
			||||||
 | 
					                        <Group>
 | 
				
			||||||
 | 
					                          <IconCodeCircle />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                          {selected && selected?.uuid === item?.uuid && (
 | 
				
			||||||
 | 
					                            <IconPlayerRecordFilled
 | 
				
			||||||
 | 
					                              size={14}
 | 
				
			||||||
 | 
					                              color="red"
 | 
				
			||||||
 | 
					                              className="animate-pulse"
 | 
				
			||||||
 | 
					                            />
 | 
				
			||||||
 | 
					                          )}
 | 
				
			||||||
 | 
					                        </Group>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        <Menu withinPortal position="bottom-end" shadow="sm">
 | 
				
			||||||
 | 
					                          <Menu.Target>
 | 
				
			||||||
 | 
					                            <ActionIcon variant="subtle" color="gray">
 | 
				
			||||||
 | 
					                              <IconDots size={16} />
 | 
				
			||||||
 | 
					                            </ActionIcon>
 | 
				
			||||||
 | 
					                          </Menu.Target>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                          <Menu.Dropdown>
 | 
				
			||||||
 | 
					                            <Menu.Item
 | 
				
			||||||
 | 
					                              onClick={handleDelete}
 | 
				
			||||||
 | 
					                              leftSection={<IconTrash size={14} />}
 | 
				
			||||||
 | 
					                              color="red"
 | 
				
			||||||
 | 
					                            >
 | 
				
			||||||
 | 
					                              Delete
 | 
				
			||||||
 | 
					                            </Menu.Item>
 | 
				
			||||||
 | 
					                          </Menu.Dropdown>
 | 
				
			||||||
 | 
					                        </Menu>
 | 
				
			||||||
 | 
					                      </Group>
 | 
				
			||||||
 | 
					                    </Card.Section>
 | 
				
			||||||
 | 
					                    <Text mt={"xs"} size="sm" fw={500}>
 | 
				
			||||||
 | 
					                      {dataRef.current?.name}
 | 
				
			||||||
 | 
					                    </Text>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    <Text mt="xs" c="dimmed" size="xs">
 | 
				
			||||||
 | 
					                      {item?.timestamp &&
 | 
				
			||||||
 | 
					                        moment(Number(item?.timestamp)).fromNow()}
 | 
				
			||||||
 | 
					                    </Text>
 | 
				
			||||||
 | 
					                  </Card>
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
 | 
					              })}
 | 
				
			||||||
 | 
					          </ScrollArea>
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        {/* Player - Main content */}
 | 
				
			||||||
 | 
					        {responseDemo?.value && selected && (
 | 
				
			||||||
 | 
					          <ScrollArea h={600} className="md:col-span-2">
 | 
				
			||||||
 | 
					            <ReactJson theme={"bespin"} src={selected || {}} />
 | 
				
			||||||
 | 
					          </ScrollArea>
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        {(!responseDemo ||
 | 
				
			||||||
 | 
					          (Array.isArray(responseDemo.value) &&
 | 
				
			||||||
 | 
					            responseDemo.value.length <= 0)) && (
 | 
				
			||||||
 | 
					          <Box className="col-span-3">
 | 
				
			||||||
 | 
					            <Text className="text-center" c="dimmed">
 | 
				
			||||||
 | 
					              No response
 | 
				
			||||||
 | 
					            </Text>
 | 
				
			||||||
 | 
					          </Box>
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
 | 
					      </Box>
 | 
				
			||||||
 | 
					    </Modal>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,71 @@
 | 
				
			||||||
 | 
					/* eslint-disable @typescript-eslint/no-unused-vars */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import Table from "../../../lib/table/table";
 | 
				
			||||||
 | 
					import { IColumn } from "../../../lib/table/type";
 | 
				
			||||||
 | 
					import { formatTime } from "../../../utils";
 | 
				
			||||||
 | 
					export interface IGraysHistoriesViewProps {
 | 
				
			||||||
 | 
					  histories: Record<string, string>[];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function AllbidsHistoriesView({
 | 
				
			||||||
 | 
					  histories,
 | 
				
			||||||
 | 
					}: IGraysHistoriesViewProps) {
 | 
				
			||||||
 | 
					  type BidHistoryEntry = {
 | 
				
			||||||
 | 
					    row_id: number;
 | 
				
			||||||
 | 
					    date: string; // ISO datetime string
 | 
				
			||||||
 | 
					    amount: number; // Số tiền đặt giá
 | 
				
			||||||
 | 
					    proxyamount: number; // Giá proxy tối đa
 | 
				
			||||||
 | 
					    bidQty: number; // Số lượng
 | 
				
			||||||
 | 
					    flashBuy: boolean; // Mua ngay
 | 
				
			||||||
 | 
					    proxyBid: boolean; // Có phải đấu giá tự động
 | 
				
			||||||
 | 
					    instantBid: boolean; // Đặt giá ngay
 | 
				
			||||||
 | 
					    userName: string; // Tên người dùng
 | 
				
			||||||
 | 
					    bidBidderID: number; // ID người đặt giá
 | 
				
			||||||
 | 
					    $$hashKey?: string; // Khóa nội bộ Angular (không cần thiết, có thể bỏ hoặc để optional)
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const columns: IColumn<BidHistoryEntry>[] = [
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      title: "Username",
 | 
				
			||||||
 | 
					      key: "userName",
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      title: "Amount",
 | 
				
			||||||
 | 
					      key: "amount",
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      title: "Proxy amount",
 | 
				
			||||||
 | 
					      key: "proxyamount",
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      title: "Bid Qty",
 | 
				
			||||||
 | 
					      key: "bidQty",
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      title: "Bid at",
 | 
				
			||||||
 | 
					      key: "date",
 | 
				
			||||||
 | 
					      renderRow(row) {
 | 
				
			||||||
 | 
					        return <span>{formatTime(row.date)}</span>;
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <Table
 | 
				
			||||||
 | 
					      striped
 | 
				
			||||||
 | 
					      highlightOnHover
 | 
				
			||||||
 | 
					      withTableBorder
 | 
				
			||||||
 | 
					      withColumnBorders
 | 
				
			||||||
 | 
					      styleDefaultHead={{
 | 
				
			||||||
 | 
					        justifyContent: "flex-start",
 | 
				
			||||||
 | 
					        width: "fit-content",
 | 
				
			||||||
 | 
					      }}
 | 
				
			||||||
 | 
					      showFilter={false}
 | 
				
			||||||
 | 
					      showActions={false}
 | 
				
			||||||
 | 
					      showChooses={false}
 | 
				
			||||||
 | 
					      columns={columns}
 | 
				
			||||||
 | 
					      rowKey="row_id"
 | 
				
			||||||
 | 
					      rows={histories as unknown as BidHistoryEntry[]}
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,79 @@
 | 
				
			||||||
 | 
					/* eslint-disable @typescript-eslint/no-unused-vars */
 | 
				
			||||||
 | 
					import Table from "../../../lib/table/table";
 | 
				
			||||||
 | 
					import { IColumn } from "../../../lib/table/type";
 | 
				
			||||||
 | 
					import { extractNumber, formatTime } from "../../../utils";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface IGraysHistoriesViewProps {
 | 
				
			||||||
 | 
					  histories: Record<string, string>[];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function GraysHistoriesView({
 | 
				
			||||||
 | 
					  histories,
 | 
				
			||||||
 | 
					}: IGraysHistoriesViewProps) {
 | 
				
			||||||
 | 
					  type BidHistoryEntry = {
 | 
				
			||||||
 | 
					    row_id: number;
 | 
				
			||||||
 | 
					    Price: string;
 | 
				
			||||||
 | 
					    Quantity: number;
 | 
				
			||||||
 | 
					    WinningQuantity: number;
 | 
				
			||||||
 | 
					    UserShortAddress: string;
 | 
				
			||||||
 | 
					    UserInitials: string;
 | 
				
			||||||
 | 
					    OriginalDate: string;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const columns: IColumn<BidHistoryEntry>[] = [
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      title: "Bidding Details",
 | 
				
			||||||
 | 
					      key: "UserInitials",
 | 
				
			||||||
 | 
					      renderRow(row) {
 | 
				
			||||||
 | 
					        return (
 | 
				
			||||||
 | 
					          <span>{`${row["UserInitials"]} - ${row["UserShortAddress"]}`}</span>
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      title: "Bid Time",
 | 
				
			||||||
 | 
					      key: "OriginalDate",
 | 
				
			||||||
 | 
					      renderRow(row) {
 | 
				
			||||||
 | 
					        return (
 | 
				
			||||||
 | 
					          <span>
 | 
				
			||||||
 | 
					            {formatTime(
 | 
				
			||||||
 | 
					              new Date(extractNumber(row["OriginalDate"]) || 0).toUTCString(),
 | 
				
			||||||
 | 
					              "HH:mm:ss DD/MM/YYYY"
 | 
				
			||||||
 | 
					            )}
 | 
				
			||||||
 | 
					          </span>
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      title: "Bid Price",
 | 
				
			||||||
 | 
					      key: "Price",
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      title: "Bid Qty",
 | 
				
			||||||
 | 
					      key: "Quantity",
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      title: "Win Qty",
 | 
				
			||||||
 | 
					      key: "WinningQuantity",
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <Table
 | 
				
			||||||
 | 
					      striped
 | 
				
			||||||
 | 
					      highlightOnHover
 | 
				
			||||||
 | 
					      withTableBorder
 | 
				
			||||||
 | 
					      withColumnBorders
 | 
				
			||||||
 | 
					      styleDefaultHead={{
 | 
				
			||||||
 | 
					        justifyContent: "flex-start",
 | 
				
			||||||
 | 
					        width: "fit-content",
 | 
				
			||||||
 | 
					      }}
 | 
				
			||||||
 | 
					      showFilter={false}
 | 
				
			||||||
 | 
					      showActions={false}
 | 
				
			||||||
 | 
					      showChooses={false}
 | 
				
			||||||
 | 
					      columns={columns}
 | 
				
			||||||
 | 
					      rowKey="row_id"
 | 
				
			||||||
 | 
					      rows={histories as unknown as BidHistoryEntry[]}
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,78 @@
 | 
				
			||||||
 | 
					/* eslint-disable @typescript-eslint/no-unused-vars */
 | 
				
			||||||
 | 
					import Table from "../../../lib/table/table";
 | 
				
			||||||
 | 
					import { IColumn } from "../../../lib/table/type";
 | 
				
			||||||
 | 
					import { formatTime } from "../../../utils";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface IGraysHistoriesViewProps {
 | 
				
			||||||
 | 
					  histories: Record<string, string>[];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function PicklesHistoriesView({
 | 
				
			||||||
 | 
					  histories,
 | 
				
			||||||
 | 
					}: IGraysHistoriesViewProps) {
 | 
				
			||||||
 | 
					  // const rows = useMemo(() => {
 | 
				
			||||||
 | 
					  //   return histories.map((element, index) => (
 | 
				
			||||||
 | 
					  //     <Table.Tr key={index}>
 | 
				
			||||||
 | 
					  //       <Table.Td>{element["bidderAnonName"]}</Table.Td>
 | 
				
			||||||
 | 
					  //       <Table.Td>{element["actualBid"]}</Table.Td>
 | 
				
			||||||
 | 
					  //       <Table.Td>
 | 
				
			||||||
 | 
					  //         {formatTime(
 | 
				
			||||||
 | 
					  //           new Date(element["bidTimeInMilliSeconds"]).toUTCString(),
 | 
				
			||||||
 | 
					  //           "HH:mm:ss DD/MM/YYYY"
 | 
				
			||||||
 | 
					  //         )}
 | 
				
			||||||
 | 
					  //       </Table.Td>
 | 
				
			||||||
 | 
					  //     </Table.Tr>
 | 
				
			||||||
 | 
					  //   ));
 | 
				
			||||||
 | 
					  // }, [histories]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  type BidHistoryEntry = {
 | 
				
			||||||
 | 
					    row_id: number;
 | 
				
			||||||
 | 
					    bidderAnonName: string;
 | 
				
			||||||
 | 
					    actualBid: number;
 | 
				
			||||||
 | 
					    bidTimeInMilliSeconds: number;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const columns: IColumn<BidHistoryEntry>[] = [
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      title: "Bidder name",
 | 
				
			||||||
 | 
					      key: "bidderAnonName",
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      title: "Actual bid",
 | 
				
			||||||
 | 
					      key: "actualBid",
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      title: "Time",
 | 
				
			||||||
 | 
					      key: "bidTimeInMilliSeconds",
 | 
				
			||||||
 | 
					      renderRow(row) {
 | 
				
			||||||
 | 
					        return (
 | 
				
			||||||
 | 
					          <span>
 | 
				
			||||||
 | 
					            {formatTime(
 | 
				
			||||||
 | 
					              new Date(row["bidTimeInMilliSeconds"]).toUTCString(),
 | 
				
			||||||
 | 
					              "HH:mm:ss DD/MM/YYYY"
 | 
				
			||||||
 | 
					            )}
 | 
				
			||||||
 | 
					          </span>
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <Table
 | 
				
			||||||
 | 
					      striped
 | 
				
			||||||
 | 
					      highlightOnHover
 | 
				
			||||||
 | 
					      withTableBorder
 | 
				
			||||||
 | 
					      withColumnBorders
 | 
				
			||||||
 | 
					      styleDefaultHead={{
 | 
				
			||||||
 | 
					        justifyContent: "flex-start",
 | 
				
			||||||
 | 
					        width: "fit-content",
 | 
				
			||||||
 | 
					      }}
 | 
				
			||||||
 | 
					      showFilter={false}
 | 
				
			||||||
 | 
					      showActions={false}
 | 
				
			||||||
 | 
					      showChooses={false}
 | 
				
			||||||
 | 
					      columns={columns}
 | 
				
			||||||
 | 
					      rowKey="row_id"
 | 
				
			||||||
 | 
					      rows={histories as unknown as BidHistoryEntry[]}
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,81 @@
 | 
				
			||||||
 | 
					/* eslint-disable @typescript-eslint/no-unused-vars */
 | 
				
			||||||
 | 
					import { LoadingOverlay, Modal, ModalProps } from "@mantine/core";
 | 
				
			||||||
 | 
					import { useCallback, useEffect, useMemo, useState } from "react";
 | 
				
			||||||
 | 
					import { getDetailBidHistories } from "../../../apis/bid-histories";
 | 
				
			||||||
 | 
					import { IBid } from "../../../system/type";
 | 
				
			||||||
 | 
					import GraysHistoriesView from "./grays-histories-view";
 | 
				
			||||||
 | 
					import constants from "../../../constant";
 | 
				
			||||||
 | 
					import PicklesHistoriesView from "./pickles-histories-view";
 | 
				
			||||||
 | 
					import AllbidsHistoriesView from "./allbids-histories-view";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface IShowHistoriesApiModalProps extends ModalProps {
 | 
				
			||||||
 | 
					  data: IBid | null;
 | 
				
			||||||
 | 
					  onUpdated?: () => void;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function ShowHistoriesApiModal({
 | 
				
			||||||
 | 
					  data,
 | 
				
			||||||
 | 
					  onUpdated,
 | 
				
			||||||
 | 
					  ...props
 | 
				
			||||||
 | 
					}: IShowHistoriesApiModalProps) {
 | 
				
			||||||
 | 
					  const [histories, setHistories] = useState<Record<string, string>[]>([]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const [loading, setLoading] = useState(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleCallApi = useCallback(async () => {
 | 
				
			||||||
 | 
					    if (!data?.lot_id) {
 | 
				
			||||||
 | 
					      setHistories([]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    setLoading(true);
 | 
				
			||||||
 | 
					    const response = await getDetailBidHistories(data?.lot_id);
 | 
				
			||||||
 | 
					    setLoading(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (response.data && response.data) {
 | 
				
			||||||
 | 
					      const values = (response.data as Record<string, string>[]).map(
 | 
				
			||||||
 | 
					        (item, index) => {
 | 
				
			||||||
 | 
					          return {
 | 
				
			||||||
 | 
					            ...item,
 | 
				
			||||||
 | 
					            row_id: String(index),
 | 
				
			||||||
 | 
					          };
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      setHistories(values);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }, [data]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    handleCallApi();
 | 
				
			||||||
 | 
					  }, [handleCallApi]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const generateView = useMemo(() => {
 | 
				
			||||||
 | 
					    switch (data?.web_bid.origin_url) {
 | 
				
			||||||
 | 
					      case constants.grays:
 | 
				
			||||||
 | 
					        return <GraysHistoriesView histories={histories} />;
 | 
				
			||||||
 | 
					      case constants.pickles:
 | 
				
			||||||
 | 
					        return <PicklesHistoriesView histories={histories} />;
 | 
				
			||||||
 | 
					      case constants.allbids:
 | 
				
			||||||
 | 
					        return <AllbidsHistoriesView histories={histories} />;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }, [data?.web_bid.origin_url, histories]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <Modal
 | 
				
			||||||
 | 
					      className="relative"
 | 
				
			||||||
 | 
					      {...props}
 | 
				
			||||||
 | 
					      size="xl"
 | 
				
			||||||
 | 
					      title={<span className="text-xl font-bold">BIDDING HISTORY</span>}
 | 
				
			||||||
 | 
					      centered
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      {generateView}
 | 
				
			||||||
 | 
					      <LoadingOverlay
 | 
				
			||||||
 | 
					        visible={loading}
 | 
				
			||||||
 | 
					        zIndex={1000}
 | 
				
			||||||
 | 
					        overlayProps={{ blur: 2 }}
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					    </Modal>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1,78 +0,0 @@
 | 
				
			||||||
/* eslint-disable @typescript-eslint/no-unused-vars */
 | 
					 | 
				
			||||||
import { LoadingOverlay, Modal, ModalProps, Table } from '@mantine/core';
 | 
					 | 
				
			||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
 | 
					 | 
				
			||||||
import { getDetailBidHistories } from '../../apis/bid-histories';
 | 
					 | 
				
			||||||
import { IBid } from '../../system/type';
 | 
					 | 
				
			||||||
import { extractNumber, formatTime } from '../../utils';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export interface IShowHistoriesBidGraysApiModalProps extends ModalProps {
 | 
					 | 
				
			||||||
    data: IBid | null;
 | 
					 | 
				
			||||||
    onUpdated?: () => void;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default function ShowHistoriesBidGraysApiModal({ data, onUpdated, ...props }: IShowHistoriesBidGraysApiModalProps) {
 | 
					 | 
				
			||||||
    const [histories, setHistories] = useState<Record<string, string>[]>([]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const [loading, setLoading] = useState(false);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const rows = useMemo(() => {
 | 
					 | 
				
			||||||
        return histories.map((element, index) => (
 | 
					 | 
				
			||||||
            <Table.Tr key={index}>
 | 
					 | 
				
			||||||
                <Table.Td>{`${element['UserInitials']} - ${element['UserShortAddress']}`}</Table.Td>
 | 
					 | 
				
			||||||
                <Table.Td>{formatTime(new Date(extractNumber(element['OriginalDate']) || 0).toUTCString(), 'HH:mm:ss DD/MM/YYYY')}</Table.Td>
 | 
					 | 
				
			||||||
                <Table.Td>{`AU $${element['Price']}`}</Table.Td>
 | 
					 | 
				
			||||||
                <Table.Td>{`${element['Quantity']}`}</Table.Td>
 | 
					 | 
				
			||||||
                <Table.Td>{`${element['WinningQuantity']}`}</Table.Td>
 | 
					 | 
				
			||||||
            </Table.Tr>
 | 
					 | 
				
			||||||
        ));
 | 
					 | 
				
			||||||
    }, [histories]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const handleCallApi = useCallback(async () => {
 | 
					 | 
				
			||||||
        if (!data?.lot_id) {
 | 
					 | 
				
			||||||
            setHistories([]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            return;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        setLoading(true);
 | 
					 | 
				
			||||||
        const response = await getDetailBidHistories(data?.lot_id);
 | 
					 | 
				
			||||||
        setLoading(false);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (response.data && response.data) {
 | 
					 | 
				
			||||||
            setHistories(response.data);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }, [data]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    useEffect(() => {
 | 
					 | 
				
			||||||
        handleCallApi();
 | 
					 | 
				
			||||||
    }, [handleCallApi]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return (
 | 
					 | 
				
			||||||
        <Modal className="relative" {...props} size="xl" title={<span className="text-xl font-bold">BIDDING HISTORY</span>} centered>
 | 
					 | 
				
			||||||
            <Table striped highlightOnHover withTableBorder withColumnBorders>
 | 
					 | 
				
			||||||
                <Table.Thead>
 | 
					 | 
				
			||||||
                    <Table.Tr>
 | 
					 | 
				
			||||||
                        <Table.Th>Bidding Details</Table.Th>
 | 
					 | 
				
			||||||
                        <Table.Th>Bid Time</Table.Th>
 | 
					 | 
				
			||||||
                        <Table.Th>Bid Price</Table.Th>
 | 
					 | 
				
			||||||
                        <Table.Th>Bid Qty</Table.Th>
 | 
					 | 
				
			||||||
                        <Table.Th>Win Qty</Table.Th>
 | 
					 | 
				
			||||||
                    </Table.Tr>
 | 
					 | 
				
			||||||
                </Table.Thead>
 | 
					 | 
				
			||||||
                <Table.Tbody>
 | 
					 | 
				
			||||||
                    {histories.length <= 0 ? (
 | 
					 | 
				
			||||||
                        <Table.Tr>
 | 
					 | 
				
			||||||
                            <Table.Td colSpan={5} className="text-center">
 | 
					 | 
				
			||||||
                                None
 | 
					 | 
				
			||||||
                            </Table.Td>
 | 
					 | 
				
			||||||
                        </Table.Tr>
 | 
					 | 
				
			||||||
                    ) : (
 | 
					 | 
				
			||||||
                        rows
 | 
					 | 
				
			||||||
                    )}
 | 
					 | 
				
			||||||
                </Table.Tbody>
 | 
					 | 
				
			||||||
            </Table>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            <LoadingOverlay visible={loading} zIndex={1000} overlayProps={{ blur: 2 }} />
 | 
					 | 
				
			||||||
        </Modal>
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,161 @@
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  ActionIcon,
 | 
				
			||||||
 | 
					  Box,
 | 
				
			||||||
 | 
					  Card,
 | 
				
			||||||
 | 
					  Group,
 | 
				
			||||||
 | 
					  LoadingOverlay,
 | 
				
			||||||
 | 
					  Text,
 | 
				
			||||||
 | 
					  TextInput,
 | 
				
			||||||
 | 
					} from "@mantine/core";
 | 
				
			||||||
 | 
					import { useForm, zodResolver } from "@mantine/form";
 | 
				
			||||||
 | 
					import { IconAt, IconMinus, IconPlus } from "@tabler/icons-react";
 | 
				
			||||||
 | 
					import { useEffect, useMemo, useState } from "react";
 | 
				
			||||||
 | 
					import { z } from "zod";
 | 
				
			||||||
 | 
					import { getConfig, upsertConfig } from "../../apis/config";
 | 
				
			||||||
 | 
					import { IConfig } from "../../system/type";
 | 
				
			||||||
 | 
					import { useConfirmStore } from "../../lib/zustand/use-confirm";
 | 
				
			||||||
 | 
					import { useDisclosure } from "@mantine/hooks";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const schema = z.object({
 | 
				
			||||||
 | 
					  email: z
 | 
				
			||||||
 | 
					    .string({ message: "Email is required" })
 | 
				
			||||||
 | 
					    .email({ message: "Invalid email address" }),
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const MailInput = ({
 | 
				
			||||||
 | 
					  initValue,
 | 
				
			||||||
 | 
					  onDelete,
 | 
				
			||||||
 | 
					  onAdd,
 | 
				
			||||||
 | 
					}: {
 | 
				
			||||||
 | 
					  initValue?: string;
 | 
				
			||||||
 | 
					  onDelete?: (data: string) => void;
 | 
				
			||||||
 | 
					  onAdd?: (data: string) => Promise<void>;
 | 
				
			||||||
 | 
					}) => {
 | 
				
			||||||
 | 
					  const form = useForm({
 | 
				
			||||||
 | 
					    initialValues: {
 | 
				
			||||||
 | 
					      email: initValue || "",
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    validate: zodResolver(schema),
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <form
 | 
				
			||||||
 | 
					      onSubmit={form.onSubmit(
 | 
				
			||||||
 | 
					        onAdd
 | 
				
			||||||
 | 
					          ? async (values) => {
 | 
				
			||||||
 | 
					              await onAdd(values.email);
 | 
				
			||||||
 | 
					              form.reset();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          : () => {}
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					      className="flex items-start gap-2 w-full"
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <TextInput
 | 
				
			||||||
 | 
					        {...form.getInputProps("email")}
 | 
				
			||||||
 | 
					        leftSection={<IconAt size={14} />}
 | 
				
			||||||
 | 
					        placeholder="Enter email"
 | 
				
			||||||
 | 
					        className="flex-1"
 | 
				
			||||||
 | 
					        size="xs"
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					      <ActionIcon
 | 
				
			||||||
 | 
					        onClick={initValue && onDelete ? () => onDelete(initValue) : undefined}
 | 
				
			||||||
 | 
					        type={!initValue ? "submit" : "button"}
 | 
				
			||||||
 | 
					        color={initValue ? "red" : "blue"}
 | 
				
			||||||
 | 
					        variant="light"
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        {initValue ? <IconMinus size={14} /> : <IconPlus size={14} />}
 | 
				
			||||||
 | 
					      </ActionIcon>
 | 
				
			||||||
 | 
					    </form>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function MailsConfig() {
 | 
				
			||||||
 | 
					  const [config, setConfig] = useState<null | IConfig>(null);
 | 
				
			||||||
 | 
					  const { setConfirm } = useConfirmStore();
 | 
				
			||||||
 | 
					  const [opened, { open, close }] = useDisclosure(false);
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    fetchConfig();
 | 
				
			||||||
 | 
					  }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const mails = useMemo(() => {
 | 
				
			||||||
 | 
					    if (!config) return [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return config?.value?.split(", ").length > 0
 | 
				
			||||||
 | 
					      ? config?.value.split(",")
 | 
				
			||||||
 | 
					      : [];
 | 
				
			||||||
 | 
					  }, [config]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const fetchConfig = async () => {
 | 
				
			||||||
 | 
					    const response = await getConfig("MAIL_SCRAP_REPORT");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!response || ![200, 201].includes(response.data?.status_code)) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    setConfig(response.data.data);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleDelete = (mail: string) => {
 | 
				
			||||||
 | 
					    setConfirm({
 | 
				
			||||||
 | 
					      message: "Are you want to delete: " + mail,
 | 
				
			||||||
 | 
					      title: "Delete",
 | 
				
			||||||
 | 
					      handleOk: async () => {
 | 
				
			||||||
 | 
					        open();
 | 
				
			||||||
 | 
					        const newMails = mails.filter((item) => item !== mail);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (!config) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const response = await upsertConfig({
 | 
				
			||||||
 | 
					          ...(config as IConfig),
 | 
				
			||||||
 | 
					          value: newMails.join(", "),
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (response) {
 | 
				
			||||||
 | 
					          fetchConfig();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        close();
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleAdd = async (mail: string) => {
 | 
				
			||||||
 | 
					    const newMails = [...mails, mail.trim()];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    open();
 | 
				
			||||||
 | 
					    const response = await upsertConfig({
 | 
				
			||||||
 | 
					      ...(config as IConfig),
 | 
				
			||||||
 | 
					      value: newMails.join(", "),
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (response) {
 | 
				
			||||||
 | 
					      fetchConfig();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    close();
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <Card withBorder shadow="sm" radius="md" w={400}>
 | 
				
			||||||
 | 
					      <Card.Section withBorder inheritPadding py="xs">
 | 
				
			||||||
 | 
					        <Group justify="space-between">
 | 
				
			||||||
 | 
					          <Text fw={500}>Mails</Text>
 | 
				
			||||||
 | 
					        </Group>
 | 
				
			||||||
 | 
					      </Card.Section>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <Card.Section p="md">
 | 
				
			||||||
 | 
					        <Box className="flex flex-col gap-2">
 | 
				
			||||||
 | 
					          {mails.length > 0 &&
 | 
				
			||||||
 | 
					            mails.map((mail) => {
 | 
				
			||||||
 | 
					              return (
 | 
				
			||||||
 | 
					                <MailInput
 | 
				
			||||||
 | 
					                  onDelete={handleDelete}
 | 
				
			||||||
 | 
					                  key={mail}
 | 
				
			||||||
 | 
					                  initValue={mail}
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
 | 
					              );
 | 
				
			||||||
 | 
					            })}
 | 
				
			||||||
 | 
					          <MailInput onAdd={handleAdd} />
 | 
				
			||||||
 | 
					        </Box>
 | 
				
			||||||
 | 
					      </Card.Section>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <LoadingOverlay visible={opened} />
 | 
				
			||||||
 | 
					    </Card>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1,21 +1,37 @@
 | 
				
			||||||
import { Badge, Box, Button, Image, Text, Tooltip } from "@mantine/core";
 | 
					import {
 | 
				
			||||||
import { useDisclosure } from "@mantine/hooks";
 | 
					  ActionIcon,
 | 
				
			||||||
 | 
					  Badge,
 | 
				
			||||||
 | 
					  Box,
 | 
				
			||||||
 | 
					  Button,
 | 
				
			||||||
 | 
					  Image,
 | 
				
			||||||
 | 
					  Text,
 | 
				
			||||||
 | 
					  Tooltip,
 | 
				
			||||||
 | 
					} from "@mantine/core";
 | 
				
			||||||
 | 
					import { upperFirst, useDisclosure } from "@mantine/hooks";
 | 
				
			||||||
import moment from "moment";
 | 
					import moment from "moment";
 | 
				
			||||||
import { useEffect, useState } from "react";
 | 
					import { useEffect, useState } from "react";
 | 
				
			||||||
import { Socket } from "socket.io-client";
 | 
					import { Socket } from "socket.io-client";
 | 
				
			||||||
import { getImagesWorking } from "../../apis/bid";
 | 
					import { getImagesWorking, updateBid } from "../../apis/bid";
 | 
				
			||||||
import { useStatusToolStore } from "../../lib/zustand/use-status-tool-store";
 | 
					import { useStatusToolStore } from "../../lib/zustand/use-status-tool-store";
 | 
				
			||||||
import { IBid, IWebBid } from "../../system/type";
 | 
					import { IBid, IWebBid } from "../../system/type";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  cn,
 | 
					  cn,
 | 
				
			||||||
  extractDomainSmart,
 | 
					  extractDomainSmart,
 | 
				
			||||||
  findNearestClosingChild,
 | 
					  findNearestClosingChild,
 | 
				
			||||||
 | 
					  getEarlyTrackingSeconds,
 | 
				
			||||||
 | 
					  getMode,
 | 
				
			||||||
  isTimeReached,
 | 
					  isTimeReached,
 | 
				
			||||||
  stringToColor,
 | 
					  stringToColor,
 | 
				
			||||||
  subtractSeconds,
 | 
					  subtractSeconds,
 | 
				
			||||||
} from "../../utils";
 | 
					} from "../../utils";
 | 
				
			||||||
import ShowImageModal from "./show-image-modal";
 | 
					import ShowImageModal from "./show-image-modal";
 | 
				
			||||||
import { IconExternalLink, IconImageInPicture } from "@tabler/icons-react";
 | 
					import {
 | 
				
			||||||
 | 
					  IconExternalLink,
 | 
				
			||||||
 | 
					  IconImageInPicture,
 | 
				
			||||||
 | 
					  IconLivePhoto,
 | 
				
			||||||
 | 
					  IconTestPipe,
 | 
				
			||||||
 | 
					} from "@tabler/icons-react";
 | 
				
			||||||
 | 
					import { useConfirmStore } from "../../lib/zustand/use-confirm";
 | 
				
			||||||
export interface IWorkingPageProps {
 | 
					export interface IWorkingPageProps {
 | 
				
			||||||
  data: (IBid | IWebBid) & { type: string };
 | 
					  data: (IBid | IWebBid) & { type: string };
 | 
				
			||||||
  socket: Socket;
 | 
					  socket: Socket;
 | 
				
			||||||
| 
						 | 
					@ -38,6 +54,8 @@ export default function WorkingPage({ data, socket }: IWorkingPageProps) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const { statusTool } = useStatusToolStore();
 | 
					  const { statusTool } = useStatusToolStore();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const { setConfirm } = useConfirmStore();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  function isIBid(obj: IBid | IWebBid): obj is IBid {
 | 
					  function isIBid(obj: IBid | IWebBid): obj is IBid {
 | 
				
			||||||
    return "name" in obj;
 | 
					    return "name" in obj;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					@ -66,6 +84,33 @@ export default function WorkingPage({ data, socket }: IWorkingPageProps) {
 | 
				
			||||||
    return !statusTool || statusTool !== "online" ? "Unknown" : "logout";
 | 
					    return !statusTool || statusTool !== "online" ? "Unknown" : "logout";
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleToogleMode = async () => {
 | 
				
			||||||
 | 
					    if (!isIBid(data)) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    setConfirm({
 | 
				
			||||||
 | 
					      title: "Confirm changing this product's mode?",
 | 
				
			||||||
 | 
					      okButton: {
 | 
				
			||||||
 | 
					        value: "Sure",
 | 
				
			||||||
 | 
					        color: "blue",
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      handleOk: async () => {
 | 
				
			||||||
 | 
					        const metadata = data.metadata;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const modeKey = metadata.find((item) => item.key_name === "mode_key");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (modeKey && modeKey.value) {
 | 
				
			||||||
 | 
					          if (String(modeKey.value) === "live") {
 | 
				
			||||||
 | 
					            modeKey.value = "sandbox";
 | 
				
			||||||
 | 
					          } else {
 | 
				
			||||||
 | 
					            modeKey.value = "live";
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        await updateBid({ ...data, metadata });
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  useEffect(() => {
 | 
					  useEffect(() => {
 | 
				
			||||||
    const updateImage = ({
 | 
					    const updateImage = ({
 | 
				
			||||||
      type,
 | 
					      type,
 | 
				
			||||||
| 
						 | 
					@ -118,10 +163,6 @@ export default function WorkingPage({ data, socket }: IWorkingPageProps) {
 | 
				
			||||||
    // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
					    // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
				
			||||||
  }, []);
 | 
					  }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (!isIBid(data)) {
 | 
					 | 
				
			||||||
    console.log(data);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <>
 | 
					    <>
 | 
				
			||||||
      <Box
 | 
					      <Box
 | 
				
			||||||
| 
						 | 
					@ -182,7 +223,7 @@ export default function WorkingPage({ data, socket }: IWorkingPageProps) {
 | 
				
			||||||
              !isTimeReached(
 | 
					              !isTimeReached(
 | 
				
			||||||
                subtractSeconds(
 | 
					                subtractSeconds(
 | 
				
			||||||
                  data.close_time,
 | 
					                  data.close_time,
 | 
				
			||||||
                  data.web_bid?.early_tracking_seconds || 0
 | 
					                  getEarlyTrackingSeconds(data) || 0
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
              ) && (
 | 
					              ) && (
 | 
				
			||||||
                <Tooltip label={"Time to tracking"}>
 | 
					                <Tooltip label={"Time to tracking"}>
 | 
				
			||||||
| 
						 | 
					@ -192,7 +233,7 @@ export default function WorkingPage({ data, socket }: IWorkingPageProps) {
 | 
				
			||||||
                  >{`TT: ${moment(
 | 
					                  >{`TT: ${moment(
 | 
				
			||||||
                    subtractSeconds(
 | 
					                    subtractSeconds(
 | 
				
			||||||
                      data.close_time,
 | 
					                      data.close_time,
 | 
				
			||||||
                      data.web_bid?.early_tracking_seconds || 0
 | 
					                      getEarlyTrackingSeconds(data) || 0
 | 
				
			||||||
                    )
 | 
					                    )
 | 
				
			||||||
                  ).format("HH:mm:ss DD/MM/YYYY")}`}</Text>
 | 
					                  ).format("HH:mm:ss DD/MM/YYYY")}`}</Text>
 | 
				
			||||||
                </Tooltip>
 | 
					                </Tooltip>
 | 
				
			||||||
| 
						 | 
					@ -200,7 +241,7 @@ export default function WorkingPage({ data, socket }: IWorkingPageProps) {
 | 
				
			||||||
          </Box>
 | 
					          </Box>
 | 
				
			||||||
          <Box className="flex items-center gap-4">
 | 
					          <Box className="flex items-center gap-4">
 | 
				
			||||||
            <Button
 | 
					            <Button
 | 
				
			||||||
             rightSection={<IconImageInPicture size={14}/>}
 | 
					              rightSection={<IconImageInPicture size={14} />}
 | 
				
			||||||
              size="xs"
 | 
					              size="xs"
 | 
				
			||||||
              color="green"
 | 
					              color="green"
 | 
				
			||||||
              onClick={open}
 | 
					              onClick={open}
 | 
				
			||||||
| 
						 | 
					@ -221,26 +262,49 @@ export default function WorkingPage({ data, socket }: IWorkingPageProps) {
 | 
				
			||||||
          </Box>
 | 
					          </Box>
 | 
				
			||||||
        </Box>
 | 
					        </Box>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <Box className="absolute top-2.5 left-2.5 flex items-center gap-2">
 | 
					        <Box className="absolute top-0 left-0 py-2  px-4 flex items-center gap-2 justify-between w-full">
 | 
				
			||||||
          <Badge
 | 
					          <Box className="flex justify-between w-full">
 | 
				
			||||||
            color={payloadLoginStatus?.login_status ? "green" : "red"}
 | 
					            <Box className="flex items-center gap-2">
 | 
				
			||||||
            size="xs"
 | 
					              <Badge
 | 
				
			||||||
          >
 | 
					                color={payloadLoginStatus?.login_status ? "green" : "red"}
 | 
				
			||||||
            {statusLabel()}
 | 
					                size="xs"
 | 
				
			||||||
          </Badge>
 | 
					              >
 | 
				
			||||||
 | 
					                {statusLabel()}
 | 
				
			||||||
 | 
					              </Badge>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          <Badge
 | 
					              <Badge
 | 
				
			||||||
            color={stringToColor(
 | 
					                color={stringToColor(
 | 
				
			||||||
              isIBid(data)
 | 
					                  isIBid(data)
 | 
				
			||||||
                ? extractDomainSmart(data.web_bid.origin_url)
 | 
					                    ? extractDomainSmart(data.web_bid.origin_url)
 | 
				
			||||||
                : extractDomainSmart(data.origin_url)
 | 
					                    : extractDomainSmart(data.origin_url)
 | 
				
			||||||
 | 
					                )}
 | 
				
			||||||
 | 
					                size="xs"
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                {isIBid(data)
 | 
				
			||||||
 | 
					                  ? extractDomainSmart(data.web_bid.origin_url)
 | 
				
			||||||
 | 
					                  : extractDomainSmart(data.origin_url)}
 | 
				
			||||||
 | 
					              </Badge>
 | 
				
			||||||
 | 
					            </Box>
 | 
				
			||||||
 | 
					            {isIBid(data) && (
 | 
				
			||||||
 | 
					              <Tooltip label={upperFirst(getMode(data))}>
 | 
				
			||||||
 | 
					                <ActionIcon
 | 
				
			||||||
 | 
					                  onClick={handleToogleMode}
 | 
				
			||||||
 | 
					                  color={getMode(data) === "live" ? "teal" : "orange"}
 | 
				
			||||||
 | 
					                  size="xs"
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                  {getMode(data) === "live" ? (
 | 
				
			||||||
 | 
					                    <IconLivePhoto size={14} />
 | 
				
			||||||
 | 
					                  ) : (
 | 
				
			||||||
 | 
					                    <IconTestPipe size={14} />
 | 
				
			||||||
 | 
					                  )}
 | 
				
			||||||
 | 
					                </ActionIcon>
 | 
				
			||||||
 | 
					              </Tooltip>
 | 
				
			||||||
            )}
 | 
					            )}
 | 
				
			||||||
            size="xs"
 | 
					          </Box>
 | 
				
			||||||
          >
 | 
					
 | 
				
			||||||
            {isIBid(data)
 | 
					          {isIBid(data) && moment(data.close_time).isSame(moment(), "day") && (
 | 
				
			||||||
              ? extractDomainSmart(data.web_bid.origin_url)
 | 
					            <div className="w-[14px] h-[14px] rounded-full bg-green-600 animate-pulse"></div>
 | 
				
			||||||
              : extractDomainSmart(data.origin_url)}
 | 
					          )}
 | 
				
			||||||
          </Badge>
 | 
					 | 
				
			||||||
        </Box>
 | 
					        </Box>
 | 
				
			||||||
      </Box>
 | 
					      </Box>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,122 +1,180 @@
 | 
				
			||||||
import { Avatar, Button, LoadingOverlay, Menu, Modal, PasswordInput } from '@mantine/core';
 | 
					import {
 | 
				
			||||||
import { useForm, zodResolver } from '@mantine/form';
 | 
					  Avatar,
 | 
				
			||||||
import { useDisclosure } from '@mantine/hooks';
 | 
					  Button,
 | 
				
			||||||
import { IconKey, IconLogout, IconSettings, IconUser } from '@tabler/icons-react';
 | 
					  LoadingOverlay,
 | 
				
			||||||
import { useState } from 'react';
 | 
					  Menu,
 | 
				
			||||||
import { useNavigate } from 'react-router';
 | 
					  Modal,
 | 
				
			||||||
import { Link } from 'react-router-dom';
 | 
					  PasswordInput,
 | 
				
			||||||
import { z } from 'zod';
 | 
					} from "@mantine/core";
 | 
				
			||||||
import { changePassword, logout } from '../apis/auth';
 | 
					import { useForm, zodResolver } from "@mantine/form";
 | 
				
			||||||
import { useConfirmStore } from '../lib/zustand/use-confirm';
 | 
					import { useDisclosure } from "@mantine/hooks";
 | 
				
			||||||
import Links from '../system/links';
 | 
					import {
 | 
				
			||||||
 | 
					  IconCode,
 | 
				
			||||||
 | 
					  IconKey,
 | 
				
			||||||
 | 
					  IconLogout,
 | 
				
			||||||
 | 
					  IconSettings,
 | 
				
			||||||
 | 
					  IconUser,
 | 
				
			||||||
 | 
					} from "@tabler/icons-react";
 | 
				
			||||||
 | 
					import { useState } from "react";
 | 
				
			||||||
 | 
					import { useNavigate } from "react-router";
 | 
				
			||||||
 | 
					import { Link } from "react-router-dom";
 | 
				
			||||||
 | 
					import { z } from "zod";
 | 
				
			||||||
 | 
					import { changePassword, logout } from "../apis/auth";
 | 
				
			||||||
 | 
					import { useConfirmStore } from "../lib/zustand/use-confirm";
 | 
				
			||||||
 | 
					import Links from "../system/links";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const schema = z
 | 
					const schema = z
 | 
				
			||||||
    .object({
 | 
					  .object({
 | 
				
			||||||
        currentPassword: z.string().min(6, 'Current password must be at least 6 characters'),
 | 
					    currentPassword: z
 | 
				
			||||||
        newPassword: z.string().min(6, 'New password must be at least 6 characters'),
 | 
					      .string()
 | 
				
			||||||
        confirmPassword: z.string(),
 | 
					      .min(6, "Current password must be at least 6 characters"),
 | 
				
			||||||
    })
 | 
					    newPassword: z
 | 
				
			||||||
    .refine((data) => data.newPassword === data.confirmPassword, {
 | 
					      .string()
 | 
				
			||||||
        path: ['confirmPassword'],
 | 
					      .min(6, "New password must be at least 6 characters"),
 | 
				
			||||||
        message: 'Passwords do not match',
 | 
					    confirmPassword: z.string(),
 | 
				
			||||||
    });
 | 
					  })
 | 
				
			||||||
 | 
					  .refine((data) => data.newPassword === data.confirmPassword, {
 | 
				
			||||||
 | 
					    path: ["confirmPassword"],
 | 
				
			||||||
 | 
					    message: "Passwords do not match",
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default function UserMenu() {
 | 
					export default function UserMenu() {
 | 
				
			||||||
    const [opened, { open, close }] = useDisclosure(false);
 | 
					  const [opened, { open, close }] = useDisclosure(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const { setConfirm } = useConfirmStore();
 | 
					  const { setConfirm } = useConfirmStore();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const [loading, setLoading] = useState(false);
 | 
					  const [loading, setLoading] = useState(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const navigate = useNavigate();
 | 
					  const navigate = useNavigate();
 | 
				
			||||||
    const form = useForm({
 | 
					  const form = useForm({
 | 
				
			||||||
        initialValues: {
 | 
					    initialValues: {
 | 
				
			||||||
            currentPassword: '',
 | 
					      currentPassword: "",
 | 
				
			||||||
            newPassword: '',
 | 
					      newPassword: "",
 | 
				
			||||||
            confirmPassword: '',
 | 
					      confirmPassword: "",
 | 
				
			||||||
        },
 | 
					    },
 | 
				
			||||||
        validate: zodResolver(schema),
 | 
					    validate: zodResolver(schema),
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleSubmit = async (values: typeof form.values) => {
 | 
				
			||||||
 | 
					    await handleChangePassword(values);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleLogout = async () => {
 | 
				
			||||||
 | 
					    setConfirm({
 | 
				
			||||||
 | 
					      title: "Are you wan't to logout?",
 | 
				
			||||||
 | 
					      message: "This account will logout !",
 | 
				
			||||||
 | 
					      okButton: { value: "Logout" },
 | 
				
			||||||
 | 
					      handleOk: async () => {
 | 
				
			||||||
 | 
					        const data = await logout();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (data && data.data) {
 | 
				
			||||||
 | 
					          navigate(Links.LOGIN);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const handleSubmit = async (values: typeof form.values) => {
 | 
					  const handleChangePassword = async (values: typeof form.values) => {
 | 
				
			||||||
        await handleChangePassword(values);
 | 
					    setConfirm({
 | 
				
			||||||
    };
 | 
					      title: "Are you wan't to update password",
 | 
				
			||||||
 | 
					      message: "This account will change password !",
 | 
				
			||||||
    const handleLogout = async () => {
 | 
					      okButton: { value: "Sure" },
 | 
				
			||||||
        setConfirm({
 | 
					      handleOk: async () => {
 | 
				
			||||||
            title: "Are you wan't to logout?",
 | 
					        setLoading(true);
 | 
				
			||||||
            message: 'This account will logout !',
 | 
					        const data = await changePassword({
 | 
				
			||||||
            okButton: { value: 'Logout' },
 | 
					          newPassword: values.newPassword,
 | 
				
			||||||
            handleOk: async () => {
 | 
					          password: values.currentPassword,
 | 
				
			||||||
                const data = await logout();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                if (data && data.data) {
 | 
					 | 
				
			||||||
                    navigate(Links.LOGIN);
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const handleChangePassword = async (values: typeof form.values) => {
 | 
					        setLoading(false);
 | 
				
			||||||
        setConfirm({
 | 
					 | 
				
			||||||
            title: "Are you wan't to update password",
 | 
					 | 
				
			||||||
            message: 'This account will change password !',
 | 
					 | 
				
			||||||
            okButton: { value: 'Sure' },
 | 
					 | 
				
			||||||
            handleOk: async () => {
 | 
					 | 
				
			||||||
                setLoading(true);
 | 
					 | 
				
			||||||
                const data = await changePassword({
 | 
					 | 
				
			||||||
                    newPassword: values.newPassword,
 | 
					 | 
				
			||||||
                    password: values.currentPassword,
 | 
					 | 
				
			||||||
                });
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                setLoading(false);
 | 
					        if (data && data.data) {
 | 
				
			||||||
 | 
					          navigate(Links.LOGIN);
 | 
				
			||||||
 | 
					          close();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                if (data && data.data) {
 | 
					  return (
 | 
				
			||||||
                    navigate(Links.LOGIN);
 | 
					    <>
 | 
				
			||||||
                    close();
 | 
					      <Menu shadow="md" width={200}>
 | 
				
			||||||
                }
 | 
					        <Menu.Target>
 | 
				
			||||||
            },
 | 
					          <Avatar color="blue" radius="xl" className="cursor-pointer">
 | 
				
			||||||
        });
 | 
					            <IconUser size={20} />
 | 
				
			||||||
    };
 | 
					          </Avatar>
 | 
				
			||||||
 | 
					        </Menu.Target>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					        <Menu.Dropdown>
 | 
				
			||||||
        <>
 | 
					          <Menu.Label>Account</Menu.Label>
 | 
				
			||||||
            <Menu shadow="md" width={200}>
 | 
					          <Menu.Item onClick={open} leftSection={<IconSettings size={14} />}>
 | 
				
			||||||
                <Menu.Target>
 | 
					            Change password
 | 
				
			||||||
                    <Avatar color="blue" radius="xl" className="cursor-pointer">
 | 
					          </Menu.Item>
 | 
				
			||||||
                        <IconUser size={20} />
 | 
					          <Menu.Item
 | 
				
			||||||
                    </Avatar>
 | 
					            component={Link}
 | 
				
			||||||
                </Menu.Target>
 | 
					            to={Links.GENERATE_KEYS}
 | 
				
			||||||
 | 
					            leftSection={<IconKey size={14} />}
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            Keys
 | 
				
			||||||
 | 
					          </Menu.Item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                <Menu.Dropdown>
 | 
					          <Menu.Item
 | 
				
			||||||
                    <Menu.Label>Account</Menu.Label>
 | 
					            component={Link}
 | 
				
			||||||
                    <Menu.Item onClick={open} leftSection={<IconSettings size={14} />}>
 | 
					            to={Links.CONFIGS}
 | 
				
			||||||
                        Change password
 | 
					            leftSection={<IconCode size={14} />}
 | 
				
			||||||
                    </Menu.Item>
 | 
					          >
 | 
				
			||||||
                    <Menu.Item component={Link} to={Links.GENERATE_KEYS} leftSection={<IconKey size={14} />}>
 | 
					            Configs
 | 
				
			||||||
                        Keys
 | 
					          </Menu.Item>
 | 
				
			||||||
                    </Menu.Item>
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    <Menu.Divider />
 | 
					          <Menu.Divider />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    <Menu.Item onClick={handleLogout} color="red" leftSection={<IconLogout size={14} />}>
 | 
					          <Menu.Item
 | 
				
			||||||
                        Logout
 | 
					            onClick={handleLogout}
 | 
				
			||||||
                    </Menu.Item>
 | 
					            color="red"
 | 
				
			||||||
                </Menu.Dropdown>
 | 
					            leftSection={<IconLogout size={14} />}
 | 
				
			||||||
            </Menu>
 | 
					          >
 | 
				
			||||||
 | 
					            Logout
 | 
				
			||||||
 | 
					          </Menu.Item>
 | 
				
			||||||
 | 
					        </Menu.Dropdown>
 | 
				
			||||||
 | 
					      </Menu>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            <Modal className="relative" opened={opened} onClose={close} title="Change password" centered>
 | 
					      <Modal
 | 
				
			||||||
                <form onSubmit={form.onSubmit(handleSubmit)} className="flex flex-col gap-2.5">
 | 
					        className="relative"
 | 
				
			||||||
                    <PasswordInput size="sm" label="Current password" {...form.getInputProps('currentPassword')} />
 | 
					        opened={opened}
 | 
				
			||||||
                    <PasswordInput size="sm" label="New password" {...form.getInputProps('newPassword')} />
 | 
					        onClose={close}
 | 
				
			||||||
                    <PasswordInput size="sm" label="Confirm password" {...form.getInputProps('confirmPassword')} />
 | 
					        title="Change password"
 | 
				
			||||||
                    <Button type="submit" fullWidth size="sm" mt="md">
 | 
					        centered
 | 
				
			||||||
                        Change
 | 
					      >
 | 
				
			||||||
                    </Button>
 | 
					        <form
 | 
				
			||||||
                </form>
 | 
					          onSubmit={form.onSubmit(handleSubmit)}
 | 
				
			||||||
 | 
					          className="flex flex-col gap-2.5"
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <PasswordInput
 | 
				
			||||||
 | 
					            size="sm"
 | 
				
			||||||
 | 
					            label="Current password"
 | 
				
			||||||
 | 
					            {...form.getInputProps("currentPassword")}
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					          <PasswordInput
 | 
				
			||||||
 | 
					            size="sm"
 | 
				
			||||||
 | 
					            label="New password"
 | 
				
			||||||
 | 
					            {...form.getInputProps("newPassword")}
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					          <PasswordInput
 | 
				
			||||||
 | 
					            size="sm"
 | 
				
			||||||
 | 
					            label="Confirm password"
 | 
				
			||||||
 | 
					            {...form.getInputProps("confirmPassword")}
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					          <Button type="submit" fullWidth size="sm" mt="md">
 | 
				
			||||||
 | 
					            Change
 | 
				
			||||||
 | 
					          </Button>
 | 
				
			||||||
 | 
					        </form>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                <LoadingOverlay visible={loading} zIndex={1000} overlayProps={{ blur: 2 }} />
 | 
					        <LoadingOverlay
 | 
				
			||||||
            </Modal>
 | 
					          visible={loading}
 | 
				
			||||||
        </>
 | 
					          zIndex={1000}
 | 
				
			||||||
    );
 | 
					          overlayProps={{ blur: 2 }}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      </Modal>
 | 
				
			||||||
 | 
					    </>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,191 @@
 | 
				
			||||||
 | 
					/* eslint-disable @typescript-eslint/no-unused-vars */
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  Button,
 | 
				
			||||||
 | 
					  LoadingOverlay,
 | 
				
			||||||
 | 
					  Modal,
 | 
				
			||||||
 | 
					  ModalProps,
 | 
				
			||||||
 | 
					  Select,
 | 
				
			||||||
 | 
					  Textarea,
 | 
				
			||||||
 | 
					  TextInput,
 | 
				
			||||||
 | 
					} from "@mantine/core";
 | 
				
			||||||
 | 
					import { useForm, zodResolver } from "@mantine/form";
 | 
				
			||||||
 | 
					import _ from "lodash";
 | 
				
			||||||
 | 
					import { useEffect, useRef, useState } from "react";
 | 
				
			||||||
 | 
					import { z } from "zod";
 | 
				
			||||||
 | 
					import { createScrapConfig, updateScrapConfig } from "../../apis/scrap";
 | 
				
			||||||
 | 
					import { useConfirmStore } from "../../lib/zustand/use-confirm";
 | 
				
			||||||
 | 
					import { IScrapConfig, IWebBid } from "../../system/type";
 | 
				
			||||||
 | 
					export interface IScrapConfigModelProps extends ModalProps {
 | 
				
			||||||
 | 
					  data: IWebBid | null;
 | 
				
			||||||
 | 
					  onUpdated?: () => void;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const schema = z.object({
 | 
				
			||||||
 | 
					  search_url: z
 | 
				
			||||||
 | 
					    .string()
 | 
				
			||||||
 | 
					    .url({ message: "Url is invalid" })
 | 
				
			||||||
 | 
					    .min(1, { message: "Url is required" }),
 | 
				
			||||||
 | 
					  keywords: z
 | 
				
			||||||
 | 
					    .string({ message: "Keyword is required" })
 | 
				
			||||||
 | 
					    .min(1, { message: "Keyword is required" })
 | 
				
			||||||
 | 
					    .optional(),
 | 
				
			||||||
 | 
					  enable: z.enum(["1", "0"], { required_error: "Enable is required" }),
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function ScrapConfigModal({
 | 
				
			||||||
 | 
					  data,
 | 
				
			||||||
 | 
					  onUpdated,
 | 
				
			||||||
 | 
					  ...props
 | 
				
			||||||
 | 
					}: IScrapConfigModelProps) {
 | 
				
			||||||
 | 
					  const form = useForm<IScrapConfig>({
 | 
				
			||||||
 | 
					    validate: zodResolver(schema),
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const [loading, setLoading] = useState(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const prevData = useRef<IScrapConfig | null>(data?.scrap_config);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const { setConfirm } = useConfirmStore();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleSubmit = async (values: typeof form.values) => {
 | 
				
			||||||
 | 
					    if (data?.scrap_config) {
 | 
				
			||||||
 | 
					      setConfirm({
 | 
				
			||||||
 | 
					        title: "Update ?",
 | 
				
			||||||
 | 
					        message: `This config will be update`,
 | 
				
			||||||
 | 
					        handleOk: async () => {
 | 
				
			||||||
 | 
					          setLoading(true);
 | 
				
			||||||
 | 
					          const result = await updateScrapConfig({
 | 
				
			||||||
 | 
					            ...values,
 | 
				
			||||||
 | 
					            id: data.scrap_config.id,
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					          setLoading(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          if (!result) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          props.onClose();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          if (onUpdated) {
 | 
				
			||||||
 | 
					            onUpdated();
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        okButton: {
 | 
				
			||||||
 | 
					          color: "blue",
 | 
				
			||||||
 | 
					          value: "Update",
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      setLoading(true);
 | 
				
			||||||
 | 
					      const result = await createScrapConfig({
 | 
				
			||||||
 | 
					        ...values,
 | 
				
			||||||
 | 
					        web_id: data?.id || 0,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      setLoading(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (!result) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      props.onClose();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (onUpdated) {
 | 
				
			||||||
 | 
					        onUpdated();
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    form.reset();
 | 
				
			||||||
 | 
					    if (!data) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const values = {
 | 
				
			||||||
 | 
					      ...data.scrap_config,
 | 
				
			||||||
 | 
					      enable: (data.scrap_config?.enable === undefined
 | 
				
			||||||
 | 
					        ? "1"
 | 
				
			||||||
 | 
					        : data.scrap_config.enable
 | 
				
			||||||
 | 
					        ? "1"
 | 
				
			||||||
 | 
					        : "0") as "0" | "1",
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    form.setValues(values);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    prevData.current = values;
 | 
				
			||||||
 | 
					    // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
				
			||||||
 | 
					  }, [data]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    if (!props.opened) {
 | 
				
			||||||
 | 
					      form.reset();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
				
			||||||
 | 
					  }, [props.opened]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <Modal
 | 
				
			||||||
 | 
					      className="relative"
 | 
				
			||||||
 | 
					      classNames={{
 | 
				
			||||||
 | 
					        header: "!flex !item-center !justify-center w-full",
 | 
				
			||||||
 | 
					      }}
 | 
				
			||||||
 | 
					      {...props}
 | 
				
			||||||
 | 
					      size={"xl"}
 | 
				
			||||||
 | 
					      title={<span className="text-xl font-bold">Scrap config</span>}
 | 
				
			||||||
 | 
					      centered
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <form
 | 
				
			||||||
 | 
					        onSubmit={form.onSubmit(handleSubmit)}
 | 
				
			||||||
 | 
					        className="grid grid-cols-2 gap-2.5"
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <Select
 | 
				
			||||||
 | 
					          className="col-span-2"
 | 
				
			||||||
 | 
					          label="Enable scrape"
 | 
				
			||||||
 | 
					          defaultChecked={true}
 | 
				
			||||||
 | 
					          defaultValue={"1"}
 | 
				
			||||||
 | 
					          data={[
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					              label: "Enbale",
 | 
				
			||||||
 | 
					              value: "1",
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					              label: "Disable",
 | 
				
			||||||
 | 
					              value: "0",
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					          ]}
 | 
				
			||||||
 | 
					          {...form.getInputProps("enable")}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					        <TextInput
 | 
				
			||||||
 | 
					          className="col-span-2"
 | 
				
			||||||
 | 
					          size="sm"
 | 
				
			||||||
 | 
					          label="Search url"
 | 
				
			||||||
 | 
					          withAsterisk
 | 
				
			||||||
 | 
					          description="Replace query keyword in url with phrase {{keyword}}"
 | 
				
			||||||
 | 
					          placeholder="https://www.abc.com/search?q={{keyword}}"
 | 
				
			||||||
 | 
					          {...form.getInputProps("search_url")}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <Textarea
 | 
				
			||||||
 | 
					          className="col-span-2"
 | 
				
			||||||
 | 
					          size="sm"
 | 
				
			||||||
 | 
					          label="Keywords"
 | 
				
			||||||
 | 
					          rows={4}
 | 
				
			||||||
 | 
					          placeholder="msg: Cisco"
 | 
				
			||||||
 | 
					          description={"Different keywords must be separated by commas."}
 | 
				
			||||||
 | 
					          {...form.getInputProps("keywords")}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <Button
 | 
				
			||||||
 | 
					          disabled={_.isEqual(form.getValues(), prevData.current)}
 | 
				
			||||||
 | 
					          className="col-span-2"
 | 
				
			||||||
 | 
					          type="submit"
 | 
				
			||||||
 | 
					          fullWidth
 | 
				
			||||||
 | 
					          size="sm"
 | 
				
			||||||
 | 
					          mt="md"
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          {data?.scrap_config ? "Update" : "Create"}
 | 
				
			||||||
 | 
					        </Button>
 | 
				
			||||||
 | 
					      </form>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <LoadingOverlay
 | 
				
			||||||
 | 
					        visible={loading}
 | 
				
			||||||
 | 
					        zIndex={1000}
 | 
				
			||||||
 | 
					        overlayProps={{ blur: 2 }}
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					    </Modal>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1,109 +1,148 @@
 | 
				
			||||||
/* eslint-disable @typescript-eslint/no-unused-vars */
 | 
					/* eslint-disable @typescript-eslint/no-unused-vars */
 | 
				
			||||||
import { Button, LoadingOverlay, Modal, ModalProps, PasswordInput, TextInput } from '@mantine/core';
 | 
					import {
 | 
				
			||||||
import { useForm, zodResolver } from '@mantine/form';
 | 
					  Button,
 | 
				
			||||||
import _ from 'lodash';
 | 
					  LoadingOverlay,
 | 
				
			||||||
import { useEffect, useRef, useState } from 'react';
 | 
					  Modal,
 | 
				
			||||||
import { z } from 'zod';
 | 
					  ModalProps,
 | 
				
			||||||
import { updateWebBid } from '../../apis/web-bid';
 | 
					  PasswordInput,
 | 
				
			||||||
import { useConfirmStore } from '../../lib/zustand/use-confirm';
 | 
					  TextInput,
 | 
				
			||||||
import { IWebBid } from '../../system/type';
 | 
					} from "@mantine/core";
 | 
				
			||||||
 | 
					import { useForm, zodResolver } from "@mantine/form";
 | 
				
			||||||
 | 
					import _ from "lodash";
 | 
				
			||||||
 | 
					import { useEffect, useRef, useState } from "react";
 | 
				
			||||||
 | 
					import { z } from "zod";
 | 
				
			||||||
 | 
					import { updateWebBid } from "../../apis/web-bid";
 | 
				
			||||||
 | 
					import { useConfirmStore } from "../../lib/zustand/use-confirm";
 | 
				
			||||||
 | 
					import { IWebBid } from "../../system/type";
 | 
				
			||||||
export interface IWebBidModelProps extends ModalProps {
 | 
					export interface IWebBidModelProps extends ModalProps {
 | 
				
			||||||
    data: IWebBid | null;
 | 
					  data: IWebBid | null;
 | 
				
			||||||
    onUpdated?: () => void;
 | 
					  onUpdated?: () => void;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const schema = z.object({
 | 
					const schema = z.object({
 | 
				
			||||||
    username: z.string().min(1, { message: 'Username is required' }),
 | 
					  username: z.string().min(1, { message: "Username is required" }),
 | 
				
			||||||
    password: z.string().min(6, { message: 'Password must be at least 6 characters long' }),
 | 
					  password: z
 | 
				
			||||||
 | 
					    .string()
 | 
				
			||||||
 | 
					    .min(6, { message: "Password must be at least 6 characters long" }),
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default function WebAccountModal({ data, onUpdated, ...props }: IWebBidModelProps) {
 | 
					export default function WebAccountModal({
 | 
				
			||||||
    const form = useForm({
 | 
					  data,
 | 
				
			||||||
        validate: zodResolver(schema),
 | 
					  onUpdated,
 | 
				
			||||||
    });
 | 
					  ...props
 | 
				
			||||||
 | 
					}: IWebBidModelProps) {
 | 
				
			||||||
 | 
					  const form = useForm({
 | 
				
			||||||
 | 
					    validate: zodResolver(schema),
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const [loading, setLoading] = useState(false);
 | 
					  const [loading, setLoading] = useState(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const prevData = useRef<IWebBid | null>(data);
 | 
					  const prevData = useRef<IWebBid | null>(data);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const { setConfirm } = useConfirmStore();
 | 
					  const { setConfirm } = useConfirmStore();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const handleSubmit = async (values: typeof form.values) => {
 | 
					  const handleSubmit = async (values: typeof form.values) => {
 | 
				
			||||||
        if (data) {
 | 
					    if (data) {
 | 
				
			||||||
            setConfirm({
 | 
					      setConfirm({
 | 
				
			||||||
                title: 'Update ?',
 | 
					        title: "Update ?",
 | 
				
			||||||
                message: `This account will be update`,
 | 
					        message: `This account will be update`,
 | 
				
			||||||
                handleOk: async () => {
 | 
					        handleOk: async () => {
 | 
				
			||||||
                    setLoading(true);
 | 
					          setLoading(true);
 | 
				
			||||||
                    const result = await updateWebBid(values);
 | 
					          const result = await updateWebBid(values);
 | 
				
			||||||
                    setLoading(false);
 | 
					          setLoading(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    if (!result) return;
 | 
					          if (!result) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    props.onClose();
 | 
					          props.onClose();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    if (onUpdated) {
 | 
					          if (onUpdated) {
 | 
				
			||||||
                        onUpdated();
 | 
					            onUpdated();
 | 
				
			||||||
                    }
 | 
					          }
 | 
				
			||||||
                },
 | 
					        },
 | 
				
			||||||
                okButton: {
 | 
					        okButton: {
 | 
				
			||||||
                    color: 'blue',
 | 
					          color: "blue",
 | 
				
			||||||
                    value: 'Update',
 | 
					          value: "Update",
 | 
				
			||||||
                },
 | 
					        },
 | 
				
			||||||
            });
 | 
					      });
 | 
				
			||||||
        } else {
 | 
					    } else {
 | 
				
			||||||
            setLoading(true);
 | 
					      setLoading(true);
 | 
				
			||||||
            const result = await updateWebBid(values);
 | 
					      const result = await updateWebBid(values);
 | 
				
			||||||
            setLoading(false);
 | 
					      setLoading(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (!result) return;
 | 
					      if (!result) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            props.onClose();
 | 
					      props.onClose();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (onUpdated) {
 | 
					      if (onUpdated) {
 | 
				
			||||||
                onUpdated();
 | 
					        onUpdated();
 | 
				
			||||||
            }
 | 
					      }
 | 
				
			||||||
        }
 | 
					    }
 | 
				
			||||||
    };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    useEffect(() => {
 | 
					  useEffect(() => {
 | 
				
			||||||
        form.reset();
 | 
					    form.reset();
 | 
				
			||||||
        if (!data) return;
 | 
					    if (!data) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        form.setValues(data);
 | 
					    form.setValues(data);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        prevData.current = data;
 | 
					    prevData.current = data;
 | 
				
			||||||
        // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
					    // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
				
			||||||
    }, [data]);
 | 
					  }, [data]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    useEffect(() => {
 | 
					  useEffect(() => {
 | 
				
			||||||
        if (!props.opened) {
 | 
					    if (!props.opened) {
 | 
				
			||||||
            form.reset();
 | 
					      form.reset();
 | 
				
			||||||
        }
 | 
					    }
 | 
				
			||||||
        // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
					    // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
				
			||||||
    }, [props.opened]);
 | 
					  }, [props.opened]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					  return (
 | 
				
			||||||
        <Modal
 | 
					    <Modal
 | 
				
			||||||
            className="relative"
 | 
					      className="relative"
 | 
				
			||||||
            classNames={{
 | 
					      classNames={{
 | 
				
			||||||
                header: '!flex !item-center !justify-center w-full',
 | 
					        header: "!flex !item-center !justify-center w-full",
 | 
				
			||||||
            }}
 | 
					      }}
 | 
				
			||||||
            {...props}
 | 
					      {...props}
 | 
				
			||||||
            size={'xl'}
 | 
					      size={"xl"}
 | 
				
			||||||
            title={<span className="text-xl font-bold">Account</span>}
 | 
					      title={<span className="text-xl font-bold">Account</span>}
 | 
				
			||||||
            centered
 | 
					      centered
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <form
 | 
				
			||||||
 | 
					        onSubmit={form.onSubmit(handleSubmit)}
 | 
				
			||||||
 | 
					        className="grid grid-cols-2 gap-2.5"
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <TextInput
 | 
				
			||||||
 | 
					          withAsterisk
 | 
				
			||||||
 | 
					          className="col-span-2"
 | 
				
			||||||
 | 
					          size="sm"
 | 
				
			||||||
 | 
					          label="Username"
 | 
				
			||||||
 | 
					          {...form.getInputProps("username")}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					        <PasswordInput
 | 
				
			||||||
 | 
					          withAsterisk
 | 
				
			||||||
 | 
					          className="col-span-2"
 | 
				
			||||||
 | 
					          size="sm"
 | 
				
			||||||
 | 
					          label="Password"
 | 
				
			||||||
 | 
					          {...form.getInputProps("password")}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <Button
 | 
				
			||||||
 | 
					          disabled={_.isEqual(form.getValues(), prevData.current)}
 | 
				
			||||||
 | 
					          className="col-span-2"
 | 
				
			||||||
 | 
					          type="submit"
 | 
				
			||||||
 | 
					          fullWidth
 | 
				
			||||||
 | 
					          size="sm"
 | 
				
			||||||
 | 
					          mt="md"
 | 
				
			||||||
        >
 | 
					        >
 | 
				
			||||||
            <form onSubmit={form.onSubmit(handleSubmit)} className="grid grid-cols-2 gap-2.5">
 | 
					          {data ? "Update" : "Create"}
 | 
				
			||||||
                <TextInput className="col-span-2" size="sm" label="Username" {...form.getInputProps('username')} />
 | 
					        </Button>
 | 
				
			||||||
                <PasswordInput className="col-span-2" size="sm" label="Password" {...form.getInputProps('password')} />
 | 
					      </form>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                <Button disabled={_.isEqual(form.getValues(), prevData.current)} className="col-span-2" type="submit" fullWidth size="sm" mt="md">
 | 
					      <LoadingOverlay
 | 
				
			||||||
                    {data ? 'Update' : 'Create'}
 | 
					        visible={loading}
 | 
				
			||||||
                </Button>
 | 
					        zIndex={1000}
 | 
				
			||||||
            </form>
 | 
					        overlayProps={{ blur: 2 }}
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
            <LoadingOverlay visible={loading} zIndex={1000} overlayProps={{ blur: 2 }} />
 | 
					    </Modal>
 | 
				
			||||||
        </Modal>
 | 
					  );
 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -26,12 +26,14 @@ const schema = {
 | 
				
			||||||
    .number({ message: "Arrival offset seconds is required" })
 | 
					    .number({ message: "Arrival offset seconds is required" })
 | 
				
			||||||
    .refine((val) => val >= 60, {
 | 
					    .refine((val) => val >= 60, {
 | 
				
			||||||
      message: "Arrival offset seconds must be at least 60 seconds (1 minute)",
 | 
					      message: "Arrival offset seconds must be at least 60 seconds (1 minute)",
 | 
				
			||||||
    }),
 | 
					    })
 | 
				
			||||||
    early_tracking_seconds: z
 | 
					    .optional(),
 | 
				
			||||||
 | 
					  early_tracking_seconds: z
 | 
				
			||||||
    .number({ message: "Early login seconds is required" })
 | 
					    .number({ message: "Early login seconds is required" })
 | 
				
			||||||
    .refine((val) => val >= 600, {
 | 
					    .refine((val) => val >= 600, {
 | 
				
			||||||
      message: "Early login seconds must be at least 600 seconds (10 minute)",
 | 
					      message: "Early login seconds must be at least 600 seconds (10 minute)",
 | 
				
			||||||
    }),
 | 
					    })
 | 
				
			||||||
 | 
					    .optional(),
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default function WebBidModal({
 | 
					export default function WebBidModal({
 | 
				
			||||||
| 
						 | 
					@ -56,11 +58,7 @@ export default function WebBidModal({
 | 
				
			||||||
        message: `This web will be update`,
 | 
					        message: `This web will be update`,
 | 
				
			||||||
        handleOk: async () => {
 | 
					        handleOk: async () => {
 | 
				
			||||||
          setLoading(true);
 | 
					          setLoading(true);
 | 
				
			||||||
          console.log(
 | 
					
 | 
				
			||||||
            "%csrc/components/web-bid/web-bid-modal.tsx:54 values",
 | 
					 | 
				
			||||||
            "color: #007acc;",
 | 
					 | 
				
			||||||
            values
 | 
					 | 
				
			||||||
          );
 | 
					 | 
				
			||||||
          const result = await updateWebBid(values);
 | 
					          const result = await updateWebBid(values);
 | 
				
			||||||
          setLoading(false);
 | 
					          setLoading(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -78,14 +76,19 @@ export default function WebBidModal({
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      const { url, origin_url, arrival_offset_seconds, early_tracking_seconds } = values;
 | 
					      const {
 | 
				
			||||||
 | 
					        url,
 | 
				
			||||||
 | 
					        origin_url,
 | 
				
			||||||
 | 
					        arrival_offset_seconds,
 | 
				
			||||||
 | 
					        early_tracking_seconds,
 | 
				
			||||||
 | 
					      } = values;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      setLoading(true);
 | 
					      setLoading(true);
 | 
				
			||||||
      const result = await createWebBid({
 | 
					      const result = await createWebBid({
 | 
				
			||||||
        url,
 | 
					        url,
 | 
				
			||||||
        origin_url,
 | 
					        origin_url,
 | 
				
			||||||
        arrival_offset_seconds,
 | 
					        arrival_offset_seconds,
 | 
				
			||||||
        early_tracking_seconds
 | 
					        early_tracking_seconds,
 | 
				
			||||||
      } as IWebBid);
 | 
					      } as IWebBid);
 | 
				
			||||||
      setLoading(false);
 | 
					      setLoading(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -157,9 +160,9 @@ export default function WebBidModal({
 | 
				
			||||||
          description="Note: that only integer minutes are accepted."
 | 
					          description="Note: that only integer minutes are accepted."
 | 
				
			||||||
          className="col-span-2"
 | 
					          className="col-span-2"
 | 
				
			||||||
          size="sm"
 | 
					          size="sm"
 | 
				
			||||||
          label={`Arrival offset seconds (${
 | 
					          label={`Arrival offset seconds (${formatTimeFromMinutes(
 | 
				
			||||||
             formatTimeFromMinutes(form.getValues()["arrival_offset_seconds"] / 60)
 | 
					            form.getValues()["arrival_offset_seconds"] / 60
 | 
				
			||||||
          })`}
 | 
					          )})`}
 | 
				
			||||||
          placeholder="msg: 300"
 | 
					          placeholder="msg: 300"
 | 
				
			||||||
          {...form.getInputProps("arrival_offset_seconds")}
 | 
					          {...form.getInputProps("arrival_offset_seconds")}
 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
| 
						 | 
					@ -167,9 +170,9 @@ export default function WebBidModal({
 | 
				
			||||||
          description="Note: that only integer minutes are accepted."
 | 
					          description="Note: that only integer minutes are accepted."
 | 
				
			||||||
          className="col-span-2"
 | 
					          className="col-span-2"
 | 
				
			||||||
          size="sm"
 | 
					          size="sm"
 | 
				
			||||||
          label={`Early tracking seconds (${
 | 
					          label={`Early tracking seconds (${formatTimeFromMinutes(
 | 
				
			||||||
             formatTimeFromMinutes(form.getValues()["early_tracking_seconds"] / 60)
 | 
					            form.getValues()["early_tracking_seconds"] / 60
 | 
				
			||||||
          })`}
 | 
					          )})`}
 | 
				
			||||||
          placeholder="msg: 600"
 | 
					          placeholder="msg: 600"
 | 
				
			||||||
          {...form.getInputProps("early_tracking_seconds")}
 | 
					          {...form.getInputProps("early_tracking_seconds")}
 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,8 +1,13 @@
 | 
				
			||||||
 const constants = {
 | 
					const constants = {
 | 
				
			||||||
    grays: 'https://www.grays.com',
 | 
					  grays: "https://www.grays.com",
 | 
				
			||||||
    pickles:'https://www.pickles.com.au'
 | 
					  pickles: "https://www.pickles.com.au",
 | 
				
			||||||
}
 | 
					  allbids: "https://www.allbids.com.au",
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const haveHistories = [constants.grays, constants.pickles]
 | 
					export const haveHistories = [
 | 
				
			||||||
 | 
					  constants.grays,
 | 
				
			||||||
 | 
					  constants.pickles,
 | 
				
			||||||
 | 
					  constants.allbids,
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default constants
 | 
					export default constants;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -116,5 +116,3 @@ export const removeFalsy = (data: { [key: string]: string | number }) => {
 | 
				
			||||||
    return prev;
 | 
					    return prev;
 | 
				
			||||||
  }, {} as { [key: string]: string | number });
 | 
					  }, {} as { [key: string]: string | number });
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -12,31 +12,39 @@ import { useDisclosure } from "@mantine/hooks";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  IconAd,
 | 
					  IconAd,
 | 
				
			||||||
  IconAdOff,
 | 
					  IconAdOff,
 | 
				
			||||||
 | 
					  IconCode,
 | 
				
			||||||
  IconEdit,
 | 
					  IconEdit,
 | 
				
			||||||
  IconHammer,
 | 
					  IconHammer,
 | 
				
			||||||
  IconHistory,
 | 
					  IconHistory,
 | 
				
			||||||
  IconMenu,
 | 
					  IconMenu,
 | 
				
			||||||
 | 
					  IconPlayerRecord,
 | 
				
			||||||
  IconPlus,
 | 
					  IconPlus,
 | 
				
			||||||
  IconTrash,
 | 
					  IconTrash,
 | 
				
			||||||
} from "@tabler/icons-react";
 | 
					} from "@tabler/icons-react";
 | 
				
			||||||
import _ from "lodash";
 | 
					import _ from "lodash";
 | 
				
			||||||
import { useMemo, useRef, useState } from "react";
 | 
					import { useMemo, useRef, useState } from "react";
 | 
				
			||||||
import { deleteBid, deletesBid, getBids, toggleBid } from "../apis/bid";
 | 
					import { deleteBid, getBids, toggleBid } from "../apis/bid";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  BidModal,
 | 
					  BidModal,
 | 
				
			||||||
  ShowHistoriesBidGraysApiModal,
 | 
					  RecordModal,
 | 
				
			||||||
  ShowHistoriesBidPicklesApiModal,
 | 
					  ResponseDemoModal,
 | 
				
			||||||
  ShowHistoriesModal,
 | 
					  ShowHistoriesModal,
 | 
				
			||||||
} from "../components/bid";
 | 
					} from "../components/bid";
 | 
				
			||||||
import constants, { haveHistories } from "../constant";
 | 
					import DeleteRowAction from "../components/bid/delete-row-action";
 | 
				
			||||||
 | 
					import ShowHistoriesApiModal from "../components/bid/show-histories-api/show-histories-api-modal";
 | 
				
			||||||
 | 
					import { haveHistories } from "../constant";
 | 
				
			||||||
import Table from "../lib/table/table";
 | 
					import Table from "../lib/table/table";
 | 
				
			||||||
import { IColumn, TRefTableFn } from "../lib/table/type";
 | 
					import { IColumn, TRefTableFn } from "../lib/table/type";
 | 
				
			||||||
 | 
					import { useChoosesStore } from "../lib/zustand/use-chooses-store";
 | 
				
			||||||
import { useConfirmStore } from "../lib/zustand/use-confirm";
 | 
					import { useConfirmStore } from "../lib/zustand/use-confirm";
 | 
				
			||||||
import { mappingStatusColors } from "../system/constants";
 | 
					import { mappingStatusColors } from "../system/constants";
 | 
				
			||||||
import { IBid } from "../system/type";
 | 
					import { IBid } from "../system/type";
 | 
				
			||||||
import { extractDomainSmart, formatTime } from "../utils";
 | 
					import {
 | 
				
			||||||
import DeleteRowAction from "../components/bid/delete-row-action";
 | 
					  extractDomainSmart,
 | 
				
			||||||
import { useChoosesStore } from "../lib/zustand/use-chooses-store";
 | 
					  formatTime,
 | 
				
			||||||
 | 
					  getMode,
 | 
				
			||||||
 | 
					  getResponseDemo,
 | 
				
			||||||
 | 
					} from "../utils";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default function Bids() {
 | 
					export default function Bids() {
 | 
				
			||||||
  const refTableFn: TRefTableFn<IBid> = useRef({});
 | 
					  const refTableFn: TRefTableFn<IBid> = useRef({});
 | 
				
			||||||
| 
						 | 
					@ -47,13 +55,12 @@ export default function Bids() {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const { setChooses } = useChoosesStore();
 | 
					  const { setChooses } = useChoosesStore();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const [openedHistories, historiesModel] = useDisclosure(false);
 | 
					  const [openedHistories, historiesModal] = useDisclosure(false);
 | 
				
			||||||
  const [openedHistoriesGraysApi, historiesGraysApiModel] =
 | 
					 | 
				
			||||||
    useDisclosure(false);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const [openedHistoriesPicklesApi, historiesPicklesApiModel] =
 | 
					  const [openedHistoriesView, openedHistoriesViewModal] = useDisclosure(false);
 | 
				
			||||||
    useDisclosure(false);
 | 
					 | 
				
			||||||
  const [openedBid, bidModal] = useDisclosure(false);
 | 
					  const [openedBid, bidModal] = useDisclosure(false);
 | 
				
			||||||
 | 
					  const [openedRecord, recordModal] = useDisclosure(false);
 | 
				
			||||||
 | 
					  const [openedResponseDemo, responseDemoModal] = useDisclosure(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const columns: IColumn<IBid>[] = [
 | 
					  const columns: IColumn<IBid>[] = [
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
| 
						 | 
					@ -84,7 +91,9 @@ export default function Bids() {
 | 
				
			||||||
      title: "Web",
 | 
					      title: "Web",
 | 
				
			||||||
      typeFilter: "none",
 | 
					      typeFilter: "none",
 | 
				
			||||||
      renderRow(row) {
 | 
					      renderRow(row) {
 | 
				
			||||||
        return <span>{extractDomainSmart(row.web_bid.origin_url)}</span>;
 | 
					        return (
 | 
				
			||||||
 | 
					          <span>{extractDomainSmart(row.web_bid?.origin_url) || "None"}</span>
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
| 
						 | 
					@ -145,7 +154,7 @@ export default function Bids() {
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
      key: "close_time",
 | 
					      key: "close_time_ts",
 | 
				
			||||||
      title: "Close time",
 | 
					      title: "Close time",
 | 
				
			||||||
      typeFilter: "date",
 | 
					      typeFilter: "date",
 | 
				
			||||||
      renderRow(row) {
 | 
					      renderRow(row) {
 | 
				
			||||||
| 
						 | 
					@ -190,6 +199,29 @@ export default function Bids() {
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      key: "metadata",
 | 
				
			||||||
 | 
					      title: "Mode",
 | 
				
			||||||
 | 
					      typeFilter: "none",
 | 
				
			||||||
 | 
					      style: {
 | 
				
			||||||
 | 
					        style: {
 | 
				
			||||||
 | 
					          minWidth: "100px",
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      renderRow(row) {
 | 
				
			||||||
 | 
					        return (
 | 
				
			||||||
 | 
					          <Box className="flex items-center justify-center">
 | 
				
			||||||
 | 
					            <Badge
 | 
				
			||||||
 | 
					              color={getMode(row) === "live" ? "teal" : "orange"}
 | 
				
			||||||
 | 
					              size="sm"
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              {row.metadata.find((item) => item.key_name === "mode_key")
 | 
				
			||||||
 | 
					                ?.value || "Live"}
 | 
				
			||||||
 | 
					            </Badge>
 | 
				
			||||||
 | 
					          </Box>
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
  ];
 | 
					  ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const handleDelete = (bid: IBid) => {
 | 
					  const handleDelete = (bid: IBid) => {
 | 
				
			||||||
| 
						 | 
					@ -238,28 +270,6 @@ export default function Bids() {
 | 
				
			||||||
        }}
 | 
					        }}
 | 
				
			||||||
        actionsOptions={{
 | 
					        actionsOptions={{
 | 
				
			||||||
          showMainAction: false,
 | 
					          showMainAction: false,
 | 
				
			||||||
          actions: [
 | 
					 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
              key: "delete",
 | 
					 | 
				
			||||||
              title: "Delete",
 | 
					 | 
				
			||||||
              callback: (data) => {
 | 
					 | 
				
			||||||
                if (!data.length) return;
 | 
					 | 
				
			||||||
                setConfirm({
 | 
					 | 
				
			||||||
                  title: "Delete",
 | 
					 | 
				
			||||||
                  message: `${data.length} will be delete`,
 | 
					 | 
				
			||||||
                  handleOk: async () => {
 | 
					 | 
				
			||||||
                    const result = await deletesBid(data);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    if (!result) return;
 | 
					 | 
				
			||||||
                    if (refTableFn.current.fetchData) {
 | 
					 | 
				
			||||||
                      refTableFn.current.fetchData();
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                  },
 | 
					 | 
				
			||||||
                });
 | 
					 | 
				
			||||||
              },
 | 
					 | 
				
			||||||
              disabled: (data) => data.length <= 0,
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
          ],
 | 
					 | 
				
			||||||
          leftActionSession: (
 | 
					          leftActionSession: (
 | 
				
			||||||
            <Box className="flex items-end gap-2">
 | 
					            <Box className="flex items-end gap-2">
 | 
				
			||||||
              <Button
 | 
					              <Button
 | 
				
			||||||
| 
						 | 
					@ -270,7 +280,11 @@ export default function Bids() {
 | 
				
			||||||
                Add
 | 
					                Add
 | 
				
			||||||
              </Button>
 | 
					              </Button>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
              <DeleteRowAction onDeleted={refTableFn.current?.fetchData} />
 | 
					              <DeleteRowAction
 | 
				
			||||||
 | 
					                onDeleted={() => {
 | 
				
			||||||
 | 
					                  refTableFn.current?.fetchData?.();
 | 
				
			||||||
 | 
					                }}
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
            </Box>
 | 
					            </Box>
 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
        }}
 | 
					        }}
 | 
				
			||||||
| 
						 | 
					@ -329,29 +343,17 @@ export default function Bids() {
 | 
				
			||||||
                    <Menu.Item
 | 
					                    <Menu.Item
 | 
				
			||||||
                      onClick={() => {
 | 
					                      onClick={() => {
 | 
				
			||||||
                        setClickData(row);
 | 
					                        setClickData(row);
 | 
				
			||||||
                        historiesModel.open();
 | 
					                        historiesModal.open();
 | 
				
			||||||
                      }}
 | 
					                      }}
 | 
				
			||||||
                      leftSection={<IconHistory size={14} />}
 | 
					                      leftSection={<IconHistory size={14} />}
 | 
				
			||||||
                    >
 | 
					                    >
 | 
				
			||||||
                      Histories
 | 
					                      Histories
 | 
				
			||||||
                    </Menu.Item>
 | 
					                    </Menu.Item>
 | 
				
			||||||
                    {haveHistories.includes(row?.web_bid.origin_url) && (
 | 
					                    {haveHistories.includes(row?.web_bid?.origin_url) && (
 | 
				
			||||||
                      <Menu.Item
 | 
					                      <Menu.Item
 | 
				
			||||||
                        onClick={() => {
 | 
					                        onClick={() => {
 | 
				
			||||||
                          setClickData(row);
 | 
					                          setClickData(row);
 | 
				
			||||||
                          switch (row.web_bid.origin_url) {
 | 
					                          openedHistoriesViewModal.open();
 | 
				
			||||||
                            case constants.grays: {
 | 
					 | 
				
			||||||
                              historiesGraysApiModel.open();
 | 
					 | 
				
			||||||
                              break;
 | 
					 | 
				
			||||||
                            }
 | 
					 | 
				
			||||||
                            case constants.pickles: {
 | 
					 | 
				
			||||||
                              historiesPicklesApiModel.open();
 | 
					 | 
				
			||||||
                              break;
 | 
					 | 
				
			||||||
                            }
 | 
					 | 
				
			||||||
                            default: {
 | 
					 | 
				
			||||||
                              historiesGraysApiModel.open();
 | 
					 | 
				
			||||||
                            }
 | 
					 | 
				
			||||||
                          }
 | 
					 | 
				
			||||||
                        }}
 | 
					                        }}
 | 
				
			||||||
                        leftSection={<IconHammer size={14} />}
 | 
					                        leftSection={<IconHammer size={14} />}
 | 
				
			||||||
                      >
 | 
					                      >
 | 
				
			||||||
| 
						 | 
					@ -373,12 +375,26 @@ export default function Bids() {
 | 
				
			||||||
                      {row.status === "biding" ? "Disable" : "Enable"}
 | 
					                      {row.status === "biding" ? "Disable" : "Enable"}
 | 
				
			||||||
                    </Menu.Item>
 | 
					                    </Menu.Item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    {/* <Menu.Item
 | 
					                    <Menu.Item
 | 
				
			||||||
                      onClick={() => handleDelete(row)}
 | 
					                      onClick={() => {
 | 
				
			||||||
                      leftSection={<IconTrash color="red" size={14} />}
 | 
					                        setClickData(row);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        if (getResponseDemo(row)) {
 | 
				
			||||||
 | 
					                          responseDemoModal.open();
 | 
				
			||||||
 | 
					                        } else {
 | 
				
			||||||
 | 
					                          recordModal.open();
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                      }}
 | 
				
			||||||
 | 
					                      leftSection={
 | 
				
			||||||
 | 
					                        getResponseDemo(row) ? (
 | 
				
			||||||
 | 
					                          <IconCode size={14} />
 | 
				
			||||||
 | 
					                        ) : (
 | 
				
			||||||
 | 
					                          <IconPlayerRecord size={14} />
 | 
				
			||||||
 | 
					                        )
 | 
				
			||||||
 | 
					                      }
 | 
				
			||||||
                    >
 | 
					                    >
 | 
				
			||||||
                      Delete
 | 
					                      {getResponseDemo(row) ? "Response demo" : "Record"}
 | 
				
			||||||
                    </Menu.Item> */}
 | 
					                    </Menu.Item>
 | 
				
			||||||
                  </Menu.Dropdown>
 | 
					                  </Menu.Dropdown>
 | 
				
			||||||
                </Menu>
 | 
					                </Menu>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -405,11 +421,12 @@ export default function Bids() {
 | 
				
			||||||
      <ShowHistoriesModal
 | 
					      <ShowHistoriesModal
 | 
				
			||||||
        opened={openedHistories}
 | 
					        opened={openedHistories}
 | 
				
			||||||
        onClose={() => {
 | 
					        onClose={() => {
 | 
				
			||||||
          historiesModel.close();
 | 
					          historiesModal.close();
 | 
				
			||||||
          setClickData(null);
 | 
					          setClickData(null);
 | 
				
			||||||
        }}
 | 
					        }}
 | 
				
			||||||
        data={clickData}
 | 
					        data={clickData}
 | 
				
			||||||
      />
 | 
					      />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <BidModal
 | 
					      <BidModal
 | 
				
			||||||
        onUpdated={() => {
 | 
					        onUpdated={() => {
 | 
				
			||||||
          if (refTableFn.current?.fetchData) {
 | 
					          if (refTableFn.current?.fetchData) {
 | 
				
			||||||
| 
						 | 
					@ -426,28 +443,43 @@ export default function Bids() {
 | 
				
			||||||
        }}
 | 
					        }}
 | 
				
			||||||
        data={clickData}
 | 
					        data={clickData}
 | 
				
			||||||
      />
 | 
					      />
 | 
				
			||||||
      {/* Grays */}
 | 
					 | 
				
			||||||
      {openedHistoriesGraysApi && (
 | 
					 | 
				
			||||||
        <ShowHistoriesBidGraysApiModal
 | 
					 | 
				
			||||||
          onUpdated={() => {
 | 
					 | 
				
			||||||
            if (refTableFn.current?.fetchData) {
 | 
					 | 
				
			||||||
              refTableFn.current.fetchData();
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
            setClickData(null);
 | 
					      <RecordModal
 | 
				
			||||||
          }}
 | 
					        onUpdated={() => {
 | 
				
			||||||
          opened={openedHistoriesGraysApi}
 | 
					          if (refTableFn.current?.fetchData) {
 | 
				
			||||||
          onClose={() => {
 | 
					            refTableFn.current.fetchData();
 | 
				
			||||||
            historiesGraysApiModel.close();
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            setClickData(null);
 | 
					          setClickData(null);
 | 
				
			||||||
          }}
 | 
					        }}
 | 
				
			||||||
          data={clickData}
 | 
					        opened={openedRecord}
 | 
				
			||||||
        />
 | 
					        onClose={() => {
 | 
				
			||||||
      )}
 | 
					          recordModal.close();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      {openedHistoriesPicklesApi && (
 | 
					          setClickData(null);
 | 
				
			||||||
        <ShowHistoriesBidPicklesApiModal
 | 
					        }}
 | 
				
			||||||
 | 
					        data={clickData}
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <ResponseDemoModal
 | 
				
			||||||
 | 
					        onUpdated={() => {
 | 
				
			||||||
 | 
					          if (refTableFn.current?.fetchData) {
 | 
				
			||||||
 | 
					            refTableFn.current.fetchData();
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          setClickData(null);
 | 
				
			||||||
 | 
					        }}
 | 
				
			||||||
 | 
					        opened={openedResponseDemo}
 | 
				
			||||||
 | 
					        onClose={() => {
 | 
				
			||||||
 | 
					          responseDemoModal.close();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          setClickData(null);
 | 
				
			||||||
 | 
					        }}
 | 
				
			||||||
 | 
					        data={clickData}
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      {openedHistoriesView && (
 | 
				
			||||||
 | 
					        <ShowHistoriesApiModal
 | 
				
			||||||
          onUpdated={() => {
 | 
					          onUpdated={() => {
 | 
				
			||||||
            if (refTableFn.current?.fetchData) {
 | 
					            if (refTableFn.current?.fetchData) {
 | 
				
			||||||
              refTableFn.current.fetchData();
 | 
					              refTableFn.current.fetchData();
 | 
				
			||||||
| 
						 | 
					@ -457,7 +489,7 @@ export default function Bids() {
 | 
				
			||||||
          }}
 | 
					          }}
 | 
				
			||||||
          opened={true}
 | 
					          opened={true}
 | 
				
			||||||
          onClose={() => {
 | 
					          onClose={() => {
 | 
				
			||||||
            historiesPicklesApiModel.close();
 | 
					            openedHistoriesViewModal.close();
 | 
				
			||||||
            setClickData(null);
 | 
					            setClickData(null);
 | 
				
			||||||
          }}
 | 
					          }}
 | 
				
			||||||
          data={clickData}
 | 
					          data={clickData}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,10 @@
 | 
				
			||||||
 | 
					import { Box } from "@mantine/core";
 | 
				
			||||||
 | 
					import MailsConfig from "../components/config/mails-config";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function Configs() {
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <Box className="flex">
 | 
				
			||||||
 | 
					      <MailsConfig />
 | 
				
			||||||
 | 
					    </Box>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -36,6 +36,7 @@ export default function DashBoard() {
 | 
				
			||||||
  const RETRY_CONNECT = useRef(2);
 | 
					  const RETRY_CONNECT = useRef(2);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  useEffect(() => {
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    setLoading(true);
 | 
				
			||||||
    socket.connect();
 | 
					    socket.connect();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    socket.on("connect", () => {
 | 
					    socket.on("connect", () => {
 | 
				
			||||||
| 
						 | 
					@ -78,6 +79,7 @@ export default function DashBoard() {
 | 
				
			||||||
      setWorkingData(newData);
 | 
					      setWorkingData(newData);
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    setLoading(false);
 | 
				
			||||||
    return () => {
 | 
					    return () => {
 | 
				
			||||||
      console.log("🔌 Cleanup WebSocket listeners...");
 | 
					      console.log("🔌 Cleanup WebSocket listeners...");
 | 
				
			||||||
      socket.off("adminBidsUpdated");
 | 
					      socket.off("adminBidsUpdated");
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,5 +1,6 @@
 | 
				
			||||||
export { default as Dashboard } from './dashboard';
 | 
					export { default as Dashboard } from "./dashboard";
 | 
				
			||||||
export { default as Bids } from './bids';
 | 
					export { default as Bids } from "./bids";
 | 
				
			||||||
export { default as OutBidsLog } from './out-bids-log';
 | 
					export { default as OutBidsLog } from "./out-bids-log";
 | 
				
			||||||
export { default as Login } from './login';
 | 
					export { default as Login } from "./login";
 | 
				
			||||||
export { default as App } from './app';
 | 
					export { default as App } from "./app";
 | 
				
			||||||
 | 
					export { default as Configs } from "./configs";
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,253 +1,306 @@
 | 
				
			||||||
import { ActionIcon, Badge, Box, Menu, Text } from '@mantine/core';
 | 
					import { ActionIcon, Badge, Box, Menu, Text } from "@mantine/core";
 | 
				
			||||||
import { IconAd, IconAdOff, IconEdit, IconMenu, IconTrash, IconUserEdit } from '@tabler/icons-react';
 | 
					import {
 | 
				
			||||||
import { useMemo, useRef, useState } from 'react';
 | 
					  IconAd,
 | 
				
			||||||
import { deletesWebBid, deleteWebBid, getWebBids, updateWebBid } from '../apis/web-bid';
 | 
					  IconAdOff,
 | 
				
			||||||
import Table from '../lib/table/table';
 | 
					  IconEdit,
 | 
				
			||||||
import { IColumn, TRefTableFn } from '../lib/table/type';
 | 
					  IconMenu,
 | 
				
			||||||
import { useConfirmStore } from '../lib/zustand/use-confirm';
 | 
					  IconSettingsCode,
 | 
				
			||||||
import { IWebBid } from '../system/type';
 | 
					  IconTrash,
 | 
				
			||||||
import { formatTime } from '../utils';
 | 
					  IconUserEdit,
 | 
				
			||||||
import { WebAccountModal, WebBidModal } from '../components/web-bid';
 | 
					} from "@tabler/icons-react";
 | 
				
			||||||
import { useDisclosure } from '@mantine/hooks';
 | 
					import { useMemo, useRef, useState } from "react";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  deletesWebBid,
 | 
				
			||||||
 | 
					  deleteWebBid,
 | 
				
			||||||
 | 
					  getWebBids,
 | 
				
			||||||
 | 
					  updateWebBid,
 | 
				
			||||||
 | 
					} from "../apis/web-bid";
 | 
				
			||||||
 | 
					import Table from "../lib/table/table";
 | 
				
			||||||
 | 
					import { IColumn, TRefTableFn } from "../lib/table/type";
 | 
				
			||||||
 | 
					import { useConfirmStore } from "../lib/zustand/use-confirm";
 | 
				
			||||||
 | 
					import { IWebBid } from "../system/type";
 | 
				
			||||||
 | 
					import { formatTime } from "../utils";
 | 
				
			||||||
 | 
					import { WebAccountModal, WebBidModal } from "../components/web-bid";
 | 
				
			||||||
 | 
					import { useDisclosure } from "@mantine/hooks";
 | 
				
			||||||
 | 
					import ScrapConfigModal from "../components/web-bid/scrap-config.modal";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default function WebBids() {
 | 
					export default function WebBids() {
 | 
				
			||||||
    const refTableFn: TRefTableFn<IWebBid> = useRef({});
 | 
					  const refTableFn: TRefTableFn<IWebBid> = useRef({});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const [clickData, setClickData] = useState<IWebBid | null>(null);
 | 
					  const [clickData, setClickData] = useState<IWebBid | null>(null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const { setConfirm } = useConfirmStore();
 | 
					  const { setConfirm } = useConfirmStore();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const [webBidOpened, webBidModal] = useDisclosure(false);
 | 
					  const [webBidOpened, webBidModal] = useDisclosure(false);
 | 
				
			||||||
    const [webAccountOpened, webAccountModal] = useDisclosure(false);
 | 
					  const [webAccountOpened, webAccountModal] = useDisclosure(false);
 | 
				
			||||||
 | 
					  const [scrapConfigOpened, scrapConfigModal] = useDisclosure(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const columns: IColumn<IWebBid>[] = [
 | 
					  const columns: IColumn<IWebBid>[] = [
 | 
				
			||||||
        {
 | 
					    {
 | 
				
			||||||
            key: 'id',
 | 
					      key: "id",
 | 
				
			||||||
            title: 'ID',
 | 
					      title: "ID",
 | 
				
			||||||
            typeFilter: 'number',
 | 
					      typeFilter: "number",
 | 
				
			||||||
        },
 | 
					    },
 | 
				
			||||||
        {
 | 
					    {
 | 
				
			||||||
            key: 'origin_url',
 | 
					      key: "origin_url",
 | 
				
			||||||
            title: 'Domain',
 | 
					      title: "Domain",
 | 
				
			||||||
            typeFilter: 'text',
 | 
					      typeFilter: "text",
 | 
				
			||||||
        },
 | 
					    },
 | 
				
			||||||
        {
 | 
					    {
 | 
				
			||||||
            key: 'url',
 | 
					      key: "url",
 | 
				
			||||||
            title: 'Tracking url',
 | 
					      title: "Tracking url",
 | 
				
			||||||
            typeFilter: 'text',
 | 
					      typeFilter: "text",
 | 
				
			||||||
            renderRow(row) {
 | 
					      renderRow(row) {
 | 
				
			||||||
                return <Text>{row.url || 'None'}</Text>;
 | 
					        return <Text>{row.url || "None"}</Text>;
 | 
				
			||||||
            },
 | 
					      },
 | 
				
			||||||
        },
 | 
					    },
 | 
				
			||||||
        {
 | 
					    {
 | 
				
			||||||
            key: 'active',
 | 
					      key: "active",
 | 
				
			||||||
            title: 'Status',
 | 
					      title: "Status",
 | 
				
			||||||
            typeFilter: 'text',
 | 
					      typeFilter: "text",
 | 
				
			||||||
            renderRow(row) {
 | 
					      renderRow(row) {
 | 
				
			||||||
                return (
 | 
					 | 
				
			||||||
                    <Box className="flex items-center justify-center">
 | 
					 | 
				
			||||||
                        <Badge color={row.active ? 'green' : 'red'} size="sm">
 | 
					 | 
				
			||||||
                            {row.active ? 'Enable' : 'Disable'}
 | 
					 | 
				
			||||||
                        </Badge>
 | 
					 | 
				
			||||||
                    </Box>
 | 
					 | 
				
			||||||
                );
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            key: 'created_at',
 | 
					 | 
				
			||||||
            title: 'Created at',
 | 
					 | 
				
			||||||
            typeFilter: 'none',
 | 
					 | 
				
			||||||
            renderRow(row) {
 | 
					 | 
				
			||||||
                return <span>{formatTime(row.created_at)}</span>;
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            key: 'updated_at',
 | 
					 | 
				
			||||||
            title: 'Update at',
 | 
					 | 
				
			||||||
            typeFilter: 'none',
 | 
					 | 
				
			||||||
            renderRow(row) {
 | 
					 | 
				
			||||||
                return <span>{formatTime(row.updated_at)}</span>;
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
    ];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const handleDelete = (data: IWebBid) => {
 | 
					 | 
				
			||||||
        setConfirm({
 | 
					 | 
				
			||||||
            title: 'Delete ?',
 | 
					 | 
				
			||||||
            message: 'This web will be delete',
 | 
					 | 
				
			||||||
            handleOk: async () => {
 | 
					 | 
				
			||||||
                await deleteWebBid(data);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                if (refTableFn.current?.fetchData) {
 | 
					 | 
				
			||||||
                    refTableFn.current.fetchData();
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const handleToggle = async (data: IWebBid) => {
 | 
					 | 
				
			||||||
        setConfirm({
 | 
					 | 
				
			||||||
            title: (data.active ? 'Disable ' : 'Enable ') + 'ID: ' + data.id,
 | 
					 | 
				
			||||||
            message: 'This web will be ' + (data.active ? 'disable ' : 'enable '),
 | 
					 | 
				
			||||||
            handleOk: async () => {
 | 
					 | 
				
			||||||
                await updateWebBid({ ...data, active: !data.active || false });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                if (refTableFn.current?.fetchData) {
 | 
					 | 
				
			||||||
                    refTableFn.current.fetchData();
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
            okButton: {
 | 
					 | 
				
			||||||
                value: data.active ? 'Disable ' : 'Enable ',
 | 
					 | 
				
			||||||
                color: data.active ? 'red' : 'blue',
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const table = useMemo(() => {
 | 
					 | 
				
			||||||
        return (
 | 
					        return (
 | 
				
			||||||
            <Table
 | 
					          <Box className="flex items-center justify-center">
 | 
				
			||||||
                actionsOptions={{
 | 
					            <Badge color={row.active ? "green" : "red"} size="sm">
 | 
				
			||||||
                    actions: [
 | 
					              {row.active ? "Enable" : "Disable"}
 | 
				
			||||||
                        {
 | 
					            </Badge>
 | 
				
			||||||
                            key: 'add',
 | 
					          </Box>
 | 
				
			||||||
                            title: 'Add',
 | 
					 | 
				
			||||||
                            callback: () => {
 | 
					 | 
				
			||||||
                                webBidModal.open();
 | 
					 | 
				
			||||||
                            },
 | 
					 | 
				
			||||||
                        },
 | 
					 | 
				
			||||||
                        {
 | 
					 | 
				
			||||||
                            key: 'delete',
 | 
					 | 
				
			||||||
                            title: 'Delete',
 | 
					 | 
				
			||||||
                            callback: (data) => {
 | 
					 | 
				
			||||||
                                if (!data.length) return;
 | 
					 | 
				
			||||||
                                setConfirm({
 | 
					 | 
				
			||||||
                                    title: 'Delete',
 | 
					 | 
				
			||||||
                                    message: `${data.length} will be delete`,
 | 
					 | 
				
			||||||
                                    handleOk: async () => {
 | 
					 | 
				
			||||||
                                        const result = await deletesWebBid(data);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                                        if (!result) return;
 | 
					 | 
				
			||||||
                                        if (refTableFn.current.fetchData) {
 | 
					 | 
				
			||||||
                                            refTableFn.current.fetchData();
 | 
					 | 
				
			||||||
                                        }
 | 
					 | 
				
			||||||
                                    },
 | 
					 | 
				
			||||||
                                });
 | 
					 | 
				
			||||||
                            },
 | 
					 | 
				
			||||||
                            disabled: (data) => data.length <= 0,
 | 
					 | 
				
			||||||
                        },
 | 
					 | 
				
			||||||
                    ],
 | 
					 | 
				
			||||||
                }}
 | 
					 | 
				
			||||||
                refTableFn={refTableFn}
 | 
					 | 
				
			||||||
                striped
 | 
					 | 
				
			||||||
                showLoading={true}
 | 
					 | 
				
			||||||
                highlightOnHover
 | 
					 | 
				
			||||||
                styleDefaultHead={{
 | 
					 | 
				
			||||||
                    justifyContent: 'flex-start',
 | 
					 | 
				
			||||||
                    width: 'fit-content',
 | 
					 | 
				
			||||||
                }}
 | 
					 | 
				
			||||||
                options={{
 | 
					 | 
				
			||||||
                    query: getWebBids,
 | 
					 | 
				
			||||||
                    pathToData: 'data.data',
 | 
					 | 
				
			||||||
                    keyOptions: {
 | 
					 | 
				
			||||||
                        last_page: 'lastPage',
 | 
					 | 
				
			||||||
                        per_page: 'perPage',
 | 
					 | 
				
			||||||
                        from: 'from',
 | 
					 | 
				
			||||||
                        to: 'to',
 | 
					 | 
				
			||||||
                        total: 'total',
 | 
					 | 
				
			||||||
                    },
 | 
					 | 
				
			||||||
                }}
 | 
					 | 
				
			||||||
                rows={[]}
 | 
					 | 
				
			||||||
                withColumnBorders
 | 
					 | 
				
			||||||
                showChooses={true}
 | 
					 | 
				
			||||||
                withTableBorder
 | 
					 | 
				
			||||||
                columns={columns}
 | 
					 | 
				
			||||||
                actions={{
 | 
					 | 
				
			||||||
                    title: <Box className="w-full text-center">Action</Box>,
 | 
					 | 
				
			||||||
                    body: (row) => {
 | 
					 | 
				
			||||||
                        return (
 | 
					 | 
				
			||||||
                            <Menu shadow="md" width={200}>
 | 
					 | 
				
			||||||
                                <Menu.Target>
 | 
					 | 
				
			||||||
                                    <Box className="flex w-full items-center justify-center">
 | 
					 | 
				
			||||||
                                        <ActionIcon size="sm" variant="light">
 | 
					 | 
				
			||||||
                                            <IconMenu size={14} />
 | 
					 | 
				
			||||||
                                        </ActionIcon>
 | 
					 | 
				
			||||||
                                    </Box>
 | 
					 | 
				
			||||||
                                </Menu.Target>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                                <Menu.Dropdown>
 | 
					 | 
				
			||||||
                                    <Menu.Item
 | 
					 | 
				
			||||||
                                        onClick={() => {
 | 
					 | 
				
			||||||
                                            setClickData(row);
 | 
					 | 
				
			||||||
                                            webBidModal.open();
 | 
					 | 
				
			||||||
                                        }}
 | 
					 | 
				
			||||||
                                        leftSection={<IconEdit size={14} />}
 | 
					 | 
				
			||||||
                                    >
 | 
					 | 
				
			||||||
                                        Edit
 | 
					 | 
				
			||||||
                                    </Menu.Item>
 | 
					 | 
				
			||||||
                                    <Menu.Item
 | 
					 | 
				
			||||||
                                        onClick={() => {
 | 
					 | 
				
			||||||
                                            setClickData(row);
 | 
					 | 
				
			||||||
                                            webAccountModal.open();
 | 
					 | 
				
			||||||
                                        }}
 | 
					 | 
				
			||||||
                                        leftSection={<IconUserEdit size={14} />}
 | 
					 | 
				
			||||||
                                    >
 | 
					 | 
				
			||||||
                                        Account
 | 
					 | 
				
			||||||
                                    </Menu.Item>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                                    <Menu.Item onClick={() => handleToggle(row)} leftSection={row.active ? <IconAdOff size={14} /> : <IconAd size={14} />}>
 | 
					 | 
				
			||||||
                                        {row.active ? 'Disable' : 'Enable'}
 | 
					 | 
				
			||||||
                                    </Menu.Item>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                                    <Menu.Item onClick={() => handleDelete(row)} leftSection={<IconTrash color="red" size={14} />}>
 | 
					 | 
				
			||||||
                                        Delete
 | 
					 | 
				
			||||||
                                    </Menu.Item>
 | 
					 | 
				
			||||||
                                </Menu.Dropdown>
 | 
					 | 
				
			||||||
                            </Menu>
 | 
					 | 
				
			||||||
                        );
 | 
					 | 
				
			||||||
                    },
 | 
					 | 
				
			||||||
                }}
 | 
					 | 
				
			||||||
                rowKey="id"
 | 
					 | 
				
			||||||
            />
 | 
					 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
        // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
					      },
 | 
				
			||||||
    }, []);
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      key: "created_at",
 | 
				
			||||||
 | 
					      title: "Created at",
 | 
				
			||||||
 | 
					      typeFilter: "none",
 | 
				
			||||||
 | 
					      renderRow(row) {
 | 
				
			||||||
 | 
					        return <span>{formatTime(row.created_at)}</span>;
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      key: "updated_at",
 | 
				
			||||||
 | 
					      title: "Update at",
 | 
				
			||||||
 | 
					      typeFilter: "none",
 | 
				
			||||||
 | 
					      renderRow(row) {
 | 
				
			||||||
 | 
					        return <span>{formatTime(row.updated_at)}</span>;
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleDelete = (data: IWebBid) => {
 | 
				
			||||||
 | 
					    setConfirm({
 | 
				
			||||||
 | 
					      title: "Delete ?",
 | 
				
			||||||
 | 
					      message: "This web will be delete",
 | 
				
			||||||
 | 
					      handleOk: async () => {
 | 
				
			||||||
 | 
					        await deleteWebBid(data);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (refTableFn.current?.fetchData) {
 | 
				
			||||||
 | 
					          refTableFn.current.fetchData();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleToggle = async (data: IWebBid) => {
 | 
				
			||||||
 | 
					    setConfirm({
 | 
				
			||||||
 | 
					      title: (data.active ? "Disable " : "Enable ") + "ID: " + data.id,
 | 
				
			||||||
 | 
					      message: "This web will be " + (data.active ? "disable " : "enable "),
 | 
				
			||||||
 | 
					      handleOk: async () => {
 | 
				
			||||||
 | 
					        await updateWebBid({ ...data, active: !data.active || false });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (refTableFn.current?.fetchData) {
 | 
				
			||||||
 | 
					          refTableFn.current.fetchData();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      okButton: {
 | 
				
			||||||
 | 
					        value: data.active ? "Disable " : "Enable ",
 | 
				
			||||||
 | 
					        color: data.active ? "red" : "blue",
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const table = useMemo(() => {
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
        <Box>
 | 
					      <Table
 | 
				
			||||||
            {table}
 | 
					        actionsOptions={{
 | 
				
			||||||
 | 
					          actions: [
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					              key: "add",
 | 
				
			||||||
 | 
					              title: "Add",
 | 
				
			||||||
 | 
					              callback: () => {
 | 
				
			||||||
 | 
					                webBidModal.open();
 | 
				
			||||||
 | 
					              },
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					              key: "delete",
 | 
				
			||||||
 | 
					              title: "Delete",
 | 
				
			||||||
 | 
					              callback: (data) => {
 | 
				
			||||||
 | 
					                if (!data.length) return;
 | 
				
			||||||
 | 
					                setConfirm({
 | 
				
			||||||
 | 
					                  title: "Delete",
 | 
				
			||||||
 | 
					                  message: `${data.length} will be delete`,
 | 
				
			||||||
 | 
					                  handleOk: async () => {
 | 
				
			||||||
 | 
					                    const result = await deletesWebBid(data);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            <WebBidModal
 | 
					                    if (!result) return;
 | 
				
			||||||
                data={clickData}
 | 
					                    if (refTableFn.current.fetchData) {
 | 
				
			||||||
                opened={webBidOpened}
 | 
					                      refTableFn.current.fetchData();
 | 
				
			||||||
                onClose={() => {
 | 
					 | 
				
			||||||
                    webBidModal.close();
 | 
					 | 
				
			||||||
                    setClickData(null);
 | 
					 | 
				
			||||||
                }}
 | 
					 | 
				
			||||||
                onUpdated={() => {
 | 
					 | 
				
			||||||
                    setClickData(null);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    if (refTableFn.current?.fetchData) {
 | 
					 | 
				
			||||||
                        refTableFn.current.fetchData();
 | 
					 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                }}
 | 
					                  },
 | 
				
			||||||
            />
 | 
					                });
 | 
				
			||||||
 | 
					              },
 | 
				
			||||||
 | 
					              disabled: (data) => data.length <= 0,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					          ],
 | 
				
			||||||
 | 
					        }}
 | 
				
			||||||
 | 
					        refTableFn={refTableFn}
 | 
				
			||||||
 | 
					        striped
 | 
				
			||||||
 | 
					        showLoading={true}
 | 
				
			||||||
 | 
					        highlightOnHover
 | 
				
			||||||
 | 
					        styleDefaultHead={{
 | 
				
			||||||
 | 
					          justifyContent: "flex-start",
 | 
				
			||||||
 | 
					          width: "fit-content",
 | 
				
			||||||
 | 
					        }}
 | 
				
			||||||
 | 
					        options={{
 | 
				
			||||||
 | 
					          query: getWebBids,
 | 
				
			||||||
 | 
					          pathToData: "data.data",
 | 
				
			||||||
 | 
					          keyOptions: {
 | 
				
			||||||
 | 
					            last_page: "lastPage",
 | 
				
			||||||
 | 
					            per_page: "perPage",
 | 
				
			||||||
 | 
					            from: "from",
 | 
				
			||||||
 | 
					            to: "to",
 | 
				
			||||||
 | 
					            total: "total",
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        }}
 | 
				
			||||||
 | 
					        rows={[]}
 | 
				
			||||||
 | 
					        withColumnBorders
 | 
				
			||||||
 | 
					        showChooses={true}
 | 
				
			||||||
 | 
					        withTableBorder
 | 
				
			||||||
 | 
					        columns={columns}
 | 
				
			||||||
 | 
					        actions={{
 | 
				
			||||||
 | 
					          title: <Box className="w-full text-center">Action</Box>,
 | 
				
			||||||
 | 
					          body: (row) => {
 | 
				
			||||||
 | 
					            return (
 | 
				
			||||||
 | 
					              <Menu shadow="md" width={200}>
 | 
				
			||||||
 | 
					                <Menu.Target>
 | 
				
			||||||
 | 
					                  <Box className="flex w-full items-center justify-center">
 | 
				
			||||||
 | 
					                    <ActionIcon size="sm" variant="light">
 | 
				
			||||||
 | 
					                      <IconMenu size={14} />
 | 
				
			||||||
 | 
					                    </ActionIcon>
 | 
				
			||||||
 | 
					                  </Box>
 | 
				
			||||||
 | 
					                </Menu.Target>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            <WebAccountModal
 | 
					                <Menu.Dropdown>
 | 
				
			||||||
                data={clickData}
 | 
					                  <Menu.Item
 | 
				
			||||||
                opened={webAccountOpened}
 | 
					                    onClick={() => {
 | 
				
			||||||
                onClose={() => {
 | 
					                      setClickData(row);
 | 
				
			||||||
                    webAccountModal.close();
 | 
					                      webBidModal.open();
 | 
				
			||||||
                    setClickData(null);
 | 
					                    }}
 | 
				
			||||||
                }}
 | 
					                    leftSection={<IconEdit size={14} />}
 | 
				
			||||||
                onUpdated={() => {
 | 
					                  >
 | 
				
			||||||
                    setClickData(null);
 | 
					                    Edit
 | 
				
			||||||
 | 
					                  </Menu.Item>
 | 
				
			||||||
 | 
					                  <Menu.Item
 | 
				
			||||||
 | 
					                    onClick={() => {
 | 
				
			||||||
 | 
					                      setClickData(row);
 | 
				
			||||||
 | 
					                      webAccountModal.open();
 | 
				
			||||||
 | 
					                    }}
 | 
				
			||||||
 | 
					                    leftSection={<IconUserEdit size={14} />}
 | 
				
			||||||
 | 
					                  >
 | 
				
			||||||
 | 
					                    Account
 | 
				
			||||||
 | 
					                  </Menu.Item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    if (refTableFn.current?.fetchData) {
 | 
					                  <Menu.Item
 | 
				
			||||||
                        refTableFn.current.fetchData();
 | 
					                    onClick={() => {
 | 
				
			||||||
 | 
					                      setClickData(row);
 | 
				
			||||||
 | 
					                      scrapConfigModal.open();
 | 
				
			||||||
 | 
					                    }}
 | 
				
			||||||
 | 
					                    leftSection={<IconSettingsCode size={14} />}
 | 
				
			||||||
 | 
					                  >
 | 
				
			||||||
 | 
					                    Scrap config
 | 
				
			||||||
 | 
					                  </Menu.Item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                  <Menu.Item
 | 
				
			||||||
 | 
					                    onClick={() => handleToggle(row)}
 | 
				
			||||||
 | 
					                    leftSection={
 | 
				
			||||||
 | 
					                      row.active ? (
 | 
				
			||||||
 | 
					                        <IconAdOff size={14} />
 | 
				
			||||||
 | 
					                      ) : (
 | 
				
			||||||
 | 
					                        <IconAd size={14} />
 | 
				
			||||||
 | 
					                      )
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                }}
 | 
					                  >
 | 
				
			||||||
            />
 | 
					                    {row.active ? "Disable" : "Enable"}
 | 
				
			||||||
        </Box>
 | 
					                  </Menu.Item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                  <Menu.Item
 | 
				
			||||||
 | 
					                    onClick={() => handleDelete(row)}
 | 
				
			||||||
 | 
					                    leftSection={<IconTrash color="red" size={14} />}
 | 
				
			||||||
 | 
					                  >
 | 
				
			||||||
 | 
					                    Delete
 | 
				
			||||||
 | 
					                  </Menu.Item>
 | 
				
			||||||
 | 
					                </Menu.Dropdown>
 | 
				
			||||||
 | 
					              </Menu>
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        }}
 | 
				
			||||||
 | 
					        rowKey="id"
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					    // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
				
			||||||
 | 
					  }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <Box>
 | 
				
			||||||
 | 
					      {table}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <WebBidModal
 | 
				
			||||||
 | 
					        data={clickData}
 | 
				
			||||||
 | 
					        opened={webBidOpened}
 | 
				
			||||||
 | 
					        onClose={() => {
 | 
				
			||||||
 | 
					          webBidModal.close();
 | 
				
			||||||
 | 
					          setClickData(null);
 | 
				
			||||||
 | 
					        }}
 | 
				
			||||||
 | 
					        onUpdated={() => {
 | 
				
			||||||
 | 
					          setClickData(null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          if (refTableFn.current?.fetchData) {
 | 
				
			||||||
 | 
					            refTableFn.current.fetchData();
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }}
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <WebAccountModal
 | 
				
			||||||
 | 
					        data={clickData}
 | 
				
			||||||
 | 
					        opened={webAccountOpened}
 | 
				
			||||||
 | 
					        onClose={() => {
 | 
				
			||||||
 | 
					          webAccountModal.close();
 | 
				
			||||||
 | 
					          setClickData(null);
 | 
				
			||||||
 | 
					        }}
 | 
				
			||||||
 | 
					        onUpdated={() => {
 | 
				
			||||||
 | 
					          setClickData(null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          if (refTableFn.current?.fetchData) {
 | 
				
			||||||
 | 
					            refTableFn.current.fetchData();
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }}
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <ScrapConfigModal
 | 
				
			||||||
 | 
					        data={clickData}
 | 
				
			||||||
 | 
					        opened={scrapConfigOpened}
 | 
				
			||||||
 | 
					        onClose={() => {
 | 
				
			||||||
 | 
					          scrapConfigModal.close();
 | 
				
			||||||
 | 
					          setClickData(null);
 | 
				
			||||||
 | 
					        }}
 | 
				
			||||||
 | 
					        onUpdated={() => {
 | 
				
			||||||
 | 
					          setClickData(null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          if (refTableFn.current?.fetchData) {
 | 
				
			||||||
 | 
					            refTableFn.current.fetchData();
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }}
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					    </Box>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,70 +1,87 @@
 | 
				
			||||||
import { IconHammer, IconHome2, IconKey, IconMessage, IconOutlet, IconPageBreak, IconUserCheck } from '@tabler/icons-react';
 | 
					import {
 | 
				
			||||||
import { Bids, Dashboard, OutBidsLog } from '../pages';
 | 
					  IconHammer,
 | 
				
			||||||
import WebBids from '../pages/web-bids';
 | 
					  IconHome2,
 | 
				
			||||||
import SendMessageHistories from '../pages/send-message-histories';
 | 
					  IconKey,
 | 
				
			||||||
import Admins from '../pages/admins';
 | 
					  IconMessage,
 | 
				
			||||||
import GenerateKeys from '../pages/generate-keys';
 | 
					  IconOutlet,
 | 
				
			||||||
 | 
					  IconPageBreak,
 | 
				
			||||||
 | 
					  IconSettings,
 | 
				
			||||||
 | 
					  IconUserCheck,
 | 
				
			||||||
 | 
					} from "@tabler/icons-react";
 | 
				
			||||||
 | 
					import { Bids, Configs, Dashboard, OutBidsLog } from "../pages";
 | 
				
			||||||
 | 
					import WebBids from "../pages/web-bids";
 | 
				
			||||||
 | 
					import SendMessageHistories from "../pages/send-message-histories";
 | 
				
			||||||
 | 
					import Admins from "../pages/admins";
 | 
				
			||||||
 | 
					import GenerateKeys from "../pages/generate-keys";
 | 
				
			||||||
export default class Links {
 | 
					export default class Links {
 | 
				
			||||||
    public static DASHBOARD = '/dashboard';
 | 
					  public static DASHBOARD = "/dashboard";
 | 
				
			||||||
    public static BIDS = '/bids';
 | 
					  public static BIDS = "/bids";
 | 
				
			||||||
    public static WEBS = '/webs';
 | 
					  public static WEBS = "/webs";
 | 
				
			||||||
    public static OUT_BIDS_LOG = '/out-bids-log';
 | 
					  public static OUT_BIDS_LOG = "/out-bids-log";
 | 
				
			||||||
    public static SEND_MESSAGE_HISTORIES = '/send-message-histories';
 | 
					  public static SEND_MESSAGE_HISTORIES = "/send-message-histories";
 | 
				
			||||||
    public static GENERATE_KEYS = '/generate-keys';
 | 
					  public static GENERATE_KEYS = "/generate-keys";
 | 
				
			||||||
    public static ADMINS = '/admins';
 | 
					  public static ADMINS = "/admins";
 | 
				
			||||||
 | 
					  public static CONFIGS = "/configs";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public static HOME = '/';
 | 
					  public static HOME = "/";
 | 
				
			||||||
    public static LOGIN = '/login';
 | 
					  public static LOGIN = "/login";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public static MENUS = [
 | 
					  public static MENUS = [
 | 
				
			||||||
        {
 | 
					    {
 | 
				
			||||||
            path: this.DASHBOARD,
 | 
					      path: this.DASHBOARD,
 | 
				
			||||||
            title: 'Dashboard',
 | 
					      title: "Dashboard",
 | 
				
			||||||
            icon: IconHome2,
 | 
					      icon: IconHome2,
 | 
				
			||||||
            element: Dashboard,
 | 
					      element: Dashboard,
 | 
				
			||||||
            show: true,
 | 
					      show: true,
 | 
				
			||||||
        },
 | 
					    },
 | 
				
			||||||
        {
 | 
					    {
 | 
				
			||||||
            path: this.ADMINS,
 | 
					      path: this.ADMINS,
 | 
				
			||||||
            title: 'Admins',
 | 
					      title: "Admins",
 | 
				
			||||||
            icon: IconUserCheck,
 | 
					      icon: IconUserCheck,
 | 
				
			||||||
            element: Admins,
 | 
					      element: Admins,
 | 
				
			||||||
            show: true,
 | 
					      show: true,
 | 
				
			||||||
        },
 | 
					    },
 | 
				
			||||||
        {
 | 
					    {
 | 
				
			||||||
            path: this.WEBS,
 | 
					      path: this.WEBS,
 | 
				
			||||||
            title: 'Webs',
 | 
					      title: "Webs",
 | 
				
			||||||
            icon: IconPageBreak,
 | 
					      icon: IconPageBreak,
 | 
				
			||||||
            element: WebBids,
 | 
					      element: WebBids,
 | 
				
			||||||
            show: true,
 | 
					      show: true,
 | 
				
			||||||
        },
 | 
					    },
 | 
				
			||||||
        {
 | 
					    {
 | 
				
			||||||
            path: this.BIDS,
 | 
					      path: this.BIDS,
 | 
				
			||||||
            title: 'Bids',
 | 
					      title: "Bids",
 | 
				
			||||||
            icon: IconHammer,
 | 
					      icon: IconHammer,
 | 
				
			||||||
            element: Bids,
 | 
					      element: Bids,
 | 
				
			||||||
            show: true,
 | 
					      show: true,
 | 
				
			||||||
        },
 | 
					    },
 | 
				
			||||||
        {
 | 
					    {
 | 
				
			||||||
            path: this.OUT_BIDS_LOG,
 | 
					      path: this.OUT_BIDS_LOG,
 | 
				
			||||||
            title: 'Out bids log',
 | 
					      title: "Out bids log",
 | 
				
			||||||
            icon: IconOutlet,
 | 
					      icon: IconOutlet,
 | 
				
			||||||
            element: OutBidsLog,
 | 
					      element: OutBidsLog,
 | 
				
			||||||
            show: true,
 | 
					      show: true,
 | 
				
			||||||
        },
 | 
					    },
 | 
				
			||||||
        {
 | 
					    {
 | 
				
			||||||
            path: this.SEND_MESSAGE_HISTORIES,
 | 
					      path: this.SEND_MESSAGE_HISTORIES,
 | 
				
			||||||
            title: 'Send message histories',
 | 
					      title: "Send message histories",
 | 
				
			||||||
            icon: IconMessage,
 | 
					      icon: IconMessage,
 | 
				
			||||||
            element: SendMessageHistories,
 | 
					      element: SendMessageHistories,
 | 
				
			||||||
            show: true,
 | 
					      show: true,
 | 
				
			||||||
        },
 | 
					    },
 | 
				
			||||||
        {
 | 
					    {
 | 
				
			||||||
            path: this.GENERATE_KEYS,
 | 
					      path: this.GENERATE_KEYS,
 | 
				
			||||||
            title: 'Generate keys',
 | 
					      title: "Generate keys",
 | 
				
			||||||
            icon: IconKey,
 | 
					      icon: IconKey,
 | 
				
			||||||
            element: GenerateKeys,
 | 
					      element: GenerateKeys,
 | 
				
			||||||
            show: false,
 | 
					      show: false,
 | 
				
			||||||
        },
 | 
					    },
 | 
				
			||||||
    ];
 | 
					    {
 | 
				
			||||||
 | 
					      path: this.CONFIGS,
 | 
				
			||||||
 | 
					      title: "Configs",
 | 
				
			||||||
 | 
					      icon: IconSettings,
 | 
				
			||||||
 | 
					      element: Configs,
 | 
				
			||||||
 | 
					      show: false,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  ];
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,79 +1,110 @@
 | 
				
			||||||
 | 
					/* eslint-disable @typescript-eslint/no-explicit-any */
 | 
				
			||||||
export interface IAdmin extends ITimestamp {
 | 
					export interface IAdmin extends ITimestamp {
 | 
				
			||||||
    id: number;
 | 
					  id: number;
 | 
				
			||||||
    email: string;
 | 
					  email: string;
 | 
				
			||||||
    username: string;
 | 
					  username: string;
 | 
				
			||||||
    fullname: string;
 | 
					  fullname: string;
 | 
				
			||||||
    password?: string;
 | 
					  password?: string;
 | 
				
			||||||
    is_system_account: boolean;
 | 
					  is_system_account: boolean;
 | 
				
			||||||
    permissions: [];
 | 
					  permissions: [];
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface IKey extends ITimestamp {
 | 
					export interface IKey extends ITimestamp {
 | 
				
			||||||
    id: number;
 | 
					  id: number;
 | 
				
			||||||
    client_key: string;
 | 
					  client_key: string;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface ITimestamp {
 | 
					export interface ITimestamp {
 | 
				
			||||||
    created_at: string;
 | 
					  created_at: string;
 | 
				
			||||||
    updated_at: string;
 | 
					  updated_at: string;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export interface IHistory extends ITimestamp {
 | 
					export interface IHistory extends ITimestamp {
 | 
				
			||||||
    id: number;
 | 
					  id: number;
 | 
				
			||||||
    price: number;
 | 
					  price: number;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface IOutBidLog extends ITimestamp {
 | 
					export interface IOutBidLog extends ITimestamp {
 | 
				
			||||||
    id: number;
 | 
					  id: number;
 | 
				
			||||||
    model: string;
 | 
					  model: string;
 | 
				
			||||||
    lot_id: string;
 | 
					  lot_id: string;
 | 
				
			||||||
    out_price: number;
 | 
					  out_price: number;
 | 
				
			||||||
    raw_data: string;
 | 
					  raw_data: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface IScrapConfig extends ITimestamp {
 | 
				
			||||||
 | 
					  id: number;
 | 
				
			||||||
 | 
					  search_url: string;
 | 
				
			||||||
 | 
					  keywords: string;
 | 
				
			||||||
 | 
					  enable: boolean | "0" | "1";
 | 
				
			||||||
 | 
					  scrap_items: IScrapItem[];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface IScrapItem extends ITimestamp {
 | 
				
			||||||
 | 
					  id: number;
 | 
				
			||||||
 | 
					  url: string;
 | 
				
			||||||
 | 
					  model: string;
 | 
				
			||||||
 | 
					  image_url: string | null;
 | 
				
			||||||
 | 
					  keyword: string;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface IWebBid extends ITimestamp {
 | 
					export interface IWebBid extends ITimestamp {
 | 
				
			||||||
    created_at: string;
 | 
					  created_at: string;
 | 
				
			||||||
    updated_at: string;
 | 
					  updated_at: string;
 | 
				
			||||||
    id: number;
 | 
					  id: number;
 | 
				
			||||||
    origin_url: string;
 | 
					  origin_url: string;
 | 
				
			||||||
    url: string | null;
 | 
					  url: string | null;
 | 
				
			||||||
    username: string | null;
 | 
					  username: string | null;
 | 
				
			||||||
    password: string | null;
 | 
					  password: string | null;
 | 
				
			||||||
    active: boolean;
 | 
					  active: boolean;
 | 
				
			||||||
    arrival_offset_seconds: number;
 | 
					  arrival_offset_seconds: number;
 | 
				
			||||||
    early_tracking_seconds: number;
 | 
					  early_tracking_seconds: number;
 | 
				
			||||||
    snapshot_at: string | null
 | 
					  snapshot_at: string | null;
 | 
				
			||||||
    children: IBid[];
 | 
					  children: IBid[];
 | 
				
			||||||
 | 
					  scrap_config: IScrapConfig;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface IMetadata extends ITimestamp {
 | 
				
			||||||
 | 
					  id: number;
 | 
				
			||||||
 | 
					  key_name: string;
 | 
				
			||||||
 | 
					  value: any;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface IBid extends ITimestamp {
 | 
					export interface IBid extends ITimestamp {
 | 
				
			||||||
    id: number;
 | 
					  id: number;
 | 
				
			||||||
    max_price: number;
 | 
					  max_price: number;
 | 
				
			||||||
    reserve_price: number;
 | 
					  reserve_price: number;
 | 
				
			||||||
    current_price: number;
 | 
					  current_price: number;
 | 
				
			||||||
    name: string | null;
 | 
					  name: string | null;
 | 
				
			||||||
    quantity: number;
 | 
					  quantity: number;
 | 
				
			||||||
    url: string;
 | 
					  url: string;
 | 
				
			||||||
    model: string;
 | 
					  model: string;
 | 
				
			||||||
    lot_id: string;
 | 
					  lot_id: string;
 | 
				
			||||||
    plus_price: number;
 | 
					  plus_price: number;
 | 
				
			||||||
    close_time: string | null;
 | 
					  close_time: string | null;
 | 
				
			||||||
    start_bid_time: string | null;
 | 
					  close_time_ts: string | null;
 | 
				
			||||||
    first_bid: boolean;
 | 
					  start_bid_time: string | null;
 | 
				
			||||||
    status: 'biding' | 'out-bid' | 'win-bid';
 | 
					  first_bid: boolean;
 | 
				
			||||||
    histories: IHistory[];
 | 
					  status: "biding" | "out-bid" | "win-bid";
 | 
				
			||||||
    web_bid: IWebBid;
 | 
					  histories: IHistory[];
 | 
				
			||||||
 | 
					  web_bid: IWebBid;
 | 
				
			||||||
 | 
					  metadata: IMetadata[];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface IConfig extends ITimestamp {
 | 
				
			||||||
 | 
					  id: number;
 | 
				
			||||||
 | 
					  key_name: string;
 | 
				
			||||||
 | 
					  value: string;
 | 
				
			||||||
 | 
					  type: "string" | "number";
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface IPermission extends ITimestamp {
 | 
					export interface IPermission extends ITimestamp {
 | 
				
			||||||
    id: number;
 | 
					  id: number;
 | 
				
			||||||
    name: string;
 | 
					  name: string;
 | 
				
			||||||
    description: string;
 | 
					  description: string;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
export interface ISendMessageHistory extends ITimestamp {
 | 
					export interface ISendMessageHistory extends ITimestamp {
 | 
				
			||||||
    id: number;
 | 
					  id: number;
 | 
				
			||||||
    message: string;
 | 
					  message: string;
 | 
				
			||||||
    bid: IBid;
 | 
					  bid: IBid;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,8 +3,8 @@
 | 
				
			||||||
import { clsx, type ClassValue } from "clsx";
 | 
					import { clsx, type ClassValue } from "clsx";
 | 
				
			||||||
import { twMerge } from "tailwind-merge";
 | 
					import { twMerge } from "tailwind-merge";
 | 
				
			||||||
import moment from "moment";
 | 
					import moment from "moment";
 | 
				
			||||||
import { IWebBid } from "../system/type";
 | 
					import { IBid, IMetadata, IWebBid } from "../system/type";
 | 
				
			||||||
import _ from 'lodash'
 | 
					import _ from "lodash";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function cn(...args: ClassValue[]) {
 | 
					export function cn(...args: ClassValue[]) {
 | 
				
			||||||
  return twMerge(clsx(args));
 | 
					  return twMerge(clsx(args));
 | 
				
			||||||
| 
						 | 
					@ -204,7 +204,6 @@ export function extractDomainSmart(url: string) {
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
export function findNearestClosingChild(webBid: IWebBid) {
 | 
					export function findNearestClosingChild(webBid: IWebBid) {
 | 
				
			||||||
  const now = Date.now();
 | 
					  const now = Date.now();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -255,11 +254,37 @@ export function formatTimeFromMinutes(minutes: number): string {
 | 
				
			||||||
  const hours = Math.floor((minutes % (60 * 24)) / 60);
 | 
					  const hours = Math.floor((minutes % (60 * 24)) / 60);
 | 
				
			||||||
  const mins = minutes % 60;
 | 
					  const mins = minutes % 60;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let result = '';
 | 
					  let result = "";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (days > 0) result += `${days} ${days > 1? 'days' :'day'} `;
 | 
					  if (days > 0) result += `${days} ${days > 1 ? "days" : "day"} `;
 | 
				
			||||||
  if (hours > 0) result += `${hours} ${hours > 1 ? 'hours' : 'hour'} `;
 | 
					  if (hours > 0) result += `${hours} ${hours > 1 ? "hours" : "hour"} `;
 | 
				
			||||||
  if (mins > 0 || result === '') result += `${mins} minutes`;
 | 
					  if (mins > 0 || result === "") result += `${mins} minutes`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return result.trim();
 | 
					  return result.trim();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const getMode = (row: IBid) => {
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    row.metadata.find((item) => item.key_name === "mode_key")?.value || "live"
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const getEarlyTrackingSeconds = (row: IBid) => {
 | 
				
			||||||
 | 
					  const mode = getMode(row);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    row.metadata.find(
 | 
				
			||||||
 | 
					      (item) => item.key_name === `early_tracking_seconds_${mode}`
 | 
				
			||||||
 | 
					    )?.value || row.web_bid.early_tracking_seconds
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const getResponseDemo = (data: IBid) => {
 | 
				
			||||||
 | 
					  if (!data?.metadata) return null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    (data?.metadata.find(
 | 
				
			||||||
 | 
					      (item) => item.key_name === "demo_response"
 | 
				
			||||||
 | 
					    ) as IMetadata) || null
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,178 +0,0 @@
 | 
				
			||||||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
 | 
					 | 
				
			||||||
<html>
 | 
					 | 
				
			||||||
<head>
 | 
					 | 
				
			||||||
<meta http-equiv=3D"Content-Type" content=3D"text/html; charset=3Dutf-8">
 | 
					 | 
				
			||||||
<title>Callcentric Email</title>
 | 
					 | 
				
			||||||
<style type=3D"text/css" media=3D"all">
 | 
					 | 
				
			||||||
body {
 | 
					 | 
				
			||||||
        font-family: arial,helvetica,sans-serif;
 | 
					 | 
				
			||||||
        font-size: 14px;
 | 
					 | 
				
			||||||
        line-height: 22px;
 | 
					 | 
				
			||||||
        color: #444;
 | 
					 | 
				
			||||||
        margin: 0;
 | 
					 | 
				
			||||||
        padding: 20px 0;
 | 
					 | 
				
			||||||
        background-color: #e5e5e5;
 | 
					 | 
				
			||||||
        outline: 0;
 | 
					 | 
				
			||||||
        vertical-align: top;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
p {
 | 
					 | 
				
			||||||
        margin-bottom: 1.5em;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
img {
 | 
					 | 
				
			||||||
        display: block;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
a:link,
 | 
					 | 
				
			||||||
a:visited {
 | 
					 | 
				
			||||||
        color: #006699;
 | 
					 | 
				
			||||||
        text-decoration: underline;
 | 
					 | 
				
			||||||
        margin: 0px;
 | 
					 | 
				
			||||||
        padding: 0px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
a:hover {=20
 | 
					 | 
				
			||||||
        text-decoration: none;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
li {
 | 
					 | 
				
			||||||
        margin-bottom: 1em;
 | 
					 | 
				
			||||||
        line-height: 22px;
 | 
					 | 
				
			||||||
}=09
 | 
					 | 
				
			||||||
=2Efooter a {
 | 
					 | 
				
			||||||
        color: #aaaaaa;
 | 
					 | 
				
			||||||
        text-decoration: underline;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
=2Efooter a:hover {
 | 
					 | 
				
			||||||
        text-decoration: none;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
=2Eframe {
 | 
					 | 
				
			||||||
        width: 580px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
#btn {
 | 
					 | 
				
			||||||
  background: #3498db;
 | 
					 | 
				
			||||||
  background-image: -webkit-linear-gradient(top, #3498db, #2980b9);
 | 
					 | 
				
			||||||
  background-image: -moz-linear-gradient(top, #3498db, #2980b9);
 | 
					 | 
				
			||||||
  background-image: -ms-linear-gradient(top, #3498db, #2980b9);
 | 
					 | 
				
			||||||
  background-image: -o-linear-gradient(top, #3498db, #2980b9);
 | 
					 | 
				
			||||||
  background-image: linear-gradient(to bottom, #3498db, #2980b9);
 | 
					 | 
				
			||||||
  -webkit-border-radius: 28;
 | 
					 | 
				
			||||||
  -moz-border-radius: 28;
 | 
					 | 
				
			||||||
  border-radius: 28px;
 | 
					 | 
				
			||||||
  font-family: Arial;
 | 
					 | 
				
			||||||
  color: #ffffff;
 | 
					 | 
				
			||||||
  font-size: 20px;
 | 
					 | 
				
			||||||
  padding: 10px 20px 10px 20px;
 | 
					 | 
				
			||||||
  text-decoration: none;
 | 
					 | 
				
			||||||
  display: inline-block;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
#btn:hover {
 | 
					 | 
				
			||||||
  background: #3cb0fd;
 | 
					 | 
				
			||||||
  background-image: -webkit-linear-gradient(top, #3cb0fd, #3498db);
 | 
					 | 
				
			||||||
  background-image: -moz-linear-gradient(top, #3cb0fd, #3498db);
 | 
					 | 
				
			||||||
  background-image: -ms-linear-gradient(top, #3cb0fd, #3498db);
 | 
					 | 
				
			||||||
  background-image: -o-linear-gradient(top, #3cb0fd, #3498db);
 | 
					 | 
				
			||||||
  background-image: linear-gradient(to bottom, #3cb0fd, #3498db);
 | 
					 | 
				
			||||||
  text-decoration: none;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
=2Esms-box { font-family: arial,helvetica,sans-serif; font-size: 14px; padd=
 | 
					 | 
				
			||||||
ing:12px; border:1px solid #e0e0e0; background-color: #e0e0e0; -webkit-bord=
 | 
					 | 
				
			||||||
er-radius: 12px; -moz-border-radius: 12px; border-radius: 12px; word-wrap:b=
 | 
					 | 
				
			||||||
reak-word; display:inline-block; }
 | 
					 | 
				
			||||||
p { font-family: arial,helvetica,sans-serif; font-size: 14px; margin-bottom=
 | 
					 | 
				
			||||||
: 1.5em; line-height: 22px; }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@media only screen and (max-device-width: 480px) {
 | 
					 | 
				
			||||||
        .frame {
 | 
					 | 
				
			||||||
                width: 442px !important;
 | 
					 | 
				
			||||||
         }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        p {
 | 
					 | 
				
			||||||
                font-size: 22px !important;
 | 
					 | 
				
			||||||
                line-height: 28px !important;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        ul li {
 | 
					 | 
				
			||||||
                font-size: 22px !important;
 | 
					 | 
				
			||||||
                line-height: 28px !important;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        .section_header{
 | 
					 | 
				
			||||||
                font-size: 28px !important;
 | 
					 | 
				
			||||||
                line-height: 30px !important;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        #btn {
 | 
					 | 
				
			||||||
                font-size: 28px !important;
 | 
					 | 
				
			||||||
                padding: 14px 24px 14px 24px !important;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
</style>
 | 
					 | 
				
			||||||
</head>
 | 
					 | 
				
			||||||
<body style=3D"font-family: Arial, sans-serif; font-size: 14px; margin-bott=
 | 
					 | 
				
			||||||
om: 1.5em; line-height: 22px; color: black; width: 100% !important; margin:=
 | 
					 | 
				
			||||||
 0; padding: 0px; background-color: #e5e5e5; outline: 0; vertical-align: to=
 | 
					 | 
				
			||||||
p;" bgcolor=3D"#e5e5e5">
 | 
					 | 
				
			||||||
<table width=3D"100%" cellpadding=3D"0" cellspacing=3D"0" border=3D"0"><tr>=
 | 
					 | 
				
			||||||
<td style=3D"padding: 0px 0px;" align=3D"center">
 | 
					 | 
				
			||||||
<table class=3D"head" width=3D"100%" bgcolor=3D"#c9c9c9" style=3D"backgroun=
 | 
					 | 
				
			||||||
d-color:#c9c9c9;">
 | 
					 | 
				
			||||||
        <tr><td align=3D"center" style=3D"color:#f3f3f3; padding:5px; font-size:12=
 | 
					 | 
				
			||||||
px;">Please do not respond to this automated message, you will not receive =
 | 
					 | 
				
			||||||
a reply</td></tr>
 | 
					 | 
				
			||||||
</table>
 | 
					 | 
				
			||||||
</td></tr><tr><td style=3D"padding: 0px 10px;" align=3D"center">
 | 
					 | 
				
			||||||
<table class=3D"frame" width=3D"580" cellpadding=3D"0" cellspacing=3D"0" bo=
 | 
					 | 
				
			||||||
rder=3D"0" align=3D"center"><tbody>
 | 
					 | 
				
			||||||
        <tr><td>
 | 
					 | 
				
			||||||
                <table width=3D"100%" cellpadding=3D"0" cellspacing=3D"0" border=3D"0">
 | 
					 | 
				
			||||||
                        <tr><td style=3D"padding: 30px 0px 10px 0px; color: #444; text-align: le=
 | 
					 | 
				
			||||||
ft; vertical-align: middle;" align=3D"left"><a href=3D"https://www.callcent=
 | 
					 | 
				
			||||||
ric.com"><img src=3D"https://www.callcentric.com/img/callcentric_logo_250_t=
 | 
					 | 
				
			||||||
rans.png" width=3D"125" height=3D"33" alt=3D"Callcentric" border=3D"0"></a>=
 | 
					 | 
				
			||||||
</td></tr>
 | 
					 | 
				
			||||||
                </table>
 | 
					 | 
				
			||||||
        </td></tr>
 | 
					 | 
				
			||||||
        <tr><td style=3D"background-color: #ffffff; padding: 0px 0px 50px; border-=
 | 
					 | 
				
			||||||
radius: 5px 5px 0px 0px;">
 | 
					 | 
				
			||||||
                <table style=3D"padding-top: 40px;" width=3D"100%" cellpadding=3D"0" cell=
 | 
					 | 
				
			||||||
spacing=3D"0" border=3D"0"><tbody>
 | 
					 | 
				
			||||||
                        <tr><td class=3D"flex-column" style=3D"width: 40px;"></td>
 | 
					 | 
				
			||||||
                        <td style=3D"text-align: left;" align=3D"left">
 | 
					 | 
				
			||||||
<p>Dear Morgan Vu,</p><p>You have received an inbound text message from 223=
 | 
					 | 
				
			||||||
95 on your DID 17146893350:</p><div class=3D"sms-box">Your LANGTONS verific=
 | 
					 | 
				
			||||||
ation code is: 012152. This code will expire in 10 minutes.</div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                        </td><td class=3D"flex-column" style=3D"width: 40px;"></td></tr>
 | 
					 | 
				
			||||||
                </tbody></table>
 | 
					 | 
				
			||||||
        </td></tr>
 | 
					 | 
				
			||||||
        <tr><td align=3D"left" class=3D"footer" style=3D"background-color: #f2f2f2=
 | 
					 | 
				
			||||||
; color: #999999; padding: 0px 0px 40px; border-radius: 0px 0px 5px 5px;">
 | 
					 | 
				
			||||||
                <table border=3D"0" cellpadding=3D"0" cellspacing=3D"0" style=3D"padding-=
 | 
					 | 
				
			||||||
top: 40px;" width=3D"100%"><tbody>
 | 
					 | 
				
			||||||
                        <tr><td style=3D"width: 40px;"></td>
 | 
					 | 
				
			||||||
                                <td align=3D"left" style=3D"text-align: left; font-family:Arial, Helvet=
 | 
					 | 
				
			||||||
ica, sans-serif; font-size: 15px; line-height: 22px; color: #999999;">
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Follow us on Twitter at:<br>
 | 
					 | 
				
			||||||
<a href=3D"https://twitter.com/Callcentric">https://twitter.com/Callcentric=
 | 
					 | 
				
			||||||
</a>
 | 
					 | 
				
			||||||
<br><br>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
If you have any questions/concerns, please contact our Customer Service Tea=
 | 
					 | 
				
			||||||
m via the following page:<br>
 | 
					 | 
				
			||||||
<a href=3D"https://www.callcentric.com/CustomerService">https://www.callcen=
 | 
					 | 
				
			||||||
tric.com/CustomerService</a><br><br>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
For instructions on how to unsubscribe from our Newsletters/Service Announc=
 | 
					 | 
				
			||||||
ements please visit:<br>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<a href=3D"https://www.callcentric.com/faq/11/306">https://www.callcentric=
 | 
					 | 
				
			||||||
=2Ecom/faq/11/306</a><br><br>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<a href=3D"https://www.callcentric.com">www.callcentric.com</a><br><br>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                                </td><td style=3D"width: 40px;"></td>
 | 
					 | 
				
			||||||
                        </tr>
 | 
					 | 
				
			||||||
                </tbody></table>
 | 
					 | 
				
			||||||
        </td></tr>
 | 
					 | 
				
			||||||
        <tr><td style=3D"padding: 20px 0; font-size: 15px; line-height: 25px; colo=
 | 
					 | 
				
			||||||
r: #999999; text-align: center;" align=3D"center"></td></tr>
 | 
					 | 
				
			||||||
</tbody></table></td></tr></table>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
</body>
 | 
					 | 
				
			||||||
</html>
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1 +1 @@
 | 
				
			||||||
{"createdAt":1747011314493}
 | 
					{"createdAt":1752455341798}
 | 
				
			||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| 
						 | 
					@ -25,6 +25,8 @@
 | 
				
			||||||
    "migration:revert": "npm run typeorm -- -d ./src/config/typeorm.ts migration:revert"
 | 
					    "migration:revert": "npm run typeorm -- -d ./src/config/typeorm.ts migration:revert"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "dependencies": {
 | 
					  "dependencies": {
 | 
				
			||||||
 | 
					    "@nestjs-modules/mailer": "^2.0.2",
 | 
				
			||||||
 | 
					    "@nestjs/bull": "^11.0.2",
 | 
				
			||||||
    "@nestjs/common": "^10.0.0",
 | 
					    "@nestjs/common": "^10.0.0",
 | 
				
			||||||
    "@nestjs/config": "^4.0.1",
 | 
					    "@nestjs/config": "^4.0.1",
 | 
				
			||||||
    "@nestjs/core": "^10.0.0",
 | 
					    "@nestjs/core": "^10.0.0",
 | 
				
			||||||
| 
						 | 
					@ -33,21 +35,26 @@
 | 
				
			||||||
    "@nestjs/mapped-types": "*",
 | 
					    "@nestjs/mapped-types": "*",
 | 
				
			||||||
    "@nestjs/platform-express": "^10.4.15",
 | 
					    "@nestjs/platform-express": "^10.4.15",
 | 
				
			||||||
    "@nestjs/platform-socket.io": "^11.0.11",
 | 
					    "@nestjs/platform-socket.io": "^11.0.11",
 | 
				
			||||||
 | 
					    "@nestjs/schedule": "^6.0.0",
 | 
				
			||||||
    "@nestjs/throttler": "^6.4.0",
 | 
					    "@nestjs/throttler": "^6.4.0",
 | 
				
			||||||
    "@nestjs/typeorm": "^11.0.0",
 | 
					    "@nestjs/typeorm": "^11.0.0",
 | 
				
			||||||
    "@nestjs/websockets": "^11.0.11",
 | 
					    "@nestjs/websockets": "^11.0.11",
 | 
				
			||||||
    "axios": "^1.8.3",
 | 
					    "axios": "^1.8.3",
 | 
				
			||||||
    "bcrypt": "^5.1.1",
 | 
					    "bcrypt": "^5.1.1",
 | 
				
			||||||
 | 
					    "bull": "^4.16.5",
 | 
				
			||||||
    "class-transformer": "^0.5.1",
 | 
					    "class-transformer": "^0.5.1",
 | 
				
			||||||
    "class-validator": "^0.14.1",
 | 
					    "class-validator": "^0.14.1",
 | 
				
			||||||
    "cookie": "^1.0.2",
 | 
					    "cookie": "^1.0.2",
 | 
				
			||||||
    "cookie-parser": "^1.4.7",
 | 
					    "cookie-parser": "^1.4.7",
 | 
				
			||||||
    "dayjs": "^1.11.13",
 | 
					    "dayjs": "^1.11.13",
 | 
				
			||||||
    "imap": "^0.8.19",
 | 
					    "imap": "^0.8.19",
 | 
				
			||||||
 | 
					    "ioredis": "^5.6.1",
 | 
				
			||||||
    "lodash": "^4.17.21",
 | 
					    "lodash": "^4.17.21",
 | 
				
			||||||
 | 
					    "moment": "^2.30.1",
 | 
				
			||||||
    "multer": "^1.4.5-lts.1",
 | 
					    "multer": "^1.4.5-lts.1",
 | 
				
			||||||
    "mysql2": "^3.13.0",
 | 
					    "mysql2": "^3.13.0",
 | 
				
			||||||
    "nestjs-paginate": "^11.1.0",
 | 
					    "nestjs-paginate": "^11.1.0",
 | 
				
			||||||
 | 
					    "nodemailer": "^7.0.3",
 | 
				
			||||||
    "reflect-metadata": "^0.2.0",
 | 
					    "reflect-metadata": "^0.2.0",
 | 
				
			||||||
    "rxjs": "^7.8.1",
 | 
					    "rxjs": "^7.8.1",
 | 
				
			||||||
    "sharp": "^0.33.5",
 | 
					    "sharp": "^0.33.5",
 | 
				
			||||||
| 
						 | 
					@ -67,7 +74,7 @@
 | 
				
			||||||
    "@types/jest": "^29.5.2",
 | 
					    "@types/jest": "^29.5.2",
 | 
				
			||||||
    "@types/lodash": "^4.17.16",
 | 
					    "@types/lodash": "^4.17.16",
 | 
				
			||||||
    "@types/multer": "^1.4.12",
 | 
					    "@types/multer": "^1.4.12",
 | 
				
			||||||
    "@types/node": "^20.3.1",
 | 
					    "@types/node": "^20.17.46",
 | 
				
			||||||
    "@types/supertest": "^6.0.0",
 | 
					    "@types/supertest": "^6.0.0",
 | 
				
			||||||
    "@typescript-eslint/eslint-plugin": "^6.0.0",
 | 
					    "@typescript-eslint/eslint-plugin": "^6.0.0",
 | 
				
			||||||
    "@typescript-eslint/parser": "^6.0.0",
 | 
					    "@typescript-eslint/parser": "^6.0.0",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -14,6 +14,9 @@ import {
 | 
				
			||||||
import { AuthorizationMiddleware } from './modules/admins/middlewares/authorization.middleware';
 | 
					import { AuthorizationMiddleware } from './modules/admins/middlewares/authorization.middleware';
 | 
				
			||||||
import { ClientAuthenticationMiddleware } from './modules/auth/middlewares/client-authentication.middleware';
 | 
					import { ClientAuthenticationMiddleware } from './modules/auth/middlewares/client-authentication.middleware';
 | 
				
			||||||
import { NotificationModule } from './modules/notification/notification.module';
 | 
					import { NotificationModule } from './modules/notification/notification.module';
 | 
				
			||||||
 | 
					import { ScrapsModule } from './modules/scraps/scraps.module';
 | 
				
			||||||
 | 
					import { ExtensionsModule } from './modules/extensions/extensions.module';
 | 
				
			||||||
 | 
					import { MailsModule } from './modules/mails/mails.module';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Module({
 | 
					@Module({
 | 
				
			||||||
  imports: [
 | 
					  imports: [
 | 
				
			||||||
| 
						 | 
					@ -24,6 +27,9 @@ import { NotificationModule } from './modules/notification/notification.module';
 | 
				
			||||||
    AuthModule,
 | 
					    AuthModule,
 | 
				
			||||||
    AdminsModule,
 | 
					    AdminsModule,
 | 
				
			||||||
    NotificationModule,
 | 
					    NotificationModule,
 | 
				
			||||||
 | 
					    ScrapsModule,
 | 
				
			||||||
 | 
					    ExtensionsModule,
 | 
				
			||||||
 | 
					    MailsModule,
 | 
				
			||||||
  ],
 | 
					  ],
 | 
				
			||||||
  controllers: [],
 | 
					  controllers: [],
 | 
				
			||||||
  providers: [],
 | 
					  providers: [],
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5,14 +5,25 @@ import { BadRequestExceptionFilter } from './system/filters/bad-request-exceptio
 | 
				
			||||||
import { IoAdapter } from '@nestjs/platform-socket.io';
 | 
					import { IoAdapter } from '@nestjs/platform-socket.io';
 | 
				
			||||||
import * as cookieParser from 'cookie-parser';
 | 
					import * as cookieParser from 'cookie-parser';
 | 
				
			||||||
import { useContainer } from 'class-validator';
 | 
					import { useContainer } from 'class-validator';
 | 
				
			||||||
 | 
					import { WebBidsService } from './modules/bids/services/web-bids.service';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function bootstrap() {
 | 
					async function bootstrap() {
 | 
				
			||||||
  const app = await NestFactory.create(AppModule);
 | 
					  const app = await NestFactory.create(AppModule);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const prefix_version = '/api/v1';
 | 
					  const prefix_version = '/api/v1';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const webBidService = app.get<WebBidsService>(WebBidsService);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const webs = await webBidService.webBidRepo.find();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const urls = webs.map((web) => web.origin_url);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const corsOrigins = process.env.CORS
 | 
				
			||||||
 | 
					    ? process.env.CORS.split(',').map((origin) => origin.trim())
 | 
				
			||||||
 | 
					    : ['*']; // hoặc [] tùy ý bạn
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  app.enableCors({
 | 
					  app.enableCors({
 | 
				
			||||||
    origin: process.env.CORS.split(', ') || '*',
 | 
					    origin: [...corsOrigins, ...urls],
 | 
				
			||||||
    methods: ['GET', 'PUT', 'POST', 'DELETE', 'PATCH'],
 | 
					    methods: ['GET', 'PUT', 'POST', 'DELETE', 'PATCH'],
 | 
				
			||||||
    allowedHeaders: 'Content-Type, Authorization',
 | 
					    allowedHeaders: 'Content-Type, Authorization',
 | 
				
			||||||
    credentials: true,
 | 
					    credentials: true,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -103,4 +103,5 @@ export class GenerateKeysService {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return AppResponse.toResponse(result);
 | 
					    return AppResponse.toResponse(result);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,7 +1,10 @@
 | 
				
			||||||
import { Module } from '@nestjs/common';
 | 
					import { BullModule } from '@nestjs/bull';
 | 
				
			||||||
import { ConfigModule } from '@nestjs/config';
 | 
					import { Global, Module } from '@nestjs/common';
 | 
				
			||||||
 | 
					import { ConfigModule, ConfigService } from '@nestjs/config';
 | 
				
			||||||
import { EventEmitterModule } from '@nestjs/event-emitter';
 | 
					import { EventEmitterModule } from '@nestjs/event-emitter';
 | 
				
			||||||
 | 
					import { ScheduleModule } from '@nestjs/schedule';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Global()
 | 
				
			||||||
@Module({
 | 
					@Module({
 | 
				
			||||||
  imports: [
 | 
					  imports: [
 | 
				
			||||||
    ConfigModule.forRoot({
 | 
					    ConfigModule.forRoot({
 | 
				
			||||||
| 
						 | 
					@ -11,6 +14,21 @@ import { EventEmitterModule } from '@nestjs/event-emitter';
 | 
				
			||||||
      wildcard: true,
 | 
					      wildcard: true,
 | 
				
			||||||
      global: true,
 | 
					      global: true,
 | 
				
			||||||
    }),
 | 
					    }),
 | 
				
			||||||
 | 
					    BullModule.forRootAsync({
 | 
				
			||||||
 | 
					      imports: [ConfigModule],
 | 
				
			||||||
 | 
					      useFactory: (configService: ConfigService) => ({
 | 
				
			||||||
 | 
					        redis: {
 | 
				
			||||||
 | 
					          host: configService.get<string>('REDIS_HOST'),
 | 
				
			||||||
 | 
					          port: configService.get<number>('REDIS_PORT'),
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      }),
 | 
				
			||||||
 | 
					      inject: [ConfigService],
 | 
				
			||||||
 | 
					    }),
 | 
				
			||||||
 | 
					    BullModule.registerQueue({
 | 
				
			||||||
 | 
					      name: 'mail-queue',
 | 
				
			||||||
 | 
					    }),
 | 
				
			||||||
 | 
					    ScheduleModule.forRoot(),
 | 
				
			||||||
  ],
 | 
					  ],
 | 
				
			||||||
 | 
					  exports: [BullModule],
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
export class AppConfigsModule {}
 | 
					export class AppConfigsModule {}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5,22 +5,17 @@ import { Bid } from '../entities/bid.entity';
 | 
				
			||||||
import { BidsService } from '../services/bids.service';
 | 
					import { BidsService } from '../services/bids.service';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Injectable()
 | 
					@Injectable()
 | 
				
			||||||
export class GraysApi {
 | 
					export class AllBidsApi {
 | 
				
			||||||
 | 
					  constructor(private readonly bidsService: BidsService) {}
 | 
				
			||||||
 | 
					 | 
				
			||||||
  constructor(private readonly bidsService: BidsService){}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async getHistoriesBid(lot_id: Bid['lot_id']) {
 | 
					  async getHistoriesBid(lot_id: Bid['lot_id']) {
 | 
				
			||||||
 | 
					    const bid = await this.bidsService.bidsRepo.findOne({
 | 
				
			||||||
 | 
					      where: { lot_id },
 | 
				
			||||||
    const bid= await this.bidsService.bidsRepo.findOne({where: {lot_id, }, relations: {web_bid: true}})
 | 
					      relations: { web_bid: true },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
 | 
					      switch (bid.web_bid.origin_url) {
 | 
				
			||||||
      switch(bid.web_bid.origin_url){
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // GRAYS
 | 
					        // GRAYS
 | 
				
			||||||
        case 'https://www.grays.com': {
 | 
					        case 'https://www.grays.com': {
 | 
				
			||||||
          const response = await axios({
 | 
					          const response = await axios({
 | 
				
			||||||
| 
						 | 
					@ -31,12 +26,11 @@ export class GraysApi {
 | 
				
			||||||
            return AppResponse.toResponse(response.data.Bids);
 | 
					            return AppResponse.toResponse(response.data.Bids);
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          return AppResponse.toResponse([])
 | 
					          return AppResponse.toResponse([]);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // PICKLES
 | 
					        // PICKLES
 | 
				
			||||||
        case 'https://www.pickles.com.au': {
 | 
					        case 'https://www.pickles.com.au': {
 | 
				
			||||||
          
 | 
					 | 
				
			||||||
          const response = await axios({
 | 
					          const response = await axios({
 | 
				
			||||||
            url: `https://www.pickles.com.au/PWR-Web/services/api/bidHistoryService/bidHistory?item=${lot_id}`,
 | 
					            url: `https://www.pickles.com.au/PWR-Web/services/api/bidHistoryService/bidHistory?item=${lot_id}`,
 | 
				
			||||||
          });
 | 
					          });
 | 
				
			||||||
| 
						 | 
					@ -45,13 +39,11 @@ export class GraysApi {
 | 
				
			||||||
            return AppResponse.toResponse(response.data.Bids);
 | 
					            return AppResponse.toResponse(response.data.Bids);
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          return AppResponse.toResponse([])
 | 
					          return AppResponse.toResponse([]);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        default:
 | 
					        default:
 | 
				
			||||||
          return AppResponse.toResponse([])
 | 
					          return AppResponse.toResponse([]);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					 | 
				
			||||||
      
 | 
					 | 
				
			||||||
    } catch (error) {
 | 
					    } catch (error) {
 | 
				
			||||||
      return AppResponse.toResponse([]);
 | 
					      return AppResponse.toResponse([]);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,62 @@
 | 
				
			||||||
 | 
					import { Injectable } from '@nestjs/common';
 | 
				
			||||||
 | 
					import axios from 'axios';
 | 
				
			||||||
 | 
					import AppResponse from 'src/response/app-response';
 | 
				
			||||||
 | 
					import { Bid } from '../entities/bid.entity';
 | 
				
			||||||
 | 
					import { BidsService } from '../services/bids.service';
 | 
				
			||||||
 | 
					import * as _ from 'lodash';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Injectable()
 | 
				
			||||||
 | 
					export class AuctionHistoresApi {
 | 
				
			||||||
 | 
					  constructor(private readonly bidsService: BidsService) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async getHistoriesBid(lot_id: Bid['lot_id']) {
 | 
				
			||||||
 | 
					    const bid = await this.bidsService.bidsRepo.findOne({
 | 
				
			||||||
 | 
					      where: { lot_id },
 | 
				
			||||||
 | 
					      relations: { web_bid: true, metadata: true },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      switch (bid.web_bid.origin_url) {
 | 
				
			||||||
 | 
					        // GRAYS
 | 
				
			||||||
 | 
					        case 'https://www.grays.com': {
 | 
				
			||||||
 | 
					          const response = await axios({
 | 
				
			||||||
 | 
					            url: `https://www.grays.com/api/LotInfo/GetBiddingHistory?lotId=${lot_id}¤cyCode=AUD`,
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          if (response.data && response.data?.Bids) {
 | 
				
			||||||
 | 
					            return AppResponse.toResponse(response.data.Bids);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          return AppResponse.toResponse([]);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // PICKLES
 | 
				
			||||||
 | 
					        case 'https://www.pickles.com.au': {
 | 
				
			||||||
 | 
					          const response = await axios({
 | 
				
			||||||
 | 
					            url: `https://www.pickles.com.au/PWR-Web/services/api/bidHistoryService/bidHistory?item=${lot_id}`,
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          if (response.data) {
 | 
				
			||||||
 | 
					            return AppResponse.toResponse(response.data.Bids);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          return AppResponse.toResponse([]);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        // ALLBIDS
 | 
				
			||||||
 | 
					        case 'https://www.allbids.com.au': {
 | 
				
			||||||
 | 
					          const data = bid.metadata.find(
 | 
				
			||||||
 | 
					            (meta) => meta.key_name === 'competor_histories',
 | 
				
			||||||
 | 
					          )?.value;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          const sorted = _.orderBy(data, ['amount'], ['desc']);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          return AppResponse.toResponse(sorted || []);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        default:
 | 
				
			||||||
 | 
					          return AppResponse.toResponse([]);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      return AppResponse.toResponse([]);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,31 @@
 | 
				
			||||||
 | 
					import { Injectable } from '@nestjs/common';
 | 
				
			||||||
 | 
					import { ConfigService } from '@nestjs/config';
 | 
				
			||||||
 | 
					import axios from 'axios';
 | 
				
			||||||
 | 
					import { BidsService } from '../services/bids.service';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Injectable()
 | 
				
			||||||
 | 
					export class HotItemApi {
 | 
				
			||||||
 | 
					  constructor(
 | 
				
			||||||
 | 
					    private readonly bidsService: BidsService,
 | 
				
			||||||
 | 
					    private readonly configService: ConfigService,
 | 
				
			||||||
 | 
					  ) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  listHotItem = async () => {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const res = await axios({
 | 
				
			||||||
 | 
					        method: 'GET',
 | 
				
			||||||
 | 
					        baseURL: this.configService.get('NEW_ITEM_BASE_URL'),
 | 
				
			||||||
 | 
					        url: '/disti/api/hotitem',
 | 
				
			||||||
 | 
					        headers: {
 | 
				
			||||||
 | 
					          //   ...axios.defaults.headers.common,
 | 
				
			||||||
 | 
					          'Content-Type': 'application/json',
 | 
				
			||||||
 | 
					          Authorization: 'Bearer ' + this.configService.get('NEW_ITEM_TOKEN'),
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return res.data.data || [];
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      return [];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1,9 +1,9 @@
 | 
				
			||||||
import { Module } from '@nestjs/common';
 | 
					import { forwardRef, Module } from '@nestjs/common';
 | 
				
			||||||
import { TypeOrmModule } from '@nestjs/typeorm';
 | 
					import { TypeOrmModule } from '@nestjs/typeorm';
 | 
				
			||||||
import { AdminsModule } from '../admins/admins.module';
 | 
					import { AdminsModule } from '../admins/admins.module';
 | 
				
			||||||
import { NotificationModule } from '../notification/notification.module';
 | 
					import { NotificationModule } from '../notification/notification.module';
 | 
				
			||||||
import { BotTelegramApi } from './apis/bot-telegram.api';
 | 
					import { BotTelegramApi } from './apis/bot-telegram.api';
 | 
				
			||||||
import { GraysApi } from './apis/grays.api';
 | 
					import { AuctionHistoresApi } from './apis/auction-histories.api';
 | 
				
			||||||
import { AdminBidHistoriesController } from './controllers/admin/admin-bid-histories.controller';
 | 
					import { AdminBidHistoriesController } from './controllers/admin/admin-bid-histories.controller';
 | 
				
			||||||
import { AdminBidsController } from './controllers/admin/admin-bids.controller';
 | 
					import { AdminBidsController } from './controllers/admin/admin-bids.controller';
 | 
				
			||||||
import { AdminOutBidLogsController } from './controllers/admin/admin-out-bid-logs.controller';
 | 
					import { AdminOutBidLogsController } from './controllers/admin/admin-out-bid-logs.controller';
 | 
				
			||||||
| 
						 | 
					@ -27,6 +27,15 @@ import { SendMessageHistoriesService } from './services/send-message-histories.s
 | 
				
			||||||
import { WebBidsService } from './services/web-bids.service';
 | 
					import { WebBidsService } from './services/web-bids.service';
 | 
				
			||||||
import { DashboardService } from './services/dashboard.service';
 | 
					import { DashboardService } from './services/dashboard.service';
 | 
				
			||||||
import { AdminDashboardController } from './controllers/admin/admin-dashboard.controller';
 | 
					import { AdminDashboardController } from './controllers/admin/admin-dashboard.controller';
 | 
				
			||||||
 | 
					import { TasksService } from './services/tasks.servise';
 | 
				
			||||||
 | 
					import { ConfigsService } from './services/configs.service';
 | 
				
			||||||
 | 
					import { Config } from './entities/configs.entity';
 | 
				
			||||||
 | 
					import { AdminConfigsController } from './controllers/admin/admin-configs.controller';
 | 
				
			||||||
 | 
					import { BidMetadatasService } from './services/bid-metadatas.service';
 | 
				
			||||||
 | 
					import { BidMetadata } from './entities/bid-metadata.entity';
 | 
				
			||||||
 | 
					import { HotItemApi } from './apis/hot-item.api';
 | 
				
			||||||
 | 
					import { MailsModule } from '../mails/mails.module';
 | 
				
			||||||
 | 
					import { AdminBidMetadataController } from './controllers/admin/admin-bid-metadata.controller';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Module({
 | 
					@Module({
 | 
				
			||||||
  imports: [
 | 
					  imports: [
 | 
				
			||||||
| 
						 | 
					@ -36,6 +45,8 @@ import { AdminDashboardController } from './controllers/admin/admin-dashboard.co
 | 
				
			||||||
      OutBidLog,
 | 
					      OutBidLog,
 | 
				
			||||||
      WebBid,
 | 
					      WebBid,
 | 
				
			||||||
      SendMessageHistory,
 | 
					      SendMessageHistory,
 | 
				
			||||||
 | 
					      Config,
 | 
				
			||||||
 | 
					      BidMetadata,
 | 
				
			||||||
    ]),
 | 
					    ]),
 | 
				
			||||||
    // AuthModule,
 | 
					    // AuthModule,
 | 
				
			||||||
    AdminsModule,
 | 
					    AdminsModule,
 | 
				
			||||||
| 
						 | 
					@ -51,6 +62,8 @@ import { AdminDashboardController } from './controllers/admin/admin-dashboard.co
 | 
				
			||||||
    AdminWebBidsController,
 | 
					    AdminWebBidsController,
 | 
				
			||||||
    AdminSendMessageHistoriesController,
 | 
					    AdminSendMessageHistoriesController,
 | 
				
			||||||
    AdminDashboardController,
 | 
					    AdminDashboardController,
 | 
				
			||||||
 | 
					    AdminConfigsController,
 | 
				
			||||||
 | 
					    AdminBidMetadataController,
 | 
				
			||||||
  ],
 | 
					  ],
 | 
				
			||||||
  providers: [
 | 
					  providers: [
 | 
				
			||||||
    BidsService,
 | 
					    BidsService,
 | 
				
			||||||
| 
						 | 
					@ -60,11 +73,24 @@ import { AdminDashboardController } from './controllers/admin/admin-dashboard.co
 | 
				
			||||||
    OutBidLogsService,
 | 
					    OutBidLogsService,
 | 
				
			||||||
    WebBidsService,
 | 
					    WebBidsService,
 | 
				
			||||||
    BotTelegramApi,
 | 
					    BotTelegramApi,
 | 
				
			||||||
    GraysApi,
 | 
					    AuctionHistoresApi,
 | 
				
			||||||
    SendMessageHistoriesService,
 | 
					    SendMessageHistoriesService,
 | 
				
			||||||
    ImapService,
 | 
					    ImapService,
 | 
				
			||||||
    DashboardService,
 | 
					    DashboardService,
 | 
				
			||||||
 | 
					    TasksService,
 | 
				
			||||||
 | 
					    ConfigsService,
 | 
				
			||||||
 | 
					    BidMetadatasService,
 | 
				
			||||||
 | 
					    HotItemApi,
 | 
				
			||||||
 | 
					  ],
 | 
				
			||||||
 | 
					  exports: [
 | 
				
			||||||
 | 
					    BotTelegramApi,
 | 
				
			||||||
 | 
					    SendMessageHistoriesService,
 | 
				
			||||||
 | 
					    BidMetadatasService,
 | 
				
			||||||
 | 
					    BidsService,
 | 
				
			||||||
 | 
					    ConfigsService,
 | 
				
			||||||
 | 
					    DashboardService,
 | 
				
			||||||
 | 
					    HotItemApi,
 | 
				
			||||||
 | 
					    WebBidsService,
 | 
				
			||||||
  ],
 | 
					  ],
 | 
				
			||||||
  exports: [BotTelegramApi, SendMessageHistoriesService, BidsService],
 | 
					 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
export class BidsModule {}
 | 
					export class BidsModule {}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,13 +4,13 @@ import { CreateBidDto } from '../../dto/bid/create-bid.dto';
 | 
				
			||||||
import { BidHistoriesService } from '../../services/bid-histories.service';
 | 
					import { BidHistoriesService } from '../../services/bid-histories.service';
 | 
				
			||||||
import { CreateBidHistoryDto } from '../../dto/bid-history/create-bid-history.dto';
 | 
					import { CreateBidHistoryDto } from '../../dto/bid-history/create-bid-history.dto';
 | 
				
			||||||
import { Bid } from '../../entities/bid.entity';
 | 
					import { Bid } from '../../entities/bid.entity';
 | 
				
			||||||
import { GraysApi } from '../../apis/grays.api';
 | 
					import { AuctionHistoresApi } from '../../apis/auction-histories.api';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Controller('admin/bid-histories')
 | 
					@Controller('admin/bid-histories')
 | 
				
			||||||
export class AdminBidHistoriesController {
 | 
					export class AdminBidHistoriesController {
 | 
				
			||||||
  constructor(
 | 
					  constructor(
 | 
				
			||||||
    private readonly bidHistoriesService: BidHistoriesService,
 | 
					    private readonly bidHistoriesService: BidHistoriesService,
 | 
				
			||||||
    private readonly graysApi: GraysApi,
 | 
					    private readonly auctionHistoresApi: AuctionHistoresApi,
 | 
				
			||||||
  ) {}
 | 
					  ) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @Post()
 | 
					  @Post()
 | 
				
			||||||
| 
						 | 
					@ -20,6 +20,6 @@ export class AdminBidHistoriesController {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @Get('detail/:lot_id')
 | 
					  @Get('detail/:lot_id')
 | 
				
			||||||
  async getBidHistories(@Param('lot_id') lot_id: Bid['lot_id']) {
 | 
					  async getBidHistories(@Param('lot_id') lot_id: Bid['lot_id']) {
 | 
				
			||||||
    return await this.graysApi.getHistoriesBid(lot_id);
 | 
					    return await this.auctionHistoresApi.getHistoriesBid(lot_id);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,21 @@
 | 
				
			||||||
 | 
					import { Body, Controller, Delete, Param, Post, Put } from '@nestjs/common';
 | 
				
			||||||
 | 
					import { BidMetadata } from '../../entities/bid-metadata.entity';
 | 
				
			||||||
 | 
					import { BidMetadatasService } from '../../services/bid-metadatas.service';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Controller('admin/bid-metadatas')
 | 
				
			||||||
 | 
					export class AdminBidMetadataController {
 | 
				
			||||||
 | 
					  constructor(private readonly bidMetadataService: BidMetadatasService) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Delete(':id')
 | 
				
			||||||
 | 
					  async delete(@Param('id') id: BidMetadata['id']) {
 | 
				
			||||||
 | 
					    return await this.bidMetadataService.delete(id);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Put(':id')
 | 
				
			||||||
 | 
					  async update(
 | 
				
			||||||
 | 
					    @Param('id') id: BidMetadata['id'],
 | 
				
			||||||
 | 
					    @Body('value') value: string,
 | 
				
			||||||
 | 
					  ) {
 | 
				
			||||||
 | 
					    return await this.bidMetadataService.update(id, value);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -67,4 +67,19 @@ export class AdminBidsController {
 | 
				
			||||||
  ) {
 | 
					  ) {
 | 
				
			||||||
    return await this.bidsService.getImagesWorking(id, type);
 | 
					    return await this.bidsService.getImagesWorking(id, type);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Get('record/:name')
 | 
				
			||||||
 | 
					  async getRecord(@Param('name') name: string, @Res() res: Response) {
 | 
				
			||||||
 | 
					    return await this.bidsService.getRecord(name, res);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Get('records/:id')
 | 
				
			||||||
 | 
					  async getRecords(@Param('id') id: Bid['id']) {
 | 
				
			||||||
 | 
					    return await this.bidsService.getRecords(id);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Delete('record/:name')
 | 
				
			||||||
 | 
					  async deleteRecord(@Param('name') name: string) {
 | 
				
			||||||
 | 
					    return await this.bidsService.deleteRecord(name);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,20 @@
 | 
				
			||||||
 | 
					import { Body, Controller, Get, Param, Post } from '@nestjs/common';
 | 
				
			||||||
 | 
					import { DashboardService } from '../../services/dashboard.service';
 | 
				
			||||||
 | 
					import { Config } from '../../entities/configs.entity';
 | 
				
			||||||
 | 
					import { ConfigsService } from '../../services/configs.service';
 | 
				
			||||||
 | 
					import { UpsertConfigDto } from '../../dto/config/upsert-config.dto';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Controller('admin/configs')
 | 
				
			||||||
 | 
					export class AdminConfigsController {
 | 
				
			||||||
 | 
					  constructor(private readonly configsService: ConfigsService) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Post('upsert')
 | 
				
			||||||
 | 
					  async upsertConfig(@Body() data: UpsertConfigDto) {
 | 
				
			||||||
 | 
					    return await this.configsService.upsertConfig(data);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Get(':key')
 | 
				
			||||||
 | 
					  async getConfig(@Param('key') key: Config['key_name']) {
 | 
				
			||||||
 | 
					    return await this.configsService.getConfigRes(key);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1,3 +1,4 @@
 | 
				
			||||||
 | 
					import AppResponse from '@/response/app-response';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  Body,
 | 
					  Body,
 | 
				
			||||||
  Controller,
 | 
					  Controller,
 | 
				
			||||||
| 
						 | 
					@ -5,6 +6,7 @@ import {
 | 
				
			||||||
  Param,
 | 
					  Param,
 | 
				
			||||||
  Post,
 | 
					  Post,
 | 
				
			||||||
  Put,
 | 
					  Put,
 | 
				
			||||||
 | 
					  Query,
 | 
				
			||||||
  UploadedFile,
 | 
					  UploadedFile,
 | 
				
			||||||
  UseInterceptors,
 | 
					  UseInterceptors,
 | 
				
			||||||
} from '@nestjs/common';
 | 
					} from '@nestjs/common';
 | 
				
			||||||
| 
						 | 
					@ -12,7 +14,9 @@ import { EventEmitter2 } from '@nestjs/event-emitter';
 | 
				
			||||||
import { FileInterceptor } from '@nestjs/platform-express';
 | 
					import { FileInterceptor } from '@nestjs/platform-express';
 | 
				
			||||||
import { plainToClass } from 'class-transformer';
 | 
					import { plainToClass } from 'class-transformer';
 | 
				
			||||||
import { memoryStorage } from 'multer';
 | 
					import { memoryStorage } from 'multer';
 | 
				
			||||||
 | 
					import { ClientInfoUpdateBidDto } from '../../dto/bid/client-info-update-bid.dto';
 | 
				
			||||||
import { ClientUpdateBidDto } from '../../dto/bid/client-update-bid.dto';
 | 
					import { ClientUpdateBidDto } from '../../dto/bid/client-update-bid.dto';
 | 
				
			||||||
 | 
					import { ClientUpdateLoginStatusDto } from '../../dto/bid/client-update-login-status.dto';
 | 
				
			||||||
import { CreateBidDto } from '../../dto/bid/create-bid.dto';
 | 
					import { CreateBidDto } from '../../dto/bid/create-bid.dto';
 | 
				
			||||||
import { UpdateStatusByPriceDto } from '../../dto/bid/update-status-by-price.dto';
 | 
					import { UpdateStatusByPriceDto } from '../../dto/bid/update-status-by-price.dto';
 | 
				
			||||||
import { Bid } from '../../entities/bid.entity';
 | 
					import { Bid } from '../../entities/bid.entity';
 | 
				
			||||||
| 
						 | 
					@ -20,8 +24,6 @@ import { WebBid } from '../../entities/wed-bid.entity';
 | 
				
			||||||
import { BidsService } from '../../services/bids.service';
 | 
					import { BidsService } from '../../services/bids.service';
 | 
				
			||||||
import { WebBidsService } from '../../services/web-bids.service';
 | 
					import { WebBidsService } from '../../services/web-bids.service';
 | 
				
			||||||
import { Event } from '../../utils/events';
 | 
					import { Event } from '../../utils/events';
 | 
				
			||||||
import AppResponse from '@/response/app-response';
 | 
					 | 
				
			||||||
import { ClientUpdateLoginStatusDto } from '../../dto/bid/client-update-login-status.dto';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Controller('bids')
 | 
					@Controller('bids')
 | 
				
			||||||
export class BidsController {
 | 
					export class BidsController {
 | 
				
			||||||
| 
						 | 
					@ -36,9 +38,14 @@ export class BidsController {
 | 
				
			||||||
    return this.bidsService.clientIndex();
 | 
					    return this.bidsService.clientIndex();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Get(':model')
 | 
				
			||||||
 | 
					  getByModel(@Param('model') model: Bid['model']) {
 | 
				
			||||||
 | 
					    return this.bidsService.getBidByModel(model);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @Post()
 | 
					  @Post()
 | 
				
			||||||
  create(@Body() data: CreateBidDto) {
 | 
					  create(@Body() data: CreateBidDto) {
 | 
				
			||||||
    return this.bidsService.create(data);
 | 
					    return this.bidsService.create(data, (result) => !!result);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @Put(':id')
 | 
					  @Put(':id')
 | 
				
			||||||
| 
						 | 
					@ -46,6 +53,11 @@ export class BidsController {
 | 
				
			||||||
    return this.bidsService.clientUpdate(id, data);
 | 
					    return this.bidsService.clientUpdate(id, data);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Put('info/:id')
 | 
				
			||||||
 | 
					  updateInfo(@Param('id') id: Bid['id'], @Body() data: ClientInfoUpdateBidDto) {
 | 
				
			||||||
 | 
					    return this.bidsService.update(id, data);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @Post('out-bid/:id')
 | 
					  @Post('out-bid/:id')
 | 
				
			||||||
  outBidStatus(@Param('id') id: Bid['id']) {
 | 
					  outBidStatus(@Param('id') id: Bid['id']) {
 | 
				
			||||||
    return this.bidsService.outBid(id);
 | 
					    return this.bidsService.outBid(id);
 | 
				
			||||||
| 
						 | 
					@ -69,11 +81,18 @@ export class BidsController {
 | 
				
			||||||
    return this.bidsService.updateStatusWork(id, type, image);
 | 
					    return this.bidsService.updateStatusWork(id, type, image);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @Post('update-login-status')
 | 
					  @Post('upload-record/:id')
 | 
				
			||||||
  async updateLoginStatus(
 | 
					  @UseInterceptors(FileInterceptor('video', { storage: memoryStorage() }))
 | 
				
			||||||
    @Body() data: ClientUpdateLoginStatusDto
 | 
					  async uploadRecord(
 | 
				
			||||||
 | 
					    @Param('id') id: Bid['id'],
 | 
				
			||||||
 | 
					    @UploadedFile() video: Express.Multer.File,
 | 
				
			||||||
  ) {
 | 
					  ) {
 | 
				
			||||||
    return await this.bidsService.emitLoginStatus(data)
 | 
					    return this.bidsService.uploadRecord(id, video);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Post('update-login-status')
 | 
				
			||||||
 | 
					  async updateLoginStatus(@Body() data: ClientUpdateLoginStatusDto) {
 | 
				
			||||||
 | 
					    return await this.bidsService.emitLoginStatus(data);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @Post('test')
 | 
					  @Post('test')
 | 
				
			||||||
| 
						 | 
					@ -92,4 +111,13 @@ export class BidsController {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return AppResponse.toResponse({ code });
 | 
					    return AppResponse.toResponse({ code });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Post('hook-action')
 | 
				
			||||||
 | 
					  async hookAction(
 | 
				
			||||||
 | 
					    @Body() data: any,
 | 
				
			||||||
 | 
					    @Query('id') id: Bid['id'],
 | 
				
			||||||
 | 
					    @Query('type') type: 'action' | 'api',
 | 
				
			||||||
 | 
					  ) {
 | 
				
			||||||
 | 
					    return this.bidsService.hookAction({ id, type }, data);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,19 @@
 | 
				
			||||||
 | 
					import { IsArray, IsNumber, IsOptional } from 'class-validator';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class ClientInfoUpdateBidDto {
 | 
				
			||||||
 | 
					  @IsNumber()
 | 
				
			||||||
 | 
					  @IsOptional()
 | 
				
			||||||
 | 
					  max_price: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @IsNumber()
 | 
				
			||||||
 | 
					  @IsOptional()
 | 
				
			||||||
 | 
					  quantity: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @IsNumber()
 | 
				
			||||||
 | 
					  @IsOptional()
 | 
				
			||||||
 | 
					  plus_price: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @IsArray()
 | 
				
			||||||
 | 
					  @IsOptional()
 | 
				
			||||||
 | 
					  metadata: Record<string, any>[];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1,4 +1,4 @@
 | 
				
			||||||
import { IsNumber, IsOptional, IsString } from 'class-validator';
 | 
					import { IsNumber, IsObject, IsOptional, IsString } from 'class-validator';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class ClientUpdateBidDto {
 | 
					export class ClientUpdateBidDto {
 | 
				
			||||||
  @IsString()
 | 
					  @IsString()
 | 
				
			||||||
| 
						 | 
					@ -24,4 +24,8 @@ export class ClientUpdateBidDto {
 | 
				
			||||||
  @IsNumber()
 | 
					  @IsNumber()
 | 
				
			||||||
  @IsOptional()
 | 
					  @IsOptional()
 | 
				
			||||||
  reserve_price: number;
 | 
					  reserve_price: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @IsObject()
 | 
				
			||||||
 | 
					  @IsOptional()
 | 
				
			||||||
 | 
					  metadata: Record<string, any>;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,5 +1,6 @@
 | 
				
			||||||
import { Optional } from '@nestjs/common';
 | 
					import { Optional } from '@nestjs/common';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
 | 
					  IsArray,
 | 
				
			||||||
  IsBoolean,
 | 
					  IsBoolean,
 | 
				
			||||||
  IsNumber,
 | 
					  IsNumber,
 | 
				
			||||||
  IsOptional,
 | 
					  IsOptional,
 | 
				
			||||||
| 
						 | 
					@ -21,4 +22,8 @@ export class CreateBidDto {
 | 
				
			||||||
  @IsNumber()
 | 
					  @IsNumber()
 | 
				
			||||||
  @IsOptional()
 | 
					  @IsOptional()
 | 
				
			||||||
  plus_price: number;
 | 
					  plus_price: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @IsArray()
 | 
				
			||||||
 | 
					  @IsOptional()
 | 
				
			||||||
 | 
					  metadata: Record<string, any>[];
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,4 +1,4 @@
 | 
				
			||||||
import { IsNumber, IsOptional } from 'class-validator';
 | 
					import { IsArray, IsNumber, IsOptional } from 'class-validator';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class UpdateBidDto {
 | 
					export class UpdateBidDto {
 | 
				
			||||||
  @IsNumber()
 | 
					  @IsNumber()
 | 
				
			||||||
| 
						 | 
					@ -12,4 +12,8 @@ export class UpdateBidDto {
 | 
				
			||||||
  @IsNumber()
 | 
					  @IsNumber()
 | 
				
			||||||
  @IsOptional()
 | 
					  @IsOptional()
 | 
				
			||||||
  plus_price: number;
 | 
					  plus_price: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @IsArray()
 | 
				
			||||||
 | 
					  @IsOptional()
 | 
				
			||||||
 | 
					  metadata: Record<string, any>[];
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,13 @@
 | 
				
			||||||
 | 
					import { IsEnum, IsNumber, IsOptional, IsString } from 'class-validator';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class UpsertConfigDto {
 | 
				
			||||||
 | 
					  @IsString()
 | 
				
			||||||
 | 
					  key_name: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @IsString()
 | 
				
			||||||
 | 
					  @IsOptional()
 | 
				
			||||||
 | 
					  value: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @IsEnum(['string', 'number'])
 | 
				
			||||||
 | 
					  type: 'string' | 'number';
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1,4 +1,4 @@
 | 
				
			||||||
import { IsNumber, IsString, IsUrl } from 'class-validator';
 | 
					import { IsNumber, IsOptional, IsString, IsUrl, Min } from 'class-validator';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class CreateWebBidDto {
 | 
					export class CreateWebBidDto {
 | 
				
			||||||
  @IsUrl()
 | 
					  @IsUrl()
 | 
				
			||||||
| 
						 | 
					@ -6,4 +6,14 @@ export class CreateWebBidDto {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @IsUrl()
 | 
					  @IsUrl()
 | 
				
			||||||
  url: string;
 | 
					  url: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @IsNumber()
 | 
				
			||||||
 | 
					  @Min(60)
 | 
				
			||||||
 | 
					  @IsOptional()
 | 
				
			||||||
 | 
					  arrival_offset_seconds: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @IsNumber()
 | 
				
			||||||
 | 
					  @Min(600)
 | 
				
			||||||
 | 
					  @IsOptional()
 | 
				
			||||||
 | 
					  early_tracking_seconds: number;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,67 @@
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  Column,
 | 
				
			||||||
 | 
					  Entity,
 | 
				
			||||||
 | 
					  ManyToOne,
 | 
				
			||||||
 | 
					  PrimaryGeneratedColumn,
 | 
				
			||||||
 | 
					  Unique,
 | 
				
			||||||
 | 
					} from 'typeorm';
 | 
				
			||||||
 | 
					import { Bid } from './bid.entity';
 | 
				
			||||||
 | 
					import { Timestamp } from './timestamp';
 | 
				
			||||||
 | 
					import { WebBid } from './wed-bid.entity';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Entity('bid_metadata')
 | 
				
			||||||
 | 
					@Unique(['key_name', 'bid'])
 | 
				
			||||||
 | 
					export class BidMetadata extends Timestamp {
 | 
				
			||||||
 | 
					  // value 'live' | 'sandbox'
 | 
				
			||||||
 | 
					  public static MODE_KEY = 'mode_key';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public static DEMO_RESPONSE = 'demo_response';
 | 
				
			||||||
 | 
					  public static LATEST_RECROD_LINK = 'lastest_record_link';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // value is minutes esg: arrival_offset_seconds of web bid parent
 | 
				
			||||||
 | 
					  public static ARRIAVAL_OFFSET_SECONDS_LIVE = 'arrival_offset_seconds_live';
 | 
				
			||||||
 | 
					  public static ARRIAVAL_OFFSET_SECONDS_SANDBOX =
 | 
				
			||||||
 | 
					    'arrival_offset_seconds_sandbox';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // value is minutes esg: early_tracking_seconds of web bid parent
 | 
				
			||||||
 | 
					  public static EARLY_TRACKING_SECONDS_LIVE = 'early_tracking_seconds_live';
 | 
				
			||||||
 | 
					  public static EARLY_TRACKING_SECONDS_SANDBOX =
 | 
				
			||||||
 | 
					    'early_tracking_seconds_sandbox';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public static DEFAULT_META_DATA = (webBid: WebBid) => {
 | 
				
			||||||
 | 
					    return [
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        key_name: BidMetadata.MODE_KEY,
 | 
				
			||||||
 | 
					        value: JSON.stringify('live'),
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        key_name: BidMetadata.ARRIAVAL_OFFSET_SECONDS_LIVE,
 | 
				
			||||||
 | 
					        value: String(webBid.arrival_offset_seconds),
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        key_name: BidMetadata.EARLY_TRACKING_SECONDS_LIVE,
 | 
				
			||||||
 | 
					        value: String(webBid.early_tracking_seconds),
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        key_name: BidMetadata.ARRIAVAL_OFFSET_SECONDS_SANDBOX,
 | 
				
			||||||
 | 
					        value: String(webBid.arrival_offset_seconds),
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        key_name: BidMetadata.EARLY_TRACKING_SECONDS_SANDBOX,
 | 
				
			||||||
 | 
					        value: String(webBid.early_tracking_seconds),
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    ] as BidMetadata[];
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @PrimaryGeneratedColumn('increment')
 | 
				
			||||||
 | 
					  id: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Column()
 | 
				
			||||||
 | 
					  key_name: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Column({ type: 'json' })
 | 
				
			||||||
 | 
					  value: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @ManyToOne(() => Bid, (bid) => bid.metadata, { onDelete: 'CASCADE' })
 | 
				
			||||||
 | 
					  bid: Bid;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -9,6 +9,7 @@ import { Timestamp } from './timestamp';
 | 
				
			||||||
import { BidHistory } from './bid-history.entity';
 | 
					import { BidHistory } from './bid-history.entity';
 | 
				
			||||||
import { WebBid } from './wed-bid.entity';
 | 
					import { WebBid } from './wed-bid.entity';
 | 
				
			||||||
import { SendMessageHistory } from './send-message-histories.entity';
 | 
					import { SendMessageHistory } from './send-message-histories.entity';
 | 
				
			||||||
 | 
					import { BidMetadata } from './bid-metadata.entity';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Entity('bids')
 | 
					@Entity('bids')
 | 
				
			||||||
export class Bid extends Timestamp {
 | 
					export class Bid extends Timestamp {
 | 
				
			||||||
| 
						 | 
					@ -45,6 +46,9 @@ export class Bid extends Timestamp {
 | 
				
			||||||
  @Column({ default: null, nullable: true })
 | 
					  @Column({ default: null, nullable: true })
 | 
				
			||||||
  close_time: string;
 | 
					  close_time: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Column({ default: null, nullable: true })
 | 
				
			||||||
 | 
					  close_time_ts: Date | null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @Column({ default: null, nullable: true })
 | 
					  @Column({ default: null, nullable: true })
 | 
				
			||||||
  start_bid_time: string;
 | 
					  start_bid_time: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -66,4 +70,7 @@ export class Bid extends Timestamp {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @ManyToOne(() => WebBid, (web) => web.children, { onDelete: 'CASCADE' })
 | 
					  @ManyToOne(() => WebBid, (web) => web.children, { onDelete: 'CASCADE' })
 | 
				
			||||||
  web_bid: WebBid;
 | 
					  web_bid: WebBid;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @OneToMany(() => BidMetadata, (metadata) => metadata.bid, { cascade: true })
 | 
				
			||||||
 | 
					  metadata: BidMetadata[];
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,17 @@
 | 
				
			||||||
 | 
					import { Column, Entity, PrimaryGeneratedColumn, Unique } from 'typeorm';
 | 
				
			||||||
 | 
					import { Timestamp } from './timestamp';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Entity('configs')
 | 
				
			||||||
 | 
					export class Config extends Timestamp {
 | 
				
			||||||
 | 
					  @PrimaryGeneratedColumn('increment')
 | 
				
			||||||
 | 
					  id: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Column({ unique: true })
 | 
				
			||||||
 | 
					  key_name: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Column({ nullable: true, default: true })
 | 
				
			||||||
 | 
					  value: string | null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Column()
 | 
				
			||||||
 | 
					  type: 'string' | 'number';
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1,8 +1,16 @@
 | 
				
			||||||
import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
 | 
					import {
 | 
				
			||||||
 | 
					  Column,
 | 
				
			||||||
 | 
					  Entity,
 | 
				
			||||||
 | 
					  JoinColumn,
 | 
				
			||||||
 | 
					  ManyToOne,
 | 
				
			||||||
 | 
					  PrimaryGeneratedColumn,
 | 
				
			||||||
 | 
					  Unique,
 | 
				
			||||||
 | 
					} from 'typeorm';
 | 
				
			||||||
import { Timestamp } from './timestamp';
 | 
					import { Timestamp } from './timestamp';
 | 
				
			||||||
import { Bid } from './bid.entity';
 | 
					import { Bid } from './bid.entity';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Entity('send_message_histories')
 | 
					@Entity('send_message_histories')
 | 
				
			||||||
 | 
					@Unique(['max_price', 'type', 'reserve_price', 'bid'])
 | 
				
			||||||
export class SendMessageHistory extends Timestamp {
 | 
					export class SendMessageHistory extends Timestamp {
 | 
				
			||||||
  @PrimaryGeneratedColumn('increment')
 | 
					  @PrimaryGeneratedColumn('increment')
 | 
				
			||||||
  id: number;
 | 
					  id: number;
 | 
				
			||||||
| 
						 | 
					@ -17,4 +25,10 @@ export class SendMessageHistory extends Timestamp {
 | 
				
			||||||
    onDelete: 'CASCADE',
 | 
					    onDelete: 'CASCADE',
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
  bid: Bid;
 | 
					  bid: Bid;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Column({ default: 0, nullable: true })
 | 
				
			||||||
 | 
					  max_price: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Column({ default: 0, nullable: true })
 | 
				
			||||||
 | 
					  reserve_price: number;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,7 +1,14 @@
 | 
				
			||||||
import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm';
 | 
					import {
 | 
				
			||||||
 | 
					  Column,
 | 
				
			||||||
 | 
					  Entity,
 | 
				
			||||||
 | 
					  OneToMany,
 | 
				
			||||||
 | 
					  OneToOne,
 | 
				
			||||||
 | 
					  PrimaryGeneratedColumn,
 | 
				
			||||||
 | 
					} from 'typeorm';
 | 
				
			||||||
import { Timestamp } from './timestamp';
 | 
					import { Timestamp } from './timestamp';
 | 
				
			||||||
import { Bid } from './bid.entity';
 | 
					import { Bid } from './bid.entity';
 | 
				
			||||||
import { Exclude } from 'class-transformer';
 | 
					import { Exclude } from 'class-transformer';
 | 
				
			||||||
 | 
					import { ScrapConfig } from '@/modules/scraps/entities/scrap-config.entity';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Entity('web_bids')
 | 
					@Entity('web_bids')
 | 
				
			||||||
export class WebBid extends Timestamp {
 | 
					export class WebBid extends Timestamp {
 | 
				
			||||||
| 
						 | 
					@ -37,4 +44,7 @@ export class WebBid extends Timestamp {
 | 
				
			||||||
    cascade: true,
 | 
					    cascade: true,
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
  children: Bid[];
 | 
					  children: Bid[];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @OneToOne(() => ScrapConfig, (scrap) => scrap.web_bid)
 | 
				
			||||||
 | 
					  scrap_config: ScrapConfig;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -21,10 +21,14 @@ export class ImageCompressionPipe implements PipeTransform {
 | 
				
			||||||
      throw new BadRequestException('No file provided');
 | 
					      throw new BadRequestException('No file provided');
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const isVideo = file.mimetype === 'video/mp4';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const ext = path.extname(file.originalname).toLowerCase();
 | 
					    const ext = path.extname(file.originalname).toLowerCase();
 | 
				
			||||||
    if (!ALLOWED_IMAGE_EXT.includes(ext)) {
 | 
					    const isImage = ALLOWED_IMAGE_EXT.includes(ext);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!isImage && !isVideo) {
 | 
				
			||||||
      throw new BadRequestException(
 | 
					      throw new BadRequestException(
 | 
				
			||||||
        `File "${file.originalname}" is not a valid image.`,
 | 
					        `File "${file.originalname}" is not a valid image or video.`,
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -36,16 +40,37 @@ export class ImageCompressionPipe implements PipeTransform {
 | 
				
			||||||
        const files = readdirSync(uploadDir);
 | 
					        const files = readdirSync(uploadDir);
 | 
				
			||||||
        files.forEach((file) => {
 | 
					        files.forEach((file) => {
 | 
				
			||||||
          const filePath = path.join(uploadDir, file);
 | 
					          const filePath = path.join(uploadDir, file);
 | 
				
			||||||
          if (/\.(jpg|jpeg|png|webp)$/i.test(file)) {
 | 
					          if (/\.(jpg|jpeg|png|webp|mp4)$/i.test(file)) {
 | 
				
			||||||
            unlinkSync(filePath);
 | 
					            unlinkSync(filePath);
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const filename = this.options.unique_name
 | 
					    let filename = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (isVideo) {
 | 
				
			||||||
 | 
					      filename = this.options.unique_name
 | 
				
			||||||
 | 
					        ? `${Date.now()}-${file.originalname}`
 | 
				
			||||||
 | 
					        : file.originalname;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const filePath = path.join(uploadDir, filename);
 | 
				
			||||||
 | 
					      // Ghi trực tiếp buffer ra file
 | 
				
			||||||
 | 
					      await new Promise<void>((resolve, reject) => {
 | 
				
			||||||
 | 
					        const fs = require('fs');
 | 
				
			||||||
 | 
					        fs.writeFile(filePath, file.buffer, (err) => {
 | 
				
			||||||
 | 
					          if (err) reject(err);
 | 
				
			||||||
 | 
					          else resolve();
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return { filename };
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    filename = this.options.unique_name
 | 
				
			||||||
      ? `${Date.now()}-${this.resize.width || Constant.DEFAULT_RESIZE}x${this.resize.height || Constant.DEFAULT_RESIZE}-${file.originalname.replace(ext, '.webp')}`
 | 
					      ? `${Date.now()}-${this.resize.width || Constant.DEFAULT_RESIZE}x${this.resize.height || Constant.DEFAULT_RESIZE}-${file.originalname.replace(ext, '.webp')}`
 | 
				
			||||||
      : file.originalname.replace(ext, '.webp');
 | 
					      : file.originalname.replace(ext, '.webp');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const filePath = path.join(uploadDir, filename);
 | 
					    const filePath = path.join(uploadDir, filename);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -16,6 +16,8 @@ import { SendMessageHistoriesService } from './send-message-histories.service';
 | 
				
			||||||
import { NotificationService } from '@/modules/notification/notification.service';
 | 
					import { NotificationService } from '@/modules/notification/notification.service';
 | 
				
			||||||
import { isTimeReached } from '@/ultils';
 | 
					import { isTimeReached } from '@/ultils';
 | 
				
			||||||
import { BidsService } from './bids.service';
 | 
					import { BidsService } from './bids.service';
 | 
				
			||||||
 | 
					import { EventEmitter2 } from '@nestjs/event-emitter';
 | 
				
			||||||
 | 
					import { Event } from '../utils/events';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Injectable()
 | 
					@Injectable()
 | 
				
			||||||
export class BidHistoriesService {
 | 
					export class BidHistoriesService {
 | 
				
			||||||
| 
						 | 
					@ -28,6 +30,7 @@ export class BidHistoriesService {
 | 
				
			||||||
    readonly sendMessageHistoriesService: SendMessageHistoriesService,
 | 
					    readonly sendMessageHistoriesService: SendMessageHistoriesService,
 | 
				
			||||||
    private readonly notificationService: NotificationService,
 | 
					    private readonly notificationService: NotificationService,
 | 
				
			||||||
    private readonly bidsService: BidsService,
 | 
					    private readonly bidsService: BidsService,
 | 
				
			||||||
 | 
					    private eventEmitter: EventEmitter2,
 | 
				
			||||||
  ) {}
 | 
					  ) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async index() {
 | 
					  async index() {
 | 
				
			||||||
| 
						 | 
					@ -38,6 +41,7 @@ export class BidHistoriesService {
 | 
				
			||||||
    // Tìm thông tin bid từ database
 | 
					    // Tìm thông tin bid từ database
 | 
				
			||||||
    const bid = await this.bidsService.bidsRepo.findOne({
 | 
					    const bid = await this.bidsService.bidsRepo.findOne({
 | 
				
			||||||
      where: { id: bid_id },
 | 
					      where: { id: bid_id },
 | 
				
			||||||
 | 
					      relations: { web_bid: true },
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Nếu không tìm thấy bid, trả về lỗi 404
 | 
					    // Nếu không tìm thấy bid, trả về lỗi 404
 | 
				
			||||||
| 
						 | 
					@ -102,7 +106,10 @@ export class BidHistoriesService {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Gửi thông tin bid đến bot telegram
 | 
					    // Gửi thông tin bid đến bot telegram
 | 
				
			||||||
    const botData = { ...bid, histories: response };
 | 
					    const botData = { ...bid, histories: response };
 | 
				
			||||||
    this.botTelegramApi.sendBidInfo(botData);
 | 
					    // this.botTelegramApi.sendBidInfo(botData);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Send event thống place bid
 | 
				
			||||||
 | 
					    this.eventEmitter.emit(Event.BID_SUBMITED, botData);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Lưu message đã gửi để theo dõi
 | 
					    // Lưu message đã gửi để theo dõi
 | 
				
			||||||
    this.sendMessageHistoriesService.sendMessageRepo.save({
 | 
					    this.sendMessageHistoriesService.sendMessageRepo.save({
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,131 @@
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  BadRequestException,
 | 
				
			||||||
 | 
					  HttpStatus,
 | 
				
			||||||
 | 
					  Injectable,
 | 
				
			||||||
 | 
					  NotFoundException,
 | 
				
			||||||
 | 
					} from '@nestjs/common';
 | 
				
			||||||
 | 
					import { InjectRepository } from '@nestjs/typeorm';
 | 
				
			||||||
 | 
					import { Repository } from 'typeorm';
 | 
				
			||||||
 | 
					import { BidMetadata } from '../entities/bid-metadata.entity';
 | 
				
			||||||
 | 
					import { Bid } from '../entities/bid.entity';
 | 
				
			||||||
 | 
					import AppResponse from '@/response/app-response';
 | 
				
			||||||
 | 
					import { plainToClass } from 'class-transformer';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Injectable()
 | 
				
			||||||
 | 
					export class BidMetadatasService {
 | 
				
			||||||
 | 
					  constructor(
 | 
				
			||||||
 | 
					    @InjectRepository(BidMetadata)
 | 
				
			||||||
 | 
					    readonly bidMetadataRepo: Repository<BidMetadata>,
 | 
				
			||||||
 | 
					  ) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async upsert(data: Record<string, any>, bid: Bid) {
 | 
				
			||||||
 | 
					    const existingMetadata = await this.bidMetadataRepo.find({
 | 
				
			||||||
 | 
					      where: { bid: { id: bid.id } },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const existingMap = new Map(
 | 
				
			||||||
 | 
					      existingMetadata.map((item) => [item.key_name, item]),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const toSave: BidMetadata[] = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for (const [key, value] of Object.entries(data)) {
 | 
				
			||||||
 | 
					      const existing = existingMap.get(key);
 | 
				
			||||||
 | 
					      if (existing) {
 | 
				
			||||||
 | 
					        existing.value = value;
 | 
				
			||||||
 | 
					        toSave.push(existing);
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        toSave.push(
 | 
				
			||||||
 | 
					          this.bidMetadataRepo.create({
 | 
				
			||||||
 | 
					            key_name: key,
 | 
				
			||||||
 | 
					            value,
 | 
				
			||||||
 | 
					            bid: { id: bid.id },
 | 
				
			||||||
 | 
					          }),
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await this.bidMetadataRepo.save(toSave);
 | 
				
			||||||
 | 
					    return true;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async update(id: BidMetadata['id'], value: string) {
 | 
				
			||||||
 | 
					    const result = await this.bidMetadataRepo.update(id, { value: value });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!result) throw new BadRequestException(AppResponse.toResponse(null));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const data = await this.bidMetadataRepo.findOne({ where: { id } });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return AppResponse.toResponse(plainToClass(BidMetadata, data));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  getMode = (metadatas: BidMetadata[]) => {
 | 
				
			||||||
 | 
					    const result = metadatas.find(
 | 
				
			||||||
 | 
					      (item) => item.key_name === BidMetadata.MODE_KEY,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return result ? result.value : null;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  getDemoResponse = (metadatas: BidMetadata[]) => {
 | 
				
			||||||
 | 
					    const result = metadatas.find(
 | 
				
			||||||
 | 
					      (item) => item.key_name === BidMetadata.DEMO_RESPONSE,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return result ? result.value : null;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  getLatestRecordLink = (metadatas: BidMetadata[]) => {
 | 
				
			||||||
 | 
					    const result = metadatas.find(
 | 
				
			||||||
 | 
					      (item) => item.key_name === BidMetadata.LATEST_RECROD_LINK,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return result ? result.value : null;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  getArrivalOffsetSecondsSandbox = (metadatas: BidMetadata[]) => {
 | 
				
			||||||
 | 
					    const result = metadatas.find(
 | 
				
			||||||
 | 
					      (item) => item.key_name === BidMetadata.ARRIAVAL_OFFSET_SECONDS_SANDBOX,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return result ? Number(result.value) : null;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  getArrivalOffsetSecondsLive = (metadatas: BidMetadata[]) => {
 | 
				
			||||||
 | 
					    const result = metadatas.find(
 | 
				
			||||||
 | 
					      (item) => item.key_name === BidMetadata.ARRIAVAL_OFFSET_SECONDS_LIVE,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return result ? Number(result.value) : null;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  getArrivalOffsetSecondsByMode = (metadatas: BidMetadata[]) => {
 | 
				
			||||||
 | 
					    const mode = this.getMode(metadatas) || 'live';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (mode === 'live') {
 | 
				
			||||||
 | 
					      return this.getArrivalOffsetSecondsLive(metadatas);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return this.getArrivalOffsetSecondsSandbox(metadatas);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  isSandbox(metadatas: BidMetadata[]) {
 | 
				
			||||||
 | 
					    return this.getMode(metadatas) !== 'live';
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async delete(id: Bid['id']) {
 | 
				
			||||||
 | 
					    const bid = await this.bidMetadataRepo.findOne({ where: { id } });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!bid)
 | 
				
			||||||
 | 
					      throw new NotFoundException(
 | 
				
			||||||
 | 
					        AppResponse.toResponse(false, {
 | 
				
			||||||
 | 
					          message: 'Meta is not found',
 | 
				
			||||||
 | 
					          status_code: HttpStatus.NOT_FOUND,
 | 
				
			||||||
 | 
					        }),
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await this.bidMetadataRepo.delete({ id: bid.id });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return AppResponse.toResponse(true, { message: 'Delete success !' });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1,3 +1,4 @@
 | 
				
			||||||
 | 
					import { NotificationService } from '@/modules/notification/notification.service';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  BadRequestException,
 | 
					  BadRequestException,
 | 
				
			||||||
  HttpStatus,
 | 
					  HttpStatus,
 | 
				
			||||||
| 
						 | 
					@ -8,7 +9,10 @@ import { EventEmitter2 } from '@nestjs/event-emitter';
 | 
				
			||||||
import { InjectRepository } from '@nestjs/typeorm';
 | 
					import { InjectRepository } from '@nestjs/typeorm';
 | 
				
			||||||
import { plainToClass } from 'class-transformer';
 | 
					import { plainToClass } from 'class-transformer';
 | 
				
			||||||
import { Response } from 'express';
 | 
					import { Response } from 'express';
 | 
				
			||||||
 | 
					import * as fs from 'fs';
 | 
				
			||||||
import { existsSync, readdirSync } from 'fs';
 | 
					import { existsSync, readdirSync } from 'fs';
 | 
				
			||||||
 | 
					import * as _ from 'lodash';
 | 
				
			||||||
 | 
					import { v4 as uuid } from 'uuid';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  FilterOperator,
 | 
					  FilterOperator,
 | 
				
			||||||
  FilterSuffix,
 | 
					  FilterSuffix,
 | 
				
			||||||
| 
						 | 
					@ -18,21 +22,27 @@ import {
 | 
				
			||||||
import { Column } from 'nestjs-paginate/lib/helper';
 | 
					import { Column } from 'nestjs-paginate/lib/helper';
 | 
				
			||||||
import { join } from 'path';
 | 
					import { join } from 'path';
 | 
				
			||||||
import AppResponse from 'src/response/app-response';
 | 
					import AppResponse from 'src/response/app-response';
 | 
				
			||||||
import { extractModelId, isTimeReached, subtractMinutes } from 'src/ultils';
 | 
					import {
 | 
				
			||||||
import { In, Repository } from 'typeorm';
 | 
					  extractModelId,
 | 
				
			||||||
 | 
					  isTimePassedByMinutes,
 | 
				
			||||||
 | 
					  isTimeReached,
 | 
				
			||||||
 | 
					  parseVideoFileName,
 | 
				
			||||||
 | 
					  subtractMinutes,
 | 
				
			||||||
 | 
					} from 'src/ultils';
 | 
				
			||||||
 | 
					import { In, IsNull, Not, Repository } from 'typeorm';
 | 
				
			||||||
import { ClientUpdateBidDto } from '../dto/bid/client-update-bid.dto';
 | 
					import { ClientUpdateBidDto } from '../dto/bid/client-update-bid.dto';
 | 
				
			||||||
 | 
					import { ClientUpdateLoginStatusDto } from '../dto/bid/client-update-login-status.dto';
 | 
				
			||||||
import { CreateBidDto } from '../dto/bid/create-bid.dto';
 | 
					import { CreateBidDto } from '../dto/bid/create-bid.dto';
 | 
				
			||||||
import { UpdateBidDto } from '../dto/bid/update-bid.dto';
 | 
					import { UpdateBidDto } from '../dto/bid/update-bid.dto';
 | 
				
			||||||
import { UpdateStatusByPriceDto } from '../dto/bid/update-status-by-price.dto';
 | 
					import { UpdateStatusByPriceDto } from '../dto/bid/update-status-by-price.dto';
 | 
				
			||||||
import { BidHistory } from '../entities/bid-history.entity';
 | 
					import { BidHistory } from '../entities/bid-history.entity';
 | 
				
			||||||
 | 
					import { BidMetadata } from '../entities/bid-metadata.entity';
 | 
				
			||||||
import { Bid } from '../entities/bid.entity';
 | 
					import { Bid } from '../entities/bid.entity';
 | 
				
			||||||
import { ImageCompressionPipe } from '../pipes/image-compression-pipe';
 | 
					import { ImageCompressionPipe } from '../pipes/image-compression-pipe';
 | 
				
			||||||
import { Constant } from '../utils/constant';
 | 
					import { Constant } from '../utils/constant';
 | 
				
			||||||
import { WebBidsService } from './web-bids.service';
 | 
					 | 
				
			||||||
import { NotificationService } from '@/modules/notification/notification.service';
 | 
					 | 
				
			||||||
import { Event } from '../utils/events';
 | 
					import { Event } from '../utils/events';
 | 
				
			||||||
import _ from 'lodash';
 | 
					import { BidMetadatasService } from './bid-metadatas.service';
 | 
				
			||||||
import { ClientUpdateLoginStatusDto } from '../dto/bid/client-update-login-status.dto';
 | 
					import { WebBidsService } from './web-bids.service';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Injectable()
 | 
					@Injectable()
 | 
				
			||||||
export class BidsService {
 | 
					export class BidsService {
 | 
				
			||||||
| 
						 | 
					@ -42,8 +52,9 @@ export class BidsService {
 | 
				
			||||||
    @InjectRepository(BidHistory)
 | 
					    @InjectRepository(BidHistory)
 | 
				
			||||||
    readonly bidHistoriesRepo: Repository<BidHistory>,
 | 
					    readonly bidHistoriesRepo: Repository<BidHistory>,
 | 
				
			||||||
    private readonly webBidsService: WebBidsService,
 | 
					    private readonly webBidsService: WebBidsService,
 | 
				
			||||||
    private eventEmitter: EventEmitter2,
 | 
					    private readonly eventEmitter: EventEmitter2,
 | 
				
			||||||
    private notificationService: NotificationService,
 | 
					    private readonly notificationService: NotificationService,
 | 
				
			||||||
 | 
					    private readonly bidMetadatasService: BidMetadatasService,
 | 
				
			||||||
  ) {}
 | 
					  ) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async index(query: PaginateQuery) {
 | 
					  async index(query: PaginateQuery) {
 | 
				
			||||||
| 
						 | 
					@ -57,7 +68,7 @@ export class BidsService {
 | 
				
			||||||
      lot_id: true,
 | 
					      lot_id: true,
 | 
				
			||||||
      close_time: true,
 | 
					      close_time: true,
 | 
				
			||||||
      name: [FilterOperator.ILIKE],
 | 
					      name: [FilterOperator.ILIKE],
 | 
				
			||||||
      status: true
 | 
					      status: true,
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    query.filter = AppResponse.processFilters(query.filter, filterableColumns);
 | 
					    query.filter = AppResponse.processFilters(query.filter, filterableColumns);
 | 
				
			||||||
| 
						 | 
					@ -66,6 +77,7 @@ export class BidsService {
 | 
				
			||||||
      sortableColumns: [
 | 
					      sortableColumns: [
 | 
				
			||||||
        'id',
 | 
					        'id',
 | 
				
			||||||
        'close_time',
 | 
					        'close_time',
 | 
				
			||||||
 | 
					        'close_time_ts',
 | 
				
			||||||
        'first_bid',
 | 
					        'first_bid',
 | 
				
			||||||
        'model',
 | 
					        'model',
 | 
				
			||||||
        'lot_id',
 | 
					        'lot_id',
 | 
				
			||||||
| 
						 | 
					@ -81,6 +93,7 @@ export class BidsService {
 | 
				
			||||||
      relations: {
 | 
					      relations: {
 | 
				
			||||||
        histories: true,
 | 
					        histories: true,
 | 
				
			||||||
        web_bid: true,
 | 
					        web_bid: true,
 | 
				
			||||||
 | 
					        metadata: true,
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -100,7 +113,7 @@ export class BidsService {
 | 
				
			||||||
    await this.webBidsService.emitAllBidEvent();
 | 
					    await this.webBidsService.emitAllBidEvent();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async create(data: CreateBidDto) {
 | 
					  async create(data: CreateBidDto, response?: (result: Bid) => any) {
 | 
				
			||||||
    const model = extractModelId(data.url);
 | 
					    const model = extractModelId(data.url);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const prev = await this.bidsRepo.findOne({ where: { model: model } });
 | 
					    const prev = await this.bidsRepo.findOne({ where: { model: model } });
 | 
				
			||||||
| 
						 | 
					@ -113,19 +126,78 @@ export class BidsService {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const webBid = await this.webBidsService.createByUrl(data.url);
 | 
					    const webBid = await this.webBidsService.createByUrl(data.url);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let metadata = BidMetadata.DEFAULT_META_DATA(webBid);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (data.metadata) {
 | 
				
			||||||
 | 
					      metadata = metadata.map((item) => {
 | 
				
			||||||
 | 
					        const reqData = data.metadata.find(
 | 
				
			||||||
 | 
					          (i) => i?.key_name === item.key_name,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (!reqData) return { ...item };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return {
 | 
				
			||||||
 | 
					          ...item,
 | 
				
			||||||
 | 
					          value:
 | 
				
			||||||
 | 
					            item.key_name === BidMetadata.MODE_KEY
 | 
				
			||||||
 | 
					              ? JSON.stringify(reqData.value)
 | 
				
			||||||
 | 
					              : reqData.value <= 0
 | 
				
			||||||
 | 
					                ? item.value
 | 
				
			||||||
 | 
					                : String(reqData.value),
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const result = await this.bidsRepo.save({
 | 
					    const result = await this.bidsRepo.save({
 | 
				
			||||||
      ...data,
 | 
					      ...data,
 | 
				
			||||||
      model,
 | 
					      model,
 | 
				
			||||||
      web_bid: webBid,
 | 
					      web_bid: webBid,
 | 
				
			||||||
 | 
					      metadata,
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await this.emitAllBidEvent();
 | 
					    await this.emitAllBidEvent();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return AppResponse.toResponse(plainToClass(Bid, result));
 | 
					    const warnings = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!webBid.username || !webBid.password) {
 | 
				
			||||||
 | 
					      // Add warning message
 | 
				
			||||||
 | 
					      warnings.push(
 | 
				
			||||||
 | 
					        `Account setup for ${webBid.origin_url} website is not yet complete.`,
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!webBid.active) {
 | 
				
			||||||
 | 
					      // Add warning message
 | 
				
			||||||
 | 
					      warnings.push(
 | 
				
			||||||
 | 
					        `${webBid.origin_url} is disabled. Please enable to continue.`,
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (warnings.length) {
 | 
				
			||||||
 | 
					      // Send event warning
 | 
				
			||||||
 | 
					      this.eventEmitter.emit(Event.SEND_WARNING, {
 | 
				
			||||||
 | 
					        title: `System Warning: Abnormal Data Detected`,
 | 
				
			||||||
 | 
					        messages: warnings,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Send event success
 | 
				
			||||||
 | 
					    this.eventEmitter.emit(Event.SEND_SUCCESS, {
 | 
				
			||||||
 | 
					      title: 'Successfully Added Product',
 | 
				
			||||||
 | 
					      messages: [`Successfully added product to bid list: ${data.url}`],
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return AppResponse.toResponse(
 | 
				
			||||||
 | 
					      response ? response(result) : plainToClass(Bid, result),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async update(id: Bid['id'], data: UpdateBidDto) {
 | 
					  async update(id: Bid['id'], { metadata = [], ...data }: UpdateBidDto) {
 | 
				
			||||||
    const prev = await this.bidsRepo.findOne({ where: { id } });
 | 
					    const prev = await this.bidsRepo.findOne({
 | 
				
			||||||
 | 
					      where: { id },
 | 
				
			||||||
 | 
					      relations: { web_bid: true, metadata: true },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!prev) {
 | 
					    if (!prev) {
 | 
				
			||||||
      throw new NotFoundException(
 | 
					      throw new NotFoundException(
 | 
				
			||||||
        AppResponse.toResponse(false, {
 | 
					        AppResponse.toResponse(false, {
 | 
				
			||||||
| 
						 | 
					@ -135,13 +207,46 @@ export class BidsService {
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const result = await this.bidsRepo.update(id, {
 | 
					    if (metadata) {
 | 
				
			||||||
      ...data,
 | 
					      await this.bidMetadatasService.bidMetadataRepo.upsert(
 | 
				
			||||||
      // status:
 | 
					        metadata.map((item) => {
 | 
				
			||||||
      //   prev.max_price + prev.plus_price > data.max_price
 | 
					          return { ...item, bid: { id }, value: JSON.stringify(item.value) };
 | 
				
			||||||
      //     ? 'out-bid'
 | 
					        }),
 | 
				
			||||||
      //     : prev.status,
 | 
					        ['id', 'bid', 'key_name'],
 | 
				
			||||||
    });
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let result = null;
 | 
				
			||||||
 | 
					    if (!prev.close_time) {
 | 
				
			||||||
 | 
					      // Trường hợp chưa có close_time => update đơn giản
 | 
				
			||||||
 | 
					      result = await this.bidsRepo.update(id, {
 | 
				
			||||||
 | 
					        ...data,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      // Trường hợp đã có close_time => kiểm tra arrival_offset_seconds có thay đổi không
 | 
				
			||||||
 | 
					      const arrival_offset_seconds =
 | 
				
			||||||
 | 
					        this.bidMetadatasService.getArrivalOffsetSecondsByMode(
 | 
				
			||||||
 | 
					          metadata as BidMetadata[],
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      const prev_arrival_offset_seconds =
 | 
				
			||||||
 | 
					        this.bidMetadatasService.getArrivalOffsetSecondsByMode(prev.metadata);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (arrival_offset_seconds !== prev_arrival_offset_seconds) {
 | 
				
			||||||
 | 
					        // Nếu offset thay đổi thì cần cập nhật lại start_bid_time
 | 
				
			||||||
 | 
					        const start_bid_time = arrival_offset_seconds
 | 
				
			||||||
 | 
					          ? subtractMinutes(prev.close_time, arrival_offset_seconds / 60)
 | 
				
			||||||
 | 
					          : prev.start_bid_time;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        result = await this.bidsRepo.update(id, {
 | 
				
			||||||
 | 
					          ...data,
 | 
				
			||||||
 | 
					          start_bid_time,
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        result = await this.bidsRepo.update(id, {
 | 
				
			||||||
 | 
					          ...data,
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!result) throw new BadRequestException(false);
 | 
					    if (!result) throw new BadRequestException(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -150,8 +255,23 @@ export class BidsService {
 | 
				
			||||||
    return AppResponse.toResponse(true);
 | 
					    return AppResponse.toResponse(true);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async getBidForClientUpdate(id: Bid['id']) {
 | 
				
			||||||
 | 
					    return await this.bidsRepo.findOne({
 | 
				
			||||||
 | 
					      where: { id },
 | 
				
			||||||
 | 
					      relations: { histories: true, web_bid: true, metadata: true },
 | 
				
			||||||
 | 
					      order: {
 | 
				
			||||||
 | 
					        histories: {
 | 
				
			||||||
 | 
					          price: 'DESC',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async toggle(id: Bid['id']) {
 | 
					  async toggle(id: Bid['id']) {
 | 
				
			||||||
    const bid = await this.bidsRepo.findOne({ where: { id } });
 | 
					    const bid = await this.bidsRepo.findOne({
 | 
				
			||||||
 | 
					      where: { id },
 | 
				
			||||||
 | 
					      relations: { web_bid: true },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!bid) {
 | 
					    if (!bid) {
 | 
				
			||||||
      throw new NotFoundException(
 | 
					      throw new NotFoundException(
 | 
				
			||||||
| 
						 | 
					@ -201,20 +321,49 @@ export class BidsService {
 | 
				
			||||||
    return AppResponse.toResponse(true);
 | 
					    return AppResponse.toResponse(true);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Workflow
 | 
				
			||||||
 | 
					   * START
 | 
				
			||||||
 | 
					   *  |
 | 
				
			||||||
 | 
					   *  |--> Tìm bid theo id --> Không có? --> Throw 404
 | 
				
			||||||
 | 
					   *  |
 | 
				
			||||||
 | 
					   *  |--> Nếu chưa có thời gian bắt đầu --> Tính offset --> Gán start_time
 | 
				
			||||||
 | 
					   *  |
 | 
				
			||||||
 | 
					   *  |--> Nếu đã hết giờ:
 | 
				
			||||||
 | 
					   *  |     |--> Nếu outbid --> Gán status 'out-bid'
 | 
				
			||||||
 | 
					   *  |     |--> Else --> Gán status 'win-bid'
 | 
				
			||||||
 | 
					   *  |
 | 
				
			||||||
 | 
					   *  |--> Nếu chưa hết giờ:
 | 
				
			||||||
 | 
					   *  |     |--> Nếu vượt giới hạn --> Gán status 'out-bid'
 | 
				
			||||||
 | 
					   *  |
 | 
				
			||||||
 | 
					   *  |--> Nếu close_time mới > cũ --> cập nhật
 | 
				
			||||||
 | 
					   *  |--> Nếu có model mới và chưa có model --> gán
 | 
				
			||||||
 | 
					   *  |
 | 
				
			||||||
 | 
					   *  |--> Gọi `save(...)` để lưu lại DB
 | 
				
			||||||
 | 
					   *  |--> Nếu có metadata --> gọi `upsert`
 | 
				
			||||||
 | 
					   *  |
 | 
				
			||||||
 | 
					   *  |--> Gửi sự kiện emitAllBidEvent
 | 
				
			||||||
 | 
					   *  |--> Nếu status là out-bid hoặc win-bid --> gửi notification
 | 
				
			||||||
 | 
					   *  |
 | 
				
			||||||
 | 
					   * * |--> Trả response
 | 
				
			||||||
 | 
					   * END
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
  async clientUpdate(
 | 
					  async clientUpdate(
 | 
				
			||||||
    id: Bid['id'],
 | 
					    id: Bid['id'],
 | 
				
			||||||
    { close_time, model, ...data }: ClientUpdateBidDto, // Nhận dữ liệu cập nhật
 | 
					    { close_time, model, metadata, ...data }: ClientUpdateBidDto, // Nhận dữ liệu cập nhật
 | 
				
			||||||
  ) {
 | 
					  ) {
 | 
				
			||||||
    // Tìm kiếm phiên đấu giá trong database theo id
 | 
					    // // Tìm kiếm phiên đấu giá trong database theo id
 | 
				
			||||||
    const bid = await this.bidsRepo.findOne({
 | 
					    // let bid = await this.bidsRepo.findOne({
 | 
				
			||||||
      where: { id },
 | 
					    //   where: { id },
 | 
				
			||||||
      relations: { histories: true, web_bid: true },
 | 
					    //   relations: { histories: true, web_bid: true, metadata: true },
 | 
				
			||||||
      order: {
 | 
					    //   order: {
 | 
				
			||||||
        histories: {
 | 
					    //     histories: {
 | 
				
			||||||
          price: 'DESC',
 | 
					    //       price: 'DESC',
 | 
				
			||||||
        },
 | 
					    //     },
 | 
				
			||||||
      },
 | 
					    //   },
 | 
				
			||||||
    });
 | 
					    // });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let bid = await this.getBidForClientUpdate(id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Nếu không tìm thấy phiên đấu giá, trả về lỗi 404
 | 
					    // Nếu không tìm thấy phiên đấu giá, trả về lỗi 404
 | 
				
			||||||
    if (!bid)
 | 
					    if (!bid)
 | 
				
			||||||
| 
						 | 
					@ -226,13 +375,41 @@ export class BidsService {
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Nếu phiên đấu giá chưa có thời gian bắt đầu và kết thúc
 | 
					    // Nếu phiên đấu giá chưa có thời gian bắt đầu và kết thúc
 | 
				
			||||||
    if (!bid.close_time && !bid.start_bid_time) {
 | 
					    if (!bid.close_time && !bid.start_bid_time && data?.lot_id) {
 | 
				
			||||||
 | 
					      // Tách lấy arrival_offset_seconds trong product nếu không có lấy trong web bid.
 | 
				
			||||||
 | 
					      const arrival_offset_seconds =
 | 
				
			||||||
 | 
					        this.bidMetadatasService.getArrivalOffsetSecondsByMode(bid.metadata) ||
 | 
				
			||||||
 | 
					        bid.web_bid.arrival_offset_seconds;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // Thiết lập thời gian bắt đầu là 5 phút trước khi đóng
 | 
					      // Thiết lập thời gian bắt đầu là 5 phút trước khi đóng
 | 
				
			||||||
      // bid.start_bid_time = new Date().toUTCString();
 | 
					      // bid.start_bid_time = new Date().toUTCString();
 | 
				
			||||||
      bid.start_bid_time = subtractMinutes(
 | 
					      bid.start_bid_time = subtractMinutes(
 | 
				
			||||||
        close_time,
 | 
					        close_time,
 | 
				
			||||||
        bid.web_bid.arrival_offset_seconds / 60,
 | 
					        arrival_offset_seconds / 60,
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // update
 | 
				
			||||||
 | 
					      await this.bidsRepo.update(
 | 
				
			||||||
 | 
					        { id },
 | 
				
			||||||
 | 
					        { start_bid_time: bid.start_bid_time },
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Cập nhật thời gian kết thúc đấu giá nếu `close_time` mới lớn hơn `close_time` cũ
 | 
				
			||||||
 | 
					    if (
 | 
				
			||||||
 | 
					      close_time &&
 | 
				
			||||||
 | 
					      new Date(close_time).getTime() > new Date(bid.close_time).getTime()
 | 
				
			||||||
 | 
					    ) {
 | 
				
			||||||
 | 
					      bid.close_time = close_time;
 | 
				
			||||||
 | 
					      bid.close_time_ts = new Date(close_time);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // update
 | 
				
			||||||
 | 
					      await this.bidsRepo.update(
 | 
				
			||||||
 | 
					        { id },
 | 
				
			||||||
 | 
					        { close_time: bid.close_time, close_time_ts: bid.close_time_ts },
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      bid = await this.getBidForClientUpdate(id);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Kiểm tra nếu thời gian đóng bid đã đạt tới (tức phiên đấu giá đã kết thúc)
 | 
					    // Kiểm tra nếu thời gian đóng bid đã đạt tới (tức phiên đấu giá đã kết thúc)
 | 
				
			||||||
| 
						 | 
					@ -251,7 +428,6 @@ export class BidsService {
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      // Nếu phiên đấu giá vẫn đang diễn ra và giá hiện tại vượt quá giới hạn đặt của người dùng
 | 
					      // Nếu phiên đấu giá vẫn đang diễn ra và giá hiện tại vượt quá giới hạn đặt của người dùng
 | 
				
			||||||
 | 
					 | 
				
			||||||
      if (
 | 
					      if (
 | 
				
			||||||
        data.current_price > bid.max_price + bid.plus_price ||
 | 
					        data.current_price > bid.max_price + bid.plus_price ||
 | 
				
			||||||
        (!bid.histories.length &&
 | 
					        (!bid.histories.length &&
 | 
				
			||||||
| 
						 | 
					@ -261,13 +437,14 @@ export class BidsService {
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Cập nhật thời gian kết thúc đấu giá nếu `close_time` mới lớn hơn `close_time` cũ
 | 
					    // // Cập nhật thời gian kết thúc đấu giá nếu `close_time` mới lớn hơn `close_time` cũ
 | 
				
			||||||
    if (
 | 
					    // if (
 | 
				
			||||||
      close_time &&
 | 
					    //   close_time &&
 | 
				
			||||||
      new Date(close_time).getTime() > new Date(bid.close_time).getTime()
 | 
					    //   new Date(close_time).getTime() > new Date(bid.close_time).getTime()
 | 
				
			||||||
    ) {
 | 
					    // ) {
 | 
				
			||||||
      bid.close_time = close_time;
 | 
					    //   bid.close_time = close_time;
 | 
				
			||||||
    }
 | 
					    //   bid.close_time_ts = new Date(close_time);
 | 
				
			||||||
 | 
					    // }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Nếu chưa có `model` nhưng dữ liệu mới có model, thì cập nhật model
 | 
					    // Nếu chưa có `model` nhưng dữ liệu mới có model, thì cập nhật model
 | 
				
			||||||
    if (model && !bid.model) {
 | 
					    if (model && !bid.model) {
 | 
				
			||||||
| 
						 | 
					@ -282,6 +459,10 @@ export class BidsService {
 | 
				
			||||||
      updated_at: new Date(), // Cập nhật timestamp
 | 
					      updated_at: new Date(), // Cập nhật timestamp
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (metadata) {
 | 
				
			||||||
 | 
					      await this.bidMetadatasService.upsert(metadata, bid);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Phát sự kiện cập nhật toàn bộ danh sách đấu giá
 | 
					    // Phát sự kiện cập nhật toàn bộ danh sách đấu giá
 | 
				
			||||||
    this.emitAllBidEvent();
 | 
					    this.emitAllBidEvent();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -291,6 +472,30 @@ export class BidsService {
 | 
				
			||||||
      this.notificationService.emitBidStatus(result);
 | 
					      this.notificationService.emitBidStatus(result);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Send INFO
 | 
				
			||||||
 | 
					    if (bid.current_price < result.current_price && bid.histories.length) {
 | 
				
			||||||
 | 
					      this.eventEmitter.emit(Event.SEND_INFO, {
 | 
				
			||||||
 | 
					        title: 'New Higher Bid Detected',
 | 
				
			||||||
 | 
					        messages: [
 | 
				
			||||||
 | 
					          `Another user just placed a higher bid of ${result.current_price} on <a href="${bid.url}">${bid.name}</a>.`,
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Send error when bidding fail
 | 
				
			||||||
 | 
					    if (
 | 
				
			||||||
 | 
					      isTimePassedByMinutes(result.start_bid_time, 1) &&
 | 
				
			||||||
 | 
					      !bid.histories.length &&
 | 
				
			||||||
 | 
					      !this.bidMetadatasService.isSandbox(bid.metadata)
 | 
				
			||||||
 | 
					    ) {
 | 
				
			||||||
 | 
					      this.eventEmitter.emit(Event.SEND_ERROR, {
 | 
				
			||||||
 | 
					        title: 'Bidding Error Detected',
 | 
				
			||||||
 | 
					        messages: [
 | 
				
			||||||
 | 
					          `An error occurred while placing a bid on <a href="${bid.url}">${bid.name}</a>. Please check the system.`,
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Trả về kết quả cập nhật dưới dạng response chuẩn
 | 
					    // Trả về kết quả cập nhật dưới dạng response chuẩn
 | 
				
			||||||
    return AppResponse.toResponse(plainToClass(Bid, result));
 | 
					    return AppResponse.toResponse(plainToClass(Bid, result));
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					@ -298,7 +503,10 @@ export class BidsService {
 | 
				
			||||||
  async outBid(id: Bid['id']) {
 | 
					  async outBid(id: Bid['id']) {
 | 
				
			||||||
    const result = await this.bidsRepo.update(id, { status: 'out-bid' });
 | 
					    const result = await this.bidsRepo.update(id, { status: 'out-bid' });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const bid = await this.bidsRepo.findOne({ where: { id } });
 | 
					    const bid = await this.bidsRepo.findOne({
 | 
				
			||||||
 | 
					      where: { id },
 | 
				
			||||||
 | 
					      relations: { web_bid: true },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!result) throw new BadRequestException(AppResponse.toResponse(false));
 | 
					    if (!result) throw new BadRequestException(AppResponse.toResponse(false));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -350,7 +558,10 @@ export class BidsService {
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async updateStatusByPrice(id: Bid['id'], data: UpdateStatusByPriceDto) {
 | 
					  async updateStatusByPrice(id: Bid['id'], data: UpdateStatusByPriceDto) {
 | 
				
			||||||
    const bid = await this.bidsRepo.findOne({ where: { id } });
 | 
					    const bid = await this.bidsRepo.findOne({
 | 
				
			||||||
 | 
					      where: { id },
 | 
				
			||||||
 | 
					      relations: { web_bid: true },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!bid)
 | 
					    if (!bid)
 | 
				
			||||||
      throw new NotFoundException(
 | 
					      throw new NotFoundException(
 | 
				
			||||||
| 
						 | 
					@ -441,6 +652,34 @@ export class BidsService {
 | 
				
			||||||
    return AppResponse.toResponse(true);
 | 
					    return AppResponse.toResponse(true);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async uploadRecord(id: Bid['id'], video: Express.Multer.File) {
 | 
				
			||||||
 | 
					    if (!video) {
 | 
				
			||||||
 | 
					      throw new BadRequestException(
 | 
				
			||||||
 | 
					        AppResponse.toResponse(null, { message: 'File or Url is required' }),
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const data: { filename: string } | undefined =
 | 
				
			||||||
 | 
					      await new ImageCompressionPipe(
 | 
				
			||||||
 | 
					        `${Constant.RECORD_FOLDER}`,
 | 
				
			||||||
 | 
					        {},
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          unique_image_folder: false,
 | 
				
			||||||
 | 
					          unique_name: false,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      ).transform(video);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!data)
 | 
				
			||||||
 | 
					      throw new BadRequestException(
 | 
				
			||||||
 | 
					        AppResponse.toResponse(null, {
 | 
				
			||||||
 | 
					          message: "Can't create media",
 | 
				
			||||||
 | 
					          status_code: HttpStatus.BAD_REQUEST,
 | 
				
			||||||
 | 
					        }),
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return AppResponse.toResponse(true);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async getStatusWorkingImage(
 | 
					  async getStatusWorkingImage(
 | 
				
			||||||
    id: Bid['id'],
 | 
					    id: Bid['id'],
 | 
				
			||||||
    type: string,
 | 
					    type: string,
 | 
				
			||||||
| 
						 | 
					@ -497,6 +736,84 @@ export class BidsService {
 | 
				
			||||||
    return res.sendFile(filePath);
 | 
					    return res.sendFile(filePath);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async getRecord(name: string, res: Response) {
 | 
				
			||||||
 | 
					    const rootDir = process.cwd();
 | 
				
			||||||
 | 
					    const filePath = join(
 | 
				
			||||||
 | 
					      rootDir,
 | 
				
			||||||
 | 
					      `${Constant.MEDIA_PATH}/${Constant.RECORD_FOLDER}/${name}`,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!existsSync(filePath)) {
 | 
				
			||||||
 | 
					      throw new NotFoundException(
 | 
				
			||||||
 | 
					        AppResponse.toResponse(null, {
 | 
				
			||||||
 | 
					          message: 'Folder not found',
 | 
				
			||||||
 | 
					          status_code: HttpStatus.NOT_FOUND,
 | 
				
			||||||
 | 
					        }),
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const stat = fs.statSync(filePath);
 | 
				
			||||||
 | 
					    const fileSize = stat.size;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    res.writeHead(200, {
 | 
				
			||||||
 | 
					      'Content-Type': 'video/mp4',
 | 
				
			||||||
 | 
					      'Content-Length': fileSize,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const readStream = fs.createReadStream(filePath);
 | 
				
			||||||
 | 
					    readStream.pipe(res);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async getRecords(id: Bid['id']) {
 | 
				
			||||||
 | 
					    const rootDir = process.cwd();
 | 
				
			||||||
 | 
					    const folderPath = join(
 | 
				
			||||||
 | 
					      rootDir,
 | 
				
			||||||
 | 
					      `${Constant.MEDIA_PATH}/${Constant.RECORD_FOLDER}`,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!existsSync(folderPath)) {
 | 
				
			||||||
 | 
					      throw new NotFoundException(
 | 
				
			||||||
 | 
					        AppResponse.toResponse(null, {
 | 
				
			||||||
 | 
					          message: 'Folder not found',
 | 
				
			||||||
 | 
					          status_code: HttpStatus.NOT_FOUND,
 | 
				
			||||||
 | 
					        }),
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const files = await fs.promises.readdir(folderPath);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const data = files
 | 
				
			||||||
 | 
					      .map((item) => {
 | 
				
			||||||
 | 
					        return parseVideoFileName(item);
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					      .filter((i) => i.bid_id == id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const sorted = _.orderBy(data, ['timestamp'], ['desc']);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return AppResponse.toResponse(sorted);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async deleteRecord(name: string) {
 | 
				
			||||||
 | 
					    const rootDir = process.cwd();
 | 
				
			||||||
 | 
					    const filePath = join(
 | 
				
			||||||
 | 
					      rootDir,
 | 
				
			||||||
 | 
					      `${Constant.MEDIA_PATH}/${Constant.RECORD_FOLDER}/${name}`,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!existsSync(filePath)) {
 | 
				
			||||||
 | 
					      throw new NotFoundException(
 | 
				
			||||||
 | 
					        AppResponse.toResponse(null, {
 | 
				
			||||||
 | 
					          message: 'File not found',
 | 
				
			||||||
 | 
					          status_code: HttpStatus.NOT_FOUND,
 | 
				
			||||||
 | 
					        }),
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await fs.promises.unlink(filePath);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return AppResponse.toResponse(true);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async getImagesWorking(id: Bid['id'], type: string) {
 | 
					  async getImagesWorking(id: Bid['id'], type: string) {
 | 
				
			||||||
    const rootDir = process.cwd();
 | 
					    const rootDir = process.cwd();
 | 
				
			||||||
    const folderPath = join(
 | 
					    const folderPath = join(
 | 
				
			||||||
| 
						 | 
					@ -524,4 +841,101 @@ export class BidsService {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return AppResponse.toResponse(true);
 | 
					    return AppResponse.toResponse(true);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async getNextBid(): Promise<Bid | null> {
 | 
				
			||||||
 | 
					    const all = await this.bidsRepo.find({
 | 
				
			||||||
 | 
					      where: { status: 'biding', close_time: Not(IsNull()) },
 | 
				
			||||||
 | 
					      relations: { web_bid: true },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const now = Date.now();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let nextBid = null;
 | 
				
			||||||
 | 
					    let minDiff = Infinity;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for (const bid of all) {
 | 
				
			||||||
 | 
					      const time = Date.parse(bid.close_time);
 | 
				
			||||||
 | 
					      if (!isNaN(time) && time >= now) {
 | 
				
			||||||
 | 
					        const diff = time - now;
 | 
				
			||||||
 | 
					        if (diff < minDiff) {
 | 
				
			||||||
 | 
					          minDiff = diff;
 | 
				
			||||||
 | 
					          nextBid = bid;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return nextBid;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async getBidByModel(model: string) {
 | 
				
			||||||
 | 
					    const bid = await this.bidsRepo.findOne({
 | 
				
			||||||
 | 
					      where: { model },
 | 
				
			||||||
 | 
					      relations: { metadata: true, web_bid: true },
 | 
				
			||||||
 | 
					      select: {
 | 
				
			||||||
 | 
					        web_bid: { arrival_offset_seconds: true, early_tracking_seconds: true },
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!bid)
 | 
				
			||||||
 | 
					      return AppResponse.toResponse(null, {
 | 
				
			||||||
 | 
					        status_code: HttpStatus.NOT_FOUND,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return AppResponse.toResponse(plainToClass(Bid, bid));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async hookAction(
 | 
				
			||||||
 | 
					    { id, type }: { id: Bid['id']; type: 'action' | 'api' },
 | 
				
			||||||
 | 
					    data: any,
 | 
				
			||||||
 | 
					  ) {
 | 
				
			||||||
 | 
					    const bid = await this.bidsRepo.findOne({
 | 
				
			||||||
 | 
					      where: { id },
 | 
				
			||||||
 | 
					      relations: { metadata: true },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!bid)
 | 
				
			||||||
 | 
					      throw new NotFoundException(
 | 
				
			||||||
 | 
					        AppResponse.toResponse(null, { message: 'Not foud bid' }),
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (type === 'api') {
 | 
				
			||||||
 | 
					      const result = JSON.parse(data?.data || {});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      result['timestamp'] = new Date().getTime();
 | 
				
			||||||
 | 
					      result['uuid'] = uuid();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const prevDemoResponse = this.bidMetadatasService.getDemoResponse(
 | 
				
			||||||
 | 
					        bid.metadata,
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      console.log({ prevDemoResponse, result });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await this.bidMetadatasService.upsert(
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          [BidMetadata.DEMO_RESPONSE]:
 | 
				
			||||||
 | 
					            JSON.stringify([result, ...(prevDemoResponse || [])]) ||
 | 
				
			||||||
 | 
					            JSON.stringify([]),
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        bid,
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      const recordUrl = data?.record_url || '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await this.bidMetadatasService.upsert(
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          [BidMetadata.LATEST_RECROD_LINK]: JSON.stringify(recordUrl),
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        bid,
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const latestBidData = await this.bidsRepo.findOne({
 | 
				
			||||||
 | 
					      where: { id },
 | 
				
			||||||
 | 
					      relations: { metadata: true },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.eventEmitter.emit(Event.BID_DEMO, { bid: latestBidData, type });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return AppResponse.toResponse(data);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,75 @@
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  BadRequestException,
 | 
				
			||||||
 | 
					  HttpStatus,
 | 
				
			||||||
 | 
					  Injectable,
 | 
				
			||||||
 | 
					  NotFoundException,
 | 
				
			||||||
 | 
					} from '@nestjs/common';
 | 
				
			||||||
 | 
					import { EventEmitter2 } from '@nestjs/event-emitter';
 | 
				
			||||||
 | 
					import { InjectRepository } from '@nestjs/typeorm';
 | 
				
			||||||
 | 
					import { Repository } from 'typeorm';
 | 
				
			||||||
 | 
					import { Config } from '../entities/configs.entity';
 | 
				
			||||||
 | 
					import AppResponse from '@/response/app-response';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Injectable()
 | 
				
			||||||
 | 
					export class ConfigsService {
 | 
				
			||||||
 | 
					  public static CONFIG_KEYS = {
 | 
				
			||||||
 | 
					    REFRESH_TOOL_TIME: 'REFRESH_TOOL_TIME',
 | 
				
			||||||
 | 
					    MAIL_SCRAP_REPORT: 'MAIL_SCRAP_REPORT',
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  constructor(
 | 
				
			||||||
 | 
					    @InjectRepository(Config)
 | 
				
			||||||
 | 
					    readonly configRepo: Repository<Config>,
 | 
				
			||||||
 | 
					  ) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async getConfig(key_name: keyof typeof ConfigsService.CONFIG_KEYS) {
 | 
				
			||||||
 | 
					    return (await this.configRepo.findOne({ where: { key_name } })) || null;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async setConfig(
 | 
				
			||||||
 | 
					    key_name: keyof typeof ConfigsService.CONFIG_KEYS,
 | 
				
			||||||
 | 
					    value: string,
 | 
				
			||||||
 | 
					    type: 'string' | 'number',
 | 
				
			||||||
 | 
					  ) {
 | 
				
			||||||
 | 
					    return await this.configRepo.upsert({ key_name, value, type }, [
 | 
				
			||||||
 | 
					      'key_name',
 | 
				
			||||||
 | 
					    ]);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async getConfigRes(key_name: string) {
 | 
				
			||||||
 | 
					    const result = await this.getConfig(
 | 
				
			||||||
 | 
					      key_name as keyof typeof ConfigsService.CONFIG_KEYS,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!result)
 | 
				
			||||||
 | 
					      throw new NotFoundException(
 | 
				
			||||||
 | 
					        AppResponse.toResponse(null, {
 | 
				
			||||||
 | 
					          message: 'Config key name not found',
 | 
				
			||||||
 | 
					          status_code: HttpStatus.NOT_FOUND,
 | 
				
			||||||
 | 
					        }),
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return AppResponse.toResponse(result);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async upsertConfig(data: Partial<Config>) {
 | 
				
			||||||
 | 
					    let response = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const prevConfig = await this.configRepo.findOne({
 | 
				
			||||||
 | 
					      where: { key_name: data.key_name },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!prevConfig) {
 | 
				
			||||||
 | 
					      response = await this.configRepo.save(data);
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      response = await this.configRepo.update(
 | 
				
			||||||
 | 
					        { key_name: data.key_name },
 | 
				
			||||||
 | 
					        data,
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!response) throw new BadRequestException(AppResponse.toResponse(false));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return AppResponse.toResponse(!!response);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -98,7 +98,8 @@ export class ImapService {
 | 
				
			||||||
            const result = verifyCode(emailContent);
 | 
					            const result = verifyCode(emailContent);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (!result) {
 | 
					            if (!result) {
 | 
				
			||||||
              throw new Error('fetchLatestEmail: Name or Code is empty');
 | 
					              console.log('fetchLatestEmail: Name or Code is empty');
 | 
				
			||||||
 | 
					              return;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            const { code, name } = result;
 | 
					            const { code, name } = result;
 | 
				
			||||||
| 
						 | 
					@ -108,7 +109,8 @@ export class ImapService {
 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (!webBid) {
 | 
					            if (!webBid) {
 | 
				
			||||||
              throw new Error('Not found web bid');
 | 
					              console.log('Not found web bid');
 | 
				
			||||||
 | 
					              return;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            // send message event
 | 
					            // send message event
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,98 @@
 | 
				
			||||||
 | 
					import { Injectable, Logger } from '@nestjs/common';
 | 
				
			||||||
 | 
					import { Cron, CronExpression } from '@nestjs/schedule';
 | 
				
			||||||
 | 
					import { IsNull, Not } from 'typeorm';
 | 
				
			||||||
 | 
					import { BidsService } from './bids.service';
 | 
				
			||||||
 | 
					import * as moment from 'moment';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  isTimeReached,
 | 
				
			||||||
 | 
					  shouldResetTool,
 | 
				
			||||||
 | 
					  subtractMinutes,
 | 
				
			||||||
 | 
					  subtractSeconds,
 | 
				
			||||||
 | 
					} from '@/ultils';
 | 
				
			||||||
 | 
					import { ConfigsService } from './configs.service';
 | 
				
			||||||
 | 
					import { DashboardService } from './dashboard.service';
 | 
				
			||||||
 | 
					import { Bid } from '../entities/bid.entity';
 | 
				
			||||||
 | 
					@Injectable()
 | 
				
			||||||
 | 
					export class TasksService {
 | 
				
			||||||
 | 
					  private readonly logger = new Logger(TasksService.name);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  constructor(
 | 
				
			||||||
 | 
					    private readonly bidsService: BidsService,
 | 
				
			||||||
 | 
					    private readonly configsService: ConfigsService,
 | 
				
			||||||
 | 
					    private readonly dashboadService: DashboardService,
 | 
				
			||||||
 | 
					  ) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // - Chạy kiểm tra và lấy thời gian close của product gần với hiện tại nhất
 | 
				
			||||||
 | 
					  // - Nếu thỏa điều kiện sẽ reset tool trước thời gian close 20 phút
 | 
				
			||||||
 | 
					  @Cron(CronExpression.EVERY_MINUTE)
 | 
				
			||||||
 | 
					  async handleResetTool() {
 | 
				
			||||||
 | 
					    const bids = await this.bidsService.bidsRepo.find({
 | 
				
			||||||
 | 
					      where: { status: 'biding' },
 | 
				
			||||||
 | 
					      select: ['close_time', 'created_at', 'start_bid_time', 'id', 'lot_id'],
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const lastResetTimeRaw =
 | 
				
			||||||
 | 
					      await this.configsService.getConfig('REFRESH_TOOL_TIME');
 | 
				
			||||||
 | 
					    const lastResetTime = lastResetTimeRaw?.value
 | 
				
			||||||
 | 
					      ? new Date(lastResetTimeRaw.value)
 | 
				
			||||||
 | 
					      : null;
 | 
				
			||||||
 | 
					    const now = new Date();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const minutesReset = 20;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    console.log('===== Reset Tool Check =====');
 | 
				
			||||||
 | 
					    console.log(
 | 
				
			||||||
 | 
					      'Last reset time:',
 | 
				
			||||||
 | 
					      lastResetTime
 | 
				
			||||||
 | 
					        ? moment(lastResetTime).format('YYYY-MM-DD HH:mm:ss')
 | 
				
			||||||
 | 
					        : 'null',
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    console.log('Current time:', moment(now).format('YYYY-MM-DD HH:mm:ss'));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const result = shouldResetTool(bids, lastResetTime, now);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (result.shouldReset) {
 | 
				
			||||||
 | 
					      console.log('Should reset due to:', result.reason);
 | 
				
			||||||
 | 
					      if (result.bidId) {
 | 
				
			||||||
 | 
					        console.log('Related bid id:', result.bidId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (result.closeTime) {
 | 
				
			||||||
 | 
					          const closeTimeDate = new Date(result.closeTime);
 | 
				
			||||||
 | 
					          const expectedReset = moment(closeTimeDate)
 | 
				
			||||||
 | 
					            .subtract(minutesReset, 'minutes')
 | 
				
			||||||
 | 
					            .format('YYYY-MM-DD HH:mm:ss');
 | 
				
			||||||
 | 
					          console.log(
 | 
				
			||||||
 | 
					            `Expected reset time (close_time - ${minutesReset} minutes):`,
 | 
				
			||||||
 | 
					            expectedReset,
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await this.dashboadService.resetTool();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await this.configsService.setConfig(
 | 
				
			||||||
 | 
					        'REFRESH_TOOL_TIME',
 | 
				
			||||||
 | 
					        now.toISOString(),
 | 
				
			||||||
 | 
					        'string',
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      console.log('Tool reset at:', moment(now).format('YYYY-MM-DD HH:mm:ss'));
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      console.log('No reset needed at this time');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Reset lại server.
 | 
				
			||||||
 | 
					  // Nếu server quá lâu không được reset sẽ đẫn đến mất kết nối với imap (không nhận được mail)
 | 
				
			||||||
 | 
					  //Chạy vào 12 giờ đêm mỗi ngày
 | 
				
			||||||
 | 
					  @Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT)
 | 
				
			||||||
 | 
					  async handleResetServer() {
 | 
				
			||||||
 | 
					    const processName = 'auto-bid-server';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    console.log('===== Reset Server Check =====');
 | 
				
			||||||
 | 
					    await this.dashboadService.resetProcessByName(processName);
 | 
				
			||||||
 | 
					    console.log(
 | 
				
			||||||
 | 
					      `Reset server at: ${moment(new Date()).format('YYYY-MM-DD HH:mm:ss')}`,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -53,6 +53,9 @@ export class WebBidsService {
 | 
				
			||||||
      filterableColumns,
 | 
					      filterableColumns,
 | 
				
			||||||
      defaultSortBy: [['id', 'DESC']],
 | 
					      defaultSortBy: [['id', 'DESC']],
 | 
				
			||||||
      maxLimit: 100,
 | 
					      maxLimit: 100,
 | 
				
			||||||
 | 
					      relations: {
 | 
				
			||||||
 | 
					        scrap_config: true,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return AppResponse.toPagination<WebBid>(data, true, WebBid);
 | 
					    return AppResponse.toPagination<WebBid>(data, true, WebBid);
 | 
				
			||||||
| 
						 | 
					@ -65,7 +68,14 @@ export class WebBidsService {
 | 
				
			||||||
        url: Not(IsNull()),
 | 
					        url: Not(IsNull()),
 | 
				
			||||||
        children: { status: 'biding' },
 | 
					        children: { status: 'biding' },
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      relations: { children: { histories: true, web_bid: true } },
 | 
					      relations: {
 | 
				
			||||||
 | 
					        children: { histories: true, web_bid: true, metadata: true },
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      order: {
 | 
				
			||||||
 | 
					        children: {
 | 
				
			||||||
 | 
					          close_time_ts: 'ASC',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return data;
 | 
					    return data;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,6 +3,7 @@ export class Constant {
 | 
				
			||||||
  public static BOT_TELEGRAM_PATH = 'bot-data';
 | 
					  public static BOT_TELEGRAM_PATH = 'bot-data';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public static WORK_IMAGES_FOLDER = 'work-images';
 | 
					  public static WORK_IMAGES_FOLDER = 'work-images';
 | 
				
			||||||
 | 
					  public static RECORD_FOLDER = 'records';
 | 
				
			||||||
  public static TMP_FOLDER = 'tmp';
 | 
					  public static TMP_FOLDER = 'tmp';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public static DEFAULT_RESIZE = 400;
 | 
					  public static DEFAULT_RESIZE = 400;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -7,6 +7,13 @@ export class Event {
 | 
				
			||||||
  public static ADMIN_BIDS_UPDATED = 'adminBidsUpdated';
 | 
					  public static ADMIN_BIDS_UPDATED = 'adminBidsUpdated';
 | 
				
			||||||
  public static WEB_UPDATED = 'webUpdated';
 | 
					  public static WEB_UPDATED = 'webUpdated';
 | 
				
			||||||
  public static LOGIN_STATUS = 'login-status';
 | 
					  public static LOGIN_STATUS = 'login-status';
 | 
				
			||||||
 | 
					  public static BID_SUBMITED = 'bid-submited';
 | 
				
			||||||
 | 
					  public static BID_STATUS = 'bid-status';
 | 
				
			||||||
 | 
					  public static BID_DEMO = 'bid-demo';
 | 
				
			||||||
 | 
					  public static SEND_WARNING = 'send-warning';
 | 
				
			||||||
 | 
					  public static SEND_SUCCESS = 'send-success';
 | 
				
			||||||
 | 
					  public static SEND_ERROR = 'send-error';
 | 
				
			||||||
 | 
					  public static SEND_INFO = 'send-info';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public static verifyCode(data: WebBid) {
 | 
					  public static verifyCode(data: WebBid) {
 | 
				
			||||||
    return `${this.VERIFY_CODE}.${data.origin_url}`;
 | 
					    return `${this.VERIFY_CODE}.${data.origin_url}`;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,22 @@
 | 
				
			||||||
 | 
					import { Constant } from '@/modules/bids/utils/constant';
 | 
				
			||||||
 | 
					import { Controller, Get, Res } from '@nestjs/common';
 | 
				
			||||||
 | 
					import { Response } from 'express';
 | 
				
			||||||
 | 
					import { join } from 'path';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Controller('extensions')
 | 
				
			||||||
 | 
					export class ExtensionsController {
 | 
				
			||||||
 | 
					  @Get('policy')
 | 
				
			||||||
 | 
					  async policy(@Res() res: Response) {
 | 
				
			||||||
 | 
					    const rootDir = process.cwd();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const filePath = join(
 | 
				
			||||||
 | 
					      rootDir,
 | 
				
			||||||
 | 
					      'src',
 | 
				
			||||||
 | 
					      'modules',
 | 
				
			||||||
 | 
					      'extensions',
 | 
				
			||||||
 | 
					      `/views/policy.html`,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return res.sendFile(filePath);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,7 @@
 | 
				
			||||||
 | 
					import { Module } from '@nestjs/common';
 | 
				
			||||||
 | 
					import { ExtensionsController } from './controllers/extensions.controller';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Module({
 | 
				
			||||||
 | 
					  controllers: [ExtensionsController],
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					export class ExtensionsModule {}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,120 @@
 | 
				
			||||||
 | 
					<!DOCTYPE html>
 | 
				
			||||||
 | 
					<html lang="en">
 | 
				
			||||||
 | 
					<head>
 | 
				
			||||||
 | 
					  <meta charset="UTF-8">
 | 
				
			||||||
 | 
					  <title>Privacy Policy - Bid Extension</title>
 | 
				
			||||||
 | 
					  <style>
 | 
				
			||||||
 | 
					    :root {
 | 
				
			||||||
 | 
					      --primary-color: #3498db;
 | 
				
			||||||
 | 
					      --secondary-color: #2c3e50;
 | 
				
			||||||
 | 
					      --text-color: #333;
 | 
				
			||||||
 | 
					      --light-bg: #f8f9fa;
 | 
				
			||||||
 | 
					      --border-color: #e0e0e0;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    body {
 | 
				
			||||||
 | 
					      font-family: 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
 | 
				
			||||||
 | 
					      line-height: 1.6;
 | 
				
			||||||
 | 
					      color: var(--text-color);
 | 
				
			||||||
 | 
					      max-width: 800px;
 | 
				
			||||||
 | 
					      margin: 0 auto;
 | 
				
			||||||
 | 
					      padding: 20px;
 | 
				
			||||||
 | 
					      background-color: #fff;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    .container {
 | 
				
			||||||
 | 
					      background-color: #fff;
 | 
				
			||||||
 | 
					      border-radius: 8px;
 | 
				
			||||||
 | 
					      box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
 | 
				
			||||||
 | 
					      padding: 30px;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    h1 {
 | 
				
			||||||
 | 
					      color: var(--primary-color);
 | 
				
			||||||
 | 
					      text-align: center;
 | 
				
			||||||
 | 
					      border-bottom: 2px solid var(--border-color);
 | 
				
			||||||
 | 
					      padding-bottom: 15px;
 | 
				
			||||||
 | 
					      margin-bottom: 25px;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    h2 {
 | 
				
			||||||
 | 
					      color: var(--secondary-color);
 | 
				
			||||||
 | 
					      border-left: 4px solid var(--primary-color);
 | 
				
			||||||
 | 
					      padding-left: 10px;
 | 
				
			||||||
 | 
					      margin-top: 30px;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    p {
 | 
				
			||||||
 | 
					      margin-bottom: 15px;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    strong {
 | 
				
			||||||
 | 
					      color: var(--secondary-color);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    code {
 | 
				
			||||||
 | 
					      background-color: var(--light-bg);
 | 
				
			||||||
 | 
					      padding: 2px 5px;
 | 
				
			||||||
 | 
					      border-radius: 3px;
 | 
				
			||||||
 | 
					      font-family: 'Courier New', Courier, monospace;
 | 
				
			||||||
 | 
					      font-size: 0.9em;
 | 
				
			||||||
 | 
					      color: #e74c3c;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    a {
 | 
				
			||||||
 | 
					      color: var(--primary-color);
 | 
				
			||||||
 | 
					      text-decoration: none;
 | 
				
			||||||
 | 
					      transition: color 0.3s;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    a:hover {
 | 
				
			||||||
 | 
					      color: #2980b9;
 | 
				
			||||||
 | 
					      text-decoration: underline;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    .effective-date {
 | 
				
			||||||
 | 
					      background-color: var(--light-bg);
 | 
				
			||||||
 | 
					      padding: 10px 15px;
 | 
				
			||||||
 | 
					      border-radius: 5px;
 | 
				
			||||||
 | 
					      display: inline-block;
 | 
				
			||||||
 | 
					      margin-bottom: 20px;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    .contact-section {
 | 
				
			||||||
 | 
					      background-color: var(--light-bg);
 | 
				
			||||||
 | 
					      padding: 15px;
 | 
				
			||||||
 | 
					      border-radius: 5px;
 | 
				
			||||||
 | 
					      margin-top: 30px;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  </style>
 | 
				
			||||||
 | 
					</head>
 | 
				
			||||||
 | 
					<body>
 | 
				
			||||||
 | 
					  <div class="container">
 | 
				
			||||||
 | 
					    <h1>Privacy Policy</h1>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <div class="effective-date">
 | 
				
			||||||
 | 
					      <strong>Effective Date:</strong> 2025-05-20
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <p>This privacy policy explains how the Chrome Extension <strong>Bid Extension</strong> handles user data.</p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <h2>1. Information Collection</h2>
 | 
				
			||||||
 | 
					    <p>The Bid Extension does <strong>not</strong> collect or transmit any personal or sensitive user data.</p>
 | 
				
			||||||
 | 
					    <p>The extension uses the <code>storage</code> permission to save settings and preferences <strong>locally</strong> in your browser.</p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <h2>2. Local Storage Only</h2>
 | 
				
			||||||
 | 
					    <p>All data used by this extension is stored using Chrome's local storage (via the <code>chrome.storage</code> API). This data never leaves your device and is not transmitted to any external server.</p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <h2>3. No Third-party Sharing</h2>
 | 
				
			||||||
 | 
					    <p>We do not share any user data with third parties. We do not use analytics, ads, or any tracking technologies.</p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <h2>4. Changes to this Policy</h2>
 | 
				
			||||||
 | 
					    <p>If we update this privacy policy in the future, we will notify users by updating this document with the new effective date.</p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <h2>5. Contact</h2>
 | 
				
			||||||
 | 
					    <div class="contact-section">
 | 
				
			||||||
 | 
					      <p>If you have any questions about this privacy policy, please contact us at: <a href="mailto:bid@apactech.io">bid@apactech.io</a></p>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</body>
 | 
				
			||||||
 | 
					</html>
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,34 @@
 | 
				
			||||||
 | 
					import { MailerModule } from '@nestjs-modules/mailer';
 | 
				
			||||||
 | 
					import { forwardRef, Module } from '@nestjs/common';
 | 
				
			||||||
 | 
					import { MailsService } from './services/mails.service';
 | 
				
			||||||
 | 
					import { ConfigModule, ConfigService } from '@nestjs/config';
 | 
				
			||||||
 | 
					import { MailProcessor } from './process/mail.processor';
 | 
				
			||||||
 | 
					import { BidsModule } from '../bids/bids.module';
 | 
				
			||||||
 | 
					@Module({
 | 
				
			||||||
 | 
					  imports: [
 | 
				
			||||||
 | 
					    forwardRef(() => BidsModule),
 | 
				
			||||||
 | 
					    MailerModule.forRootAsync({
 | 
				
			||||||
 | 
					      imports: [ConfigModule],
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      useFactory: async (config: ConfigService) => ({
 | 
				
			||||||
 | 
					        transport: {
 | 
				
			||||||
 | 
					          host: config.get<string>('MAIL_SERVER'),
 | 
				
			||||||
 | 
					          port: 465,
 | 
				
			||||||
 | 
					          secure: true, // true nếu port là 465
 | 
				
			||||||
 | 
					          auth: {
 | 
				
			||||||
 | 
					            user: config.get<string>('MAIL_USER'),
 | 
				
			||||||
 | 
					            pass: config.get<string>('MAIL_PASSWORD'),
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        defaults: {
 | 
				
			||||||
 | 
					          from: `"Bids" <${config.get<string>('MAIL_USER')}>`,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      }),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      inject: [ConfigService],
 | 
				
			||||||
 | 
					    }),
 | 
				
			||||||
 | 
					  ],
 | 
				
			||||||
 | 
					  providers: [MailsService, MailProcessor],
 | 
				
			||||||
 | 
					  exports: [MailsService],
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					export class MailsModule {}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,16 @@
 | 
				
			||||||
 | 
					// processors/mail.processor.ts
 | 
				
			||||||
 | 
					import { Process, Processor } from '@nestjs/bull';
 | 
				
			||||||
 | 
					import { Job } from 'bull';
 | 
				
			||||||
 | 
					import { MailsService } from '../services/mails.service';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Processor('mail-queue')
 | 
				
			||||||
 | 
					export class MailProcessor {
 | 
				
			||||||
 | 
					  constructor(private readonly mailsService: MailsService) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Process('send-mail')
 | 
				
			||||||
 | 
					  async handleSendMail(job: Job) {
 | 
				
			||||||
 | 
					    const { to, subject, html } = job.data;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await this.mailsService.sendPlainHtml(to, subject, html);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,535 @@
 | 
				
			||||||
 | 
					import { Bid } from '@/modules/bids/entities/bid.entity';
 | 
				
			||||||
 | 
					import { BidMetadatasService } from '@/modules/bids/services/bid-metadatas.service';
 | 
				
			||||||
 | 
					import { BidsService } from '@/modules/bids/services/bids.service';
 | 
				
			||||||
 | 
					import { ScrapItem } from '@/modules/scraps/entities/scrap-item.entity';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  extractDomain,
 | 
				
			||||||
 | 
					  extractDomainSmart,
 | 
				
			||||||
 | 
					  formatEndTime,
 | 
				
			||||||
 | 
					  isHotItemFn,
 | 
				
			||||||
 | 
					  isTimeReached,
 | 
				
			||||||
 | 
					} from '@/ultils';
 | 
				
			||||||
 | 
					import { MailerService } from '@nestjs-modules/mailer';
 | 
				
			||||||
 | 
					import { InjectQueue } from '@nestjs/bull';
 | 
				
			||||||
 | 
					import { Injectable } from '@nestjs/common';
 | 
				
			||||||
 | 
					import { Queue } from 'bull';
 | 
				
			||||||
 | 
					import * as _ from 'lodash';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Injectable()
 | 
				
			||||||
 | 
					export class MailsService {
 | 
				
			||||||
 | 
					  constructor(
 | 
				
			||||||
 | 
					    private readonly mailerService: MailerService,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @InjectQueue('mail-queue') private mailQueue: Queue,
 | 
				
			||||||
 | 
					    private readonly bidsService: BidsService,
 | 
				
			||||||
 | 
					    private readonly bidMetadataService: BidMetadatasService,
 | 
				
			||||||
 | 
					  ) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async sendPlainText(to: string, subject: string, content: string) {
 | 
				
			||||||
 | 
					    await this.mailerService.sendMail({
 | 
				
			||||||
 | 
					      to,
 | 
				
			||||||
 | 
					      subject,
 | 
				
			||||||
 | 
					      text: content,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async sendHtmlMailJob(mailData: {
 | 
				
			||||||
 | 
					    to: string;
 | 
				
			||||||
 | 
					    subject: string;
 | 
				
			||||||
 | 
					    html: string;
 | 
				
			||||||
 | 
					  }) {
 | 
				
			||||||
 | 
					    await this.mailQueue.add('send-mail', mailData);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async sendPlainHtml(to: string, subject: string, html: string) {
 | 
				
			||||||
 | 
					    const emails = to
 | 
				
			||||||
 | 
					      .split(',')
 | 
				
			||||||
 | 
					      .map((e) => e.trim())
 | 
				
			||||||
 | 
					      .filter(Boolean);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const mainRecipient = emails[0];
 | 
				
			||||||
 | 
					    const ccRecipients = emails.slice(1); // Những email còn lại
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await this.mailerService.sendMail({
 | 
				
			||||||
 | 
					      to: mainRecipient,
 | 
				
			||||||
 | 
					      cc: ccRecipients.length > 0 ? ccRecipients : undefined,
 | 
				
			||||||
 | 
					      subject,
 | 
				
			||||||
 | 
					      html,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  generateProductTableHTML(
 | 
				
			||||||
 | 
					    products: ScrapItem[],
 | 
				
			||||||
 | 
					    hotItems: { name: string }[],
 | 
				
			||||||
 | 
					  ): {
 | 
				
			||||||
 | 
					    html: string;
 | 
				
			||||||
 | 
					    hasHotItem: boolean;
 | 
				
			||||||
 | 
					  } {
 | 
				
			||||||
 | 
					    const from = process.env.MAIL_USER || 'no-reply@example.com';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let hasHotItem = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!products.length) {
 | 
				
			||||||
 | 
					      return {
 | 
				
			||||||
 | 
					        html: `
 | 
				
			||||||
 | 
					    <!DOCTYPE html>
 | 
				
			||||||
 | 
					    <html lang="en">
 | 
				
			||||||
 | 
					    <head>
 | 
				
			||||||
 | 
					      <meta charset="UTF-8" />
 | 
				
			||||||
 | 
					      <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
 | 
				
			||||||
 | 
					      <title>Product Listing</title>
 | 
				
			||||||
 | 
					    </head>
 | 
				
			||||||
 | 
					    <body style="font-family: sans-serif; background: #f8f9fa; padding: 20px;">
 | 
				
			||||||
 | 
					      <h2 style="text-align: center; color: #333;">Product Listing</h2>
 | 
				
			||||||
 | 
					      <p style="text-align: center; color: #666;">No matching products found for your keywords today.</p>
 | 
				
			||||||
 | 
					      <p style="text-align: center; color: #999; font-size: 12px; margin-top: 40px;">From: ${from}</p>
 | 
				
			||||||
 | 
					    </body>
 | 
				
			||||||
 | 
					    </html>
 | 
				
			||||||
 | 
					    `,
 | 
				
			||||||
 | 
					        hasHotItem,
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Decorate products with isHotItem
 | 
				
			||||||
 | 
					    const decorated = products.map((p) => {
 | 
				
			||||||
 | 
					      const isHotItem = hotItems.some((obj) =>
 | 
				
			||||||
 | 
					        p.name.toLowerCase().includes(obj.name.toLowerCase()),
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      if (isHotItem) hasHotItem = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return {
 | 
				
			||||||
 | 
					        ...p,
 | 
				
			||||||
 | 
					        isHotItem,
 | 
				
			||||||
 | 
					        isNew:
 | 
				
			||||||
 | 
					          new Date(p.created_at).getTime() === new Date(p.updated_at).getTime(),
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Separate hot and non-hot items, preserving original order
 | 
				
			||||||
 | 
					    const hotItemsList = decorated.filter((p) => p.isHotItem);
 | 
				
			||||||
 | 
					    const nonHotItems = decorated.filter((p) => !p.isHotItem);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Đưa các item mới (isNew) lên đầu trong nhóm không phải hot item
 | 
				
			||||||
 | 
					    const sortedNonHotItems = [
 | 
				
			||||||
 | 
					      ...nonHotItems.filter((p) => p.isNew),
 | 
				
			||||||
 | 
					      ...nonHotItems.filter((p) => !p.isNew),
 | 
				
			||||||
 | 
					    ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Separate hot and non-hot items, preserving original order
 | 
				
			||||||
 | 
					    const hotItemsFirst = [...hotItemsList, ...sortedNonHotItems];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const rows = hotItemsFirst
 | 
				
			||||||
 | 
					      .map((p) => {
 | 
				
			||||||
 | 
					        const isHotItem = isHotItemFn(p, hotItems);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (isHotItem) {
 | 
				
			||||||
 | 
					          hasHotItem = true;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const isNew =
 | 
				
			||||||
 | 
					          new Date(p.created_at).getTime() === new Date(p.updated_at).getTime();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const nameStyle = isNew ? 'color: #2f9e44; font-weight: bold;' : '';
 | 
				
			||||||
 | 
					        const namePrefix = isHotItem
 | 
				
			||||||
 | 
					          ? '<span style="font-weight: bold; color: #e03131;">[HOT ITEM]</span> '
 | 
				
			||||||
 | 
					          : '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return `
 | 
				
			||||||
 | 
					        <tr ${isHotItem ? 'style="background-color: #ff848496;margin-bottom: 4px;"' : 'margin-bottom: 4px;'}>
 | 
				
			||||||
 | 
					          <td style="display:flex;max-width: 60px;"><img src="${p.image_url}" alt="Product Image" style="height: 60px !important;width:60px !important; object-fit: contain; border-radius: 4px;" /></td>
 | 
				
			||||||
 | 
					          <td style="${nameStyle}">${namePrefix}${p.name}</td>
 | 
				
			||||||
 | 
					          <td style="font-weight: bold; color: #e03131;">${p.current_price ? '$' + p.current_price : 'None'}</td>
 | 
				
			||||||
 | 
					          <td><a href="${p.url}" target="_blank" style="color: #007bff;">View</a></td>
 | 
				
			||||||
 | 
					          <td>${extractDomainSmart(p.scrap_config.web_bid.origin_url)}</td>
 | 
				
			||||||
 | 
					        </tr>
 | 
				
			||||||
 | 
					      `;
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					      .join('');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const title = hasHotItem
 | 
				
			||||||
 | 
					      ? '<span style="font-weight: bold; color: #e03131;">[HOT ITEMS]</span> Product Listing'
 | 
				
			||||||
 | 
					      : 'Product Listing';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      html: `
 | 
				
			||||||
 | 
					  <!DOCTYPE html>
 | 
				
			||||||
 | 
					  <html lang="en">
 | 
				
			||||||
 | 
					  <head>
 | 
				
			||||||
 | 
					    <meta charset="UTF-8" />
 | 
				
			||||||
 | 
					    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
 | 
				
			||||||
 | 
					    <title>${title}</title>
 | 
				
			||||||
 | 
					  </head>
 | 
				
			||||||
 | 
					  <body style="font-family: sans-serif; background: #f8f9fa; padding: 20px;">
 | 
				
			||||||
 | 
					    <h2 style="text-align: center; color: #333;">${title}</h2>
 | 
				
			||||||
 | 
					    <div style="overflow-x: auto;">
 | 
				
			||||||
 | 
					      <table style="width: 100%; min-width: 500px; border-collapse: collapse; background: #fff; border-radius: 6px; box-shadow: 0 2px 6px rgba(0,0,0,0.05);">
 | 
				
			||||||
 | 
					        <thead style="background: #f1f3f5;">
 | 
				
			||||||
 | 
					          <tr>
 | 
				
			||||||
 | 
					            <th style="padding: 8px; text-align: left; border-bottom: 1px solid #ccc;">Image</th>
 | 
				
			||||||
 | 
					            <th style="padding: 8px; text-align: left; border-bottom: 1px solid #ccc;">Name</th>
 | 
				
			||||||
 | 
					            <th style="padding: 8px; text-align: left; border-bottom: 1px solid #ccc;">Price</th>
 | 
				
			||||||
 | 
					            <th style="padding: 8px; text-align: left; border-bottom: 1px solid #ccc;">Link</th>
 | 
				
			||||||
 | 
					            <th style="padding: 8px; text-align: left; border-bottom: 1px solid #ccc;">Domain</th>
 | 
				
			||||||
 | 
					          </tr>
 | 
				
			||||||
 | 
					        </thead>
 | 
				
			||||||
 | 
					        <tbody>
 | 
				
			||||||
 | 
					          ${rows}
 | 
				
			||||||
 | 
					        </tbody>
 | 
				
			||||||
 | 
					      </table>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					    <p style="text-align: center; color: #999; font-size: 12px; margin-top: 40px;">From: ${from}</p>
 | 
				
			||||||
 | 
					  </body>
 | 
				
			||||||
 | 
					  </html>
 | 
				
			||||||
 | 
					  `,
 | 
				
			||||||
 | 
					      hasHotItem,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  getAuctionStatusEmailContent(bid: Bid): string {
 | 
				
			||||||
 | 
					    const webname = extractDomain(bid.web_bid.origin_url);
 | 
				
			||||||
 | 
					    const title = `[${webname}] ${bid.name || 'Unnamed Item'}`;
 | 
				
			||||||
 | 
					    const endTime = formatEndTime(bid.close_time, false);
 | 
				
			||||||
 | 
					    const competitor = `$${bid.current_price}`;
 | 
				
			||||||
 | 
					    const max = `$${bid.max_price}`;
 | 
				
			||||||
 | 
					    const submitted = `$${bid.max_price}`;
 | 
				
			||||||
 | 
					    const nextBid = bid.max_price + bid.plus_price;
 | 
				
			||||||
 | 
					    const from = process.env.MAIL_USER || 'no-reply@example.com';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const cardStyle = `
 | 
				
			||||||
 | 
					    max-width: 600px; 
 | 
				
			||||||
 | 
					    margin: 20px auto; 
 | 
				
			||||||
 | 
					    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; 
 | 
				
			||||||
 | 
					    background: #ffffff; 
 | 
				
			||||||
 | 
					    border-radius: 8px; 
 | 
				
			||||||
 | 
					    box-shadow: 0 4px 8px rgba(0,0,0,0.1);
 | 
				
			||||||
 | 
					    color: #333;
 | 
				
			||||||
 | 
					    padding: 20px;
 | 
				
			||||||
 | 
					  `;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const headerStyle = (color: string) =>
 | 
				
			||||||
 | 
					      `font-size: 22px; font-weight: 700; color: ${color}; margin-bottom: 15px;`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const labelStyle = `font-weight: 600; width: 120px; display: inline-block; color: #555;`;
 | 
				
			||||||
 | 
					    const valueStyle = `color: #222;`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const renderRow = (label: string, value: string) =>
 | 
				
			||||||
 | 
					      `<p><span style="${labelStyle}">${label}:</span> <span style="${valueStyle}">${value}</span></p>`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    switch (bid.status) {
 | 
				
			||||||
 | 
					      case 'biding':
 | 
				
			||||||
 | 
					        return `
 | 
				
			||||||
 | 
					      <div style="${cardStyle}">
 | 
				
			||||||
 | 
					        <h2 style="${headerStyle('#2c7a7b')}">✅ Auto Bid Started</h2>
 | 
				
			||||||
 | 
					        ${renderRow('Title', title)}
 | 
				
			||||||
 | 
					        ${renderRow('Max', max)}
 | 
				
			||||||
 | 
					        ${renderRow('End time', endTime)}
 | 
				
			||||||
 | 
					        ${renderRow('Competitor', competitor)}
 | 
				
			||||||
 | 
					        ${renderRow('Bid submitted', submitted)}
 | 
				
			||||||
 | 
					        ${renderRow('From', from)}
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    `;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      case 'out-bid': {
 | 
				
			||||||
 | 
					        const overLimit = bid.current_price >= nextBid;
 | 
				
			||||||
 | 
					        const belowReserve = bid.reserve_price > nextBid;
 | 
				
			||||||
 | 
					        const timeExtended = bid.close_time ? 'Time extended' : 'No extension';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (isTimeReached(bid.close_time)) {
 | 
				
			||||||
 | 
					          return `
 | 
				
			||||||
 | 
					        <div style="${cardStyle}">
 | 
				
			||||||
 | 
					          <h2 style="${headerStyle('#718096')}">⏳ Auction Ended</h2>
 | 
				
			||||||
 | 
					          ${renderRow('Title', title)}
 | 
				
			||||||
 | 
					          ${renderRow('End time', endTime)}
 | 
				
			||||||
 | 
					          ${renderRow('Final price', competitor)}
 | 
				
			||||||
 | 
					          ${renderRow('From', from)}
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      `;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (overLimit || belowReserve) {
 | 
				
			||||||
 | 
					          return `
 | 
				
			||||||
 | 
					        <div style="${cardStyle}">
 | 
				
			||||||
 | 
					          <h2 style="${headerStyle('#dd6b20')}">⚠️ Outbid (${timeExtended})</h2>
 | 
				
			||||||
 | 
					          ${renderRow('Title', title)}
 | 
				
			||||||
 | 
					          ${renderRow('Competitor', competitor)}
 | 
				
			||||||
 | 
					          ${renderRow('Max', max)}
 | 
				
			||||||
 | 
					          ${renderRow('Next bid at', `$${nextBid}`)}
 | 
				
			||||||
 | 
					          ${renderRow('End time', endTime)}
 | 
				
			||||||
 | 
					          ${renderRow('From', from)}
 | 
				
			||||||
 | 
					          <p style="color:#c05621; font-weight: 600;">⚠️ Current bid exceeds your max bid.</p>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      `;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return `
 | 
				
			||||||
 | 
					      <div style="${cardStyle}">
 | 
				
			||||||
 | 
					        <h2 style="${headerStyle('#e53e3e')}">🛑 Auction Canceled (${timeExtended})</h2>
 | 
				
			||||||
 | 
					        ${renderRow('Title', title)}
 | 
				
			||||||
 | 
					        ${renderRow('Competitor', competitor)}
 | 
				
			||||||
 | 
					        ${renderRow('Max', max)}
 | 
				
			||||||
 | 
					        ${renderRow('Next bid at', `$${nextBid}`)}
 | 
				
			||||||
 | 
					        ${renderRow('End time', endTime)}
 | 
				
			||||||
 | 
					        ${renderRow('From', from)}
 | 
				
			||||||
 | 
					        <p style="color:#9b2c2c; font-weight: 600;">🛑 Auction has been canceled.</p>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    `;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      case 'win-bid':
 | 
				
			||||||
 | 
					        return `
 | 
				
			||||||
 | 
					      <div style="${cardStyle}">
 | 
				
			||||||
 | 
					        <h2 style="${headerStyle('#2b6cb0')}">🎉 You Won!</h2>
 | 
				
			||||||
 | 
					        ${renderRow('Title', title)}
 | 
				
			||||||
 | 
					        ${renderRow('Price won', `$${bid.current_price}`)}
 | 
				
			||||||
 | 
					        ${renderRow('Max', max)}
 | 
				
			||||||
 | 
					        ${renderRow('From', from)}
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    `;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      default:
 | 
				
			||||||
 | 
					        return `
 | 
				
			||||||
 | 
					      <div style="${cardStyle}">
 | 
				
			||||||
 | 
					        <h2 style="${headerStyle('#718096')}">❓ Unknown Status</h2>
 | 
				
			||||||
 | 
					        ${renderRow('Title', title)}
 | 
				
			||||||
 | 
					        ${renderRow('From', from)}
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    `;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  getBidSubmittedEmailContent(bid: Bid): string {
 | 
				
			||||||
 | 
					    const webname = extractDomain(bid.web_bid.origin_url);
 | 
				
			||||||
 | 
					    const title = `[${webname}] ${bid.name || 'Unnamed Item'}`;
 | 
				
			||||||
 | 
					    const endTime = formatEndTime(bid.close_time, false);
 | 
				
			||||||
 | 
					    const competitor = `$${bid.current_price}`;
 | 
				
			||||||
 | 
					    const max = `$${bid.max_price}`;
 | 
				
			||||||
 | 
					    const submitted = `$${bid.max_price}`;
 | 
				
			||||||
 | 
					    const maxReached = bid.max_price <= bid.max_price;
 | 
				
			||||||
 | 
					    const from = process.env.MAIL_USER || 'no-reply@example.com';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return `
 | 
				
			||||||
 | 
					  <!DOCTYPE html>
 | 
				
			||||||
 | 
					  <html lang="en">
 | 
				
			||||||
 | 
					  <head>
 | 
				
			||||||
 | 
					    <style>
 | 
				
			||||||
 | 
					      body {
 | 
				
			||||||
 | 
					        font-family: Arial, sans-serif;
 | 
				
			||||||
 | 
					        background: #f9f9f9;
 | 
				
			||||||
 | 
					        color: #333;
 | 
				
			||||||
 | 
					        padding: 20px;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      .container {
 | 
				
			||||||
 | 
					        background: #fff;
 | 
				
			||||||
 | 
					        padding: 20px;
 | 
				
			||||||
 | 
					        border-radius: 8px;
 | 
				
			||||||
 | 
					        box-shadow: 0 2px 8px rgba(0,0,0,0.1);
 | 
				
			||||||
 | 
					        max-width: 600px;
 | 
				
			||||||
 | 
					        margin: auto;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      h2 {
 | 
				
			||||||
 | 
					        color: #007bff;
 | 
				
			||||||
 | 
					        text-align: center;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      table {
 | 
				
			||||||
 | 
					        width: 100%;
 | 
				
			||||||
 | 
					        border-collapse: collapse;
 | 
				
			||||||
 | 
					        margin-top: 15px;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      th, td {
 | 
				
			||||||
 | 
					        padding: 10px;
 | 
				
			||||||
 | 
					        border-bottom: 1px solid #ddd;
 | 
				
			||||||
 | 
					        text-align: left;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      th {
 | 
				
			||||||
 | 
					        background-color: #f1f1f1;
 | 
				
			||||||
 | 
					        color: #555;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      .highlight {
 | 
				
			||||||
 | 
					        color: #e03131;
 | 
				
			||||||
 | 
					        font-weight: bold;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      .max-reach {
 | 
				
			||||||
 | 
					        color: #d6336c;
 | 
				
			||||||
 | 
					        font-weight: bold;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    </style>
 | 
				
			||||||
 | 
					  </head>
 | 
				
			||||||
 | 
					  <body>
 | 
				
			||||||
 | 
					    <div class="container">
 | 
				
			||||||
 | 
					      <h2>Bid Submitted${bid.close_time ? ', Time extended' : ', No extension'}${maxReached ? ' <span class="max-reach">* MAX REACH *</span>' : ''}</h2>
 | 
				
			||||||
 | 
					      <table>
 | 
				
			||||||
 | 
					        <tbody>
 | 
				
			||||||
 | 
					          <tr>
 | 
				
			||||||
 | 
					            <th>Title</th>
 | 
				
			||||||
 | 
					            <td>${title}</td>
 | 
				
			||||||
 | 
					          </tr>
 | 
				
			||||||
 | 
					          <tr>
 | 
				
			||||||
 | 
					            <th>Competitor</th>
 | 
				
			||||||
 | 
					            <td>${competitor}</td>
 | 
				
			||||||
 | 
					          </tr>
 | 
				
			||||||
 | 
					          <tr>
 | 
				
			||||||
 | 
					            <th>Bid Submitted</th>
 | 
				
			||||||
 | 
					            <td>${submitted} ${maxReached ? '<span class="max-reach">(<b>***MAXIMUM REACH***</b>)</span>' : ''}</td>
 | 
				
			||||||
 | 
					          </tr>
 | 
				
			||||||
 | 
					          <tr>
 | 
				
			||||||
 | 
					            <th>Max</th>
 | 
				
			||||||
 | 
					            <td class="highlight">${max}</td>
 | 
				
			||||||
 | 
					          </tr>
 | 
				
			||||||
 | 
					          <tr>
 | 
				
			||||||
 | 
					            <th>End Time</th>
 | 
				
			||||||
 | 
					            <td>${endTime}</td>
 | 
				
			||||||
 | 
					          </tr>
 | 
				
			||||||
 | 
					          <tr>
 | 
				
			||||||
 | 
					            <th>From</th>
 | 
				
			||||||
 | 
					            <td>${from}</td>
 | 
				
			||||||
 | 
					          </tr>
 | 
				
			||||||
 | 
					        </tbody>
 | 
				
			||||||
 | 
					      </table>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  </body>
 | 
				
			||||||
 | 
					  </html>
 | 
				
			||||||
 | 
					  `;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async getBidRecordEmailContent(bid: Bid, type: 'action' | 'api') {
 | 
				
			||||||
 | 
					    let recordData: {
 | 
				
			||||||
 | 
					      bid_id: number;
 | 
				
			||||||
 | 
					      name: string;
 | 
				
			||||||
 | 
					      original_name: string;
 | 
				
			||||||
 | 
					      timestamp: number;
 | 
				
			||||||
 | 
					    }[] = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const data = await this.bidsService.getRecords(bid.id);
 | 
				
			||||||
 | 
					      recordData = data.data || [];
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      recordData = [];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const json = this.bidMetadataService.getDemoResponse(bid.metadata);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const videoUrl = this.bidMetadataService.getLatestRecordLink(bid.metadata);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const closeTime = bid.close_time
 | 
				
			||||||
 | 
					      ? new Date(bid.close_time).toLocaleString('en-AU', {
 | 
				
			||||||
 | 
					          timeZone: 'Australia/Sydney',
 | 
				
			||||||
 | 
					          dateStyle: 'medium',
 | 
				
			||||||
 | 
					          timeStyle: 'short',
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					      : 'Unknown';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let content = '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (type === 'action') {
 | 
				
			||||||
 | 
					      content = `
 | 
				
			||||||
 | 
					      <h3>📹 Record Preview</h3>
 | 
				
			||||||
 | 
					      <p><strong>Name:</strong> ${bid?.name}</p>
 | 
				
			||||||
 | 
					      <p><strong>Recorded:</strong> ${new Date().toLocaleString()}</p>
 | 
				
			||||||
 | 
					      <p>
 | 
				
			||||||
 | 
					        <a href="${videoUrl || ''}" target="_blank" style="
 | 
				
			||||||
 | 
					          display: inline-block;
 | 
				
			||||||
 | 
					          padding: 10px 16px;
 | 
				
			||||||
 | 
					          background-color: #007bff;
 | 
				
			||||||
 | 
					          color: #fff;
 | 
				
			||||||
 | 
					          text-decoration: none;
 | 
				
			||||||
 | 
					          border-radius: 4px;
 | 
				
			||||||
 | 
					          margin-top: 12px;
 | 
				
			||||||
 | 
					        ">
 | 
				
			||||||
 | 
					          ▶️ Watch Video
 | 
				
			||||||
 | 
					        </a>
 | 
				
			||||||
 | 
					      </p>
 | 
				
			||||||
 | 
					    `;
 | 
				
			||||||
 | 
					    } else if (type === 'api') {
 | 
				
			||||||
 | 
					      content = `
 | 
				
			||||||
 | 
					      <h3>📦 Record JSON</h3>
 | 
				
			||||||
 | 
					      <pre style="
 | 
				
			||||||
 | 
					        background-color: #f4f4f4;
 | 
				
			||||||
 | 
					        padding: 12px;
 | 
				
			||||||
 | 
					        border-radius: 4px;
 | 
				
			||||||
 | 
					        font-size: 13px;
 | 
				
			||||||
 | 
					        overflow-x: auto;
 | 
				
			||||||
 | 
					      ">${JSON.stringify(json, null, 2)}</pre>
 | 
				
			||||||
 | 
					    `;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return `
 | 
				
			||||||
 | 
					    <div style="font-family: Arial, sans-serif; color: #333; max-width: 600px; margin: 0 auto;">
 | 
				
			||||||
 | 
					      <h2>📝 Bid Demo: ${bid?.name}</h2>
 | 
				
			||||||
 | 
					      <p><strong>⏰ Close Time:</strong> ${closeTime}</p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      ${
 | 
				
			||||||
 | 
					        content
 | 
				
			||||||
 | 
					          ? content
 | 
				
			||||||
 | 
					          : `<p style="color: red;">⚠️ No record available for this bid.</p>`
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <hr style="margin: 24px 0;">
 | 
				
			||||||
 | 
					      <p style="font-size: 12px; color: #888;">This is an automated message. Please do not reply.</p>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  `;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  generateWarningEmailTemplate(title: string, messages: string[]): string {
 | 
				
			||||||
 | 
					    const timestamp = new Date().toLocaleString(); // hoặc new Date().toISOString()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return `
 | 
				
			||||||
 | 
					    <div style="font-family: Arial, sans-serif; padding: 20px; background-color: #fffbe6; color: #333;">
 | 
				
			||||||
 | 
					      <h2 style="color: #d9534f;">⚠️ ${title}</h2>
 | 
				
			||||||
 | 
					      <ul>
 | 
				
			||||||
 | 
					        ${messages.map((msg) => `<li>${msg}</li>`).join('')}
 | 
				
			||||||
 | 
					      </ul>
 | 
				
			||||||
 | 
					      <p style="margin-top: 20px; font-size: 12px; color: #999;">
 | 
				
			||||||
 | 
					        Sent at: ${timestamp}
 | 
				
			||||||
 | 
					      </p>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  `;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  generateSuccessEmailTemplate(title: string, messages: string[]): string {
 | 
				
			||||||
 | 
					    const timestamp = new Date().toLocaleString(); // hoặc new Date().toISOString()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return `
 | 
				
			||||||
 | 
					    <div style="font-family: Arial, sans-serif; padding: 20px; background-color: #e6fff2; color: #333;">
 | 
				
			||||||
 | 
					      <h2 style="color: #28a745;">✅ ${title}</h2>
 | 
				
			||||||
 | 
					      <ul>
 | 
				
			||||||
 | 
					        ${messages.map((msg) => `<li>${msg}</li>`).join('')}
 | 
				
			||||||
 | 
					      </ul>
 | 
				
			||||||
 | 
					      <p style="margin-top: 20px; font-size: 12px; color: #999;">
 | 
				
			||||||
 | 
					        Sent at: ${timestamp}
 | 
				
			||||||
 | 
					      </p>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  `;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  generateErrorEmailTemplate(title: string, messages: string[]): string {
 | 
				
			||||||
 | 
					    const timestamp = new Date().toLocaleString();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return `
 | 
				
			||||||
 | 
					    <div style="font-family: Arial, sans-serif; padding: 20px; background-color: #fdecea; color: #333;">
 | 
				
			||||||
 | 
					      <h2 style="color: #c82333;">❌ ${title}</h2>
 | 
				
			||||||
 | 
					      <ul>
 | 
				
			||||||
 | 
					        ${messages.map((msg) => `<li>${msg}</li>`).join('')}
 | 
				
			||||||
 | 
					      </ul>
 | 
				
			||||||
 | 
					      <p style="margin-top: 20px; font-size: 12px; color: #999;">
 | 
				
			||||||
 | 
					        Sent at: ${timestamp}
 | 
				
			||||||
 | 
					      </p>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  `;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  generateInfoEmailTemplate(title: string, messages: string[]): string {
 | 
				
			||||||
 | 
					    const timestamp = new Date().toLocaleString();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return `
 | 
				
			||||||
 | 
					    <div style="font-family: Arial, sans-serif; padding: 20px; background-color: #e7f3fe; color: #333;">
 | 
				
			||||||
 | 
					      <h2 style="color: #31708f;">ℹ️ ${title}</h2>
 | 
				
			||||||
 | 
					      <ul>
 | 
				
			||||||
 | 
					        ${messages.map((msg) => `<li>${msg}</li>`).join('')}
 | 
				
			||||||
 | 
					      </ul>
 | 
				
			||||||
 | 
					      <p style="margin-top: 20px; font-size: 12px; color: #999;">
 | 
				
			||||||
 | 
					        Sent at: ${timestamp}
 | 
				
			||||||
 | 
					      </p>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  `;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1,3 +1,3 @@
 | 
				
			||||||
export const NAME_EVENTS = {
 | 
					// export const NAME_EVENTS = {
 | 
				
			||||||
  BID_STATUS: 'notify.bid-status',
 | 
					//   BID_STATUS: 'notify.bid-status',
 | 
				
			||||||
};
 | 
					// };
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -20,7 +20,8 @@ export class ClientNotificationController {
 | 
				
			||||||
  @Post('test')
 | 
					  @Post('test')
 | 
				
			||||||
  async test() {
 | 
					  async test() {
 | 
				
			||||||
    const bid = await this.bidsService.bidsRepo.findOne({
 | 
					    const bid = await this.bidsService.bidsRepo.findOne({
 | 
				
			||||||
      where: { lot_id: '26077023' },
 | 
					      where: { lot_id: '23755862' },
 | 
				
			||||||
 | 
					      relations: { web_bid: true },
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return await this.notifyService.emitBidStatus({
 | 
					    return await this.notifyService.emitBidStatus({
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,16 +1,23 @@
 | 
				
			||||||
import { Injectable } from '@nestjs/common';
 | 
					import { Injectable } from '@nestjs/common';
 | 
				
			||||||
import { OnEvent } from '@nestjs/event-emitter';
 | 
					import { OnEvent } from '@nestjs/event-emitter';
 | 
				
			||||||
import { NAME_EVENTS } from '../constants';
 | 
					 | 
				
			||||||
import { Bid } from '@/modules/bids/entities/bid.entity';
 | 
					import { Bid } from '@/modules/bids/entities/bid.entity';
 | 
				
			||||||
import { Notification } from '../entities/notification.entity';
 | 
					import { Notification } from '../entities/notification.entity';
 | 
				
			||||||
import { BotTelegramApi } from '@/modules/bids/apis/bot-telegram.api';
 | 
					import { BotTelegramApi } from '@/modules/bids/apis/bot-telegram.api';
 | 
				
			||||||
 | 
					import { MailsService } from '@/modules/mails/services/mails.service';
 | 
				
			||||||
 | 
					import { ConfigsService } from '@/modules/bids/services/configs.service';
 | 
				
			||||||
 | 
					import * as moment from 'moment';
 | 
				
			||||||
 | 
					import { Event } from '@/modules/bids/utils/events';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Injectable()
 | 
					@Injectable()
 | 
				
			||||||
export class AdminNotificationListener {
 | 
					export class AdminNotificationListener {
 | 
				
			||||||
  constructor(private readonly botTelegramApi: BotTelegramApi) {}
 | 
					  constructor(
 | 
				
			||||||
 | 
					    private readonly botTelegramApi: BotTelegramApi,
 | 
				
			||||||
 | 
					    private readonly mailsService: MailsService,
 | 
				
			||||||
 | 
					    private readonly configsSerice: ConfigsService,
 | 
				
			||||||
 | 
					  ) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @OnEvent(NAME_EVENTS.BID_STATUS)
 | 
					  @OnEvent(Event.BID_STATUS)
 | 
				
			||||||
  handleBidStatus({
 | 
					  async handleBidStatus({
 | 
				
			||||||
    bid,
 | 
					    bid,
 | 
				
			||||||
    notification,
 | 
					    notification,
 | 
				
			||||||
  }: {
 | 
					  }: {
 | 
				
			||||||
| 
						 | 
					@ -19,6 +26,141 @@ export class AdminNotificationListener {
 | 
				
			||||||
  }) {
 | 
					  }) {
 | 
				
			||||||
    if (JSON.parse(notification.send_to).length <= 0) return;
 | 
					    if (JSON.parse(notification.send_to).length <= 0) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.botTelegramApi.sendMessage(notification.message);
 | 
					    // this.botTelegramApi.sendMessage(notification.message);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const mails =
 | 
				
			||||||
 | 
					      (await this.configsSerice.getConfig('MAIL_SCRAP_REPORT')).value || '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.mailsService.sendHtmlMailJob({
 | 
				
			||||||
 | 
					      to: mails,
 | 
				
			||||||
 | 
					      html: this.mailsService.getAuctionStatusEmailContent(bid),
 | 
				
			||||||
 | 
					      subject:
 | 
				
			||||||
 | 
					        `[${bid.status.toUpperCase()}] Report Auto Auctions System ` +
 | 
				
			||||||
 | 
					        moment(new Date()).format('YYYY-MM-DD HH:mm'),
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @OnEvent(Event.BID_SUBMITED)
 | 
				
			||||||
 | 
					  async handleBidSubmited(bid: Bid) {
 | 
				
			||||||
 | 
					    const mails =
 | 
				
			||||||
 | 
					      (await this.configsSerice.getConfig('MAIL_SCRAP_REPORT')).value || '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.mailsService.sendHtmlMailJob({
 | 
				
			||||||
 | 
					      to: mails,
 | 
				
			||||||
 | 
					      html: this.mailsService.getBidSubmittedEmailContent(bid),
 | 
				
			||||||
 | 
					      subject:
 | 
				
			||||||
 | 
					        'Report Auto Auctions System ' +
 | 
				
			||||||
 | 
					        moment(new Date()).format('YYYY-MM-DD HH:mm'),
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @OnEvent(Event.BID_DEMO)
 | 
				
			||||||
 | 
					  async handleBidDemo({ bid, type }: { bid: Bid; type: 'api' | 'action' }) {
 | 
				
			||||||
 | 
					    const mails =
 | 
				
			||||||
 | 
					      (await this.configsSerice.getConfig('MAIL_SCRAP_REPORT')).value || '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const html = await this.mailsService.getBidRecordEmailContent(bid, type);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.mailsService.sendHtmlMailJob({
 | 
				
			||||||
 | 
					      to: mails,
 | 
				
			||||||
 | 
					      html: html,
 | 
				
			||||||
 | 
					      subject:
 | 
				
			||||||
 | 
					        'Demo Auto Auctions System ' +
 | 
				
			||||||
 | 
					        moment(new Date()).format('YYYY-MM-DD HH:mm'),
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @OnEvent(Event.SEND_WARNING)
 | 
				
			||||||
 | 
					  async handleSendWarning({
 | 
				
			||||||
 | 
					    title,
 | 
				
			||||||
 | 
					    messages,
 | 
				
			||||||
 | 
					  }: {
 | 
				
			||||||
 | 
					    title: string;
 | 
				
			||||||
 | 
					    messages: string[];
 | 
				
			||||||
 | 
					  }) {
 | 
				
			||||||
 | 
					    const mails =
 | 
				
			||||||
 | 
					      (await this.configsSerice.getConfig('MAIL_SCRAP_REPORT')).value || '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const html = this.mailsService.generateWarningEmailTemplate(
 | 
				
			||||||
 | 
					      title,
 | 
				
			||||||
 | 
					      messages,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.mailsService.sendHtmlMailJob({
 | 
				
			||||||
 | 
					      to: mails,
 | 
				
			||||||
 | 
					      html: html,
 | 
				
			||||||
 | 
					      subject:
 | 
				
			||||||
 | 
					        '[WARNING] Auto Auctions System ' +
 | 
				
			||||||
 | 
					        moment(new Date()).format('YYYY-MM-DD HH:mm'),
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @OnEvent(Event.SEND_SUCCESS)
 | 
				
			||||||
 | 
					  async handleSendSuccess({
 | 
				
			||||||
 | 
					    title,
 | 
				
			||||||
 | 
					    messages,
 | 
				
			||||||
 | 
					  }: {
 | 
				
			||||||
 | 
					    title: string;
 | 
				
			||||||
 | 
					    messages: string[];
 | 
				
			||||||
 | 
					  }) {
 | 
				
			||||||
 | 
					    const mails =
 | 
				
			||||||
 | 
					      (await this.configsSerice.getConfig('MAIL_SCRAP_REPORT')).value || '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const html = this.mailsService.generateSuccessEmailTemplate(
 | 
				
			||||||
 | 
					      title,
 | 
				
			||||||
 | 
					      messages,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.mailsService.sendHtmlMailJob({
 | 
				
			||||||
 | 
					      to: mails,
 | 
				
			||||||
 | 
					      html: html,
 | 
				
			||||||
 | 
					      subject:
 | 
				
			||||||
 | 
					        '[SUCCESS] Auto Auctions System ' +
 | 
				
			||||||
 | 
					        moment(new Date()).format('YYYY-MM-DD HH:mm'),
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @OnEvent(Event.SEND_ERROR)
 | 
				
			||||||
 | 
					  async handleSendError({
 | 
				
			||||||
 | 
					    title,
 | 
				
			||||||
 | 
					    messages,
 | 
				
			||||||
 | 
					  }: {
 | 
				
			||||||
 | 
					    title: string;
 | 
				
			||||||
 | 
					    messages: string[];
 | 
				
			||||||
 | 
					  }) {
 | 
				
			||||||
 | 
					    const mails =
 | 
				
			||||||
 | 
					      (await this.configsSerice.getConfig('MAIL_SCRAP_REPORT')).value || '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const html = this.mailsService.generateErrorEmailTemplate(title, messages);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.mailsService.sendHtmlMailJob({
 | 
				
			||||||
 | 
					      to: mails,
 | 
				
			||||||
 | 
					      html: html,
 | 
				
			||||||
 | 
					      subject:
 | 
				
			||||||
 | 
					        '[ERROR] Auto Auctions System ' +
 | 
				
			||||||
 | 
					        moment(new Date()).format('YYYY-MM-DD HH:mm'),
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @OnEvent(Event.SEND_INFO)
 | 
				
			||||||
 | 
					  async handleSendInfo({
 | 
				
			||||||
 | 
					    title,
 | 
				
			||||||
 | 
					    messages,
 | 
				
			||||||
 | 
					  }: {
 | 
				
			||||||
 | 
					    title: string;
 | 
				
			||||||
 | 
					    messages: string[];
 | 
				
			||||||
 | 
					  }) {
 | 
				
			||||||
 | 
					    const mails =
 | 
				
			||||||
 | 
					      (await this.configsSerice.getConfig('MAIL_SCRAP_REPORT')).value || '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const html = this.mailsService.generateInfoEmailTemplate(title, messages);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.mailsService.sendHtmlMailJob({
 | 
				
			||||||
 | 
					      to: mails,
 | 
				
			||||||
 | 
					      html: html,
 | 
				
			||||||
 | 
					      subject:
 | 
				
			||||||
 | 
					        '[INFO] Auto Auctions System ' +
 | 
				
			||||||
 | 
					        moment(new Date()).format('YYYY-MM-DD HH:mm'),
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -8,11 +8,13 @@ import { AdminNotificationListener } from './listeners/admin-notification.listen
 | 
				
			||||||
import { NotificationService } from './notification.service';
 | 
					import { NotificationService } from './notification.service';
 | 
				
			||||||
import { SendMessageHistoriesService } from '../bids/services/send-message-histories.service';
 | 
					import { SendMessageHistoriesService } from '../bids/services/send-message-histories.service';
 | 
				
			||||||
import { SendMessageHistory } from '../bids/entities/send-message-histories.entity';
 | 
					import { SendMessageHistory } from '../bids/entities/send-message-histories.entity';
 | 
				
			||||||
 | 
					import { MailsModule } from '../mails/mails.module';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Module({
 | 
					@Module({
 | 
				
			||||||
  imports: [
 | 
					  imports: [
 | 
				
			||||||
    forwardRef(() => BidsModule),
 | 
					    forwardRef(() => BidsModule),
 | 
				
			||||||
    TypeOrmModule.forFeature([Notification, SendMessageHistory]),
 | 
					    TypeOrmModule.forFeature([Notification, SendMessageHistory]),
 | 
				
			||||||
 | 
					    MailsModule,
 | 
				
			||||||
  ],
 | 
					  ],
 | 
				
			||||||
  controllers: [NotificationController, ClientNotificationController],
 | 
					  controllers: [NotificationController, ClientNotificationController],
 | 
				
			||||||
  providers: [NotificationService, AdminNotificationListener],
 | 
					  providers: [NotificationService, AdminNotificationListener],
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,7 +1,6 @@
 | 
				
			||||||
import { Injectable } from '@nestjs/common';
 | 
					import { Injectable } from '@nestjs/common';
 | 
				
			||||||
import { EventEmitter2 } from '@nestjs/event-emitter';
 | 
					import { EventEmitter2 } from '@nestjs/event-emitter';
 | 
				
			||||||
import { Bid } from '../bids/entities/bid.entity';
 | 
					import { Bid } from '../bids/entities/bid.entity';
 | 
				
			||||||
import { NAME_EVENTS } from './constants';
 | 
					 | 
				
			||||||
import { BotTelegramApi } from '../bids/apis/bot-telegram.api';
 | 
					import { BotTelegramApi } from '../bids/apis/bot-telegram.api';
 | 
				
			||||||
import { InjectRepository } from '@nestjs/typeorm';
 | 
					import { InjectRepository } from '@nestjs/typeorm';
 | 
				
			||||||
import { Notification } from './entities/notification.entity';
 | 
					import { Notification } from './entities/notification.entity';
 | 
				
			||||||
| 
						 | 
					@ -17,6 +16,7 @@ import { Column } from 'nestjs-paginate/lib/helper';
 | 
				
			||||||
import AppResponse from '@/response/app-response';
 | 
					import AppResponse from '@/response/app-response';
 | 
				
			||||||
import { SendMessageHistoriesService } from '../bids/services/send-message-histories.service';
 | 
					import { SendMessageHistoriesService } from '../bids/services/send-message-histories.service';
 | 
				
			||||||
import { SendMessageHistory } from '../bids/entities/send-message-histories.entity';
 | 
					import { SendMessageHistory } from '../bids/entities/send-message-histories.entity';
 | 
				
			||||||
 | 
					import { Event } from '../bids/utils/events';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Injectable()
 | 
					@Injectable()
 | 
				
			||||||
export class NotificationService {
 | 
					export class NotificationService {
 | 
				
			||||||
| 
						 | 
					@ -109,18 +109,40 @@ export class NotificationService {
 | 
				
			||||||
      send_to: JSON.stringify(sendToData),
 | 
					      send_to: JSON.stringify(sendToData),
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await this.sendMessageRepo.save({
 | 
					    try {
 | 
				
			||||||
      bid: { id: bid.id },
 | 
					      const prevAnyMessage = await this.sendMessageRepo.findOne({
 | 
				
			||||||
      message: notification.message,
 | 
					        where: {
 | 
				
			||||||
      type: bid.status,
 | 
					          bid: { id: bid.id },
 | 
				
			||||||
    });
 | 
					          message: notification.message,
 | 
				
			||||||
 | 
					          type: bid.status,
 | 
				
			||||||
 | 
					          max_price: bid.max_price,
 | 
				
			||||||
 | 
					          reserve_price: bid.reserve_price,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.eventEmitter.emit(NAME_EVENTS.BID_STATUS, {
 | 
					      if (prevAnyMessage) return;
 | 
				
			||||||
      bid: {
 | 
					
 | 
				
			||||||
        ...bid,
 | 
					      await this.sendMessageRepo.save({
 | 
				
			||||||
        status: 'out-bid',
 | 
					        bid: { id: bid.id },
 | 
				
			||||||
      },
 | 
					        message: notification.message,
 | 
				
			||||||
      notification,
 | 
					        type: bid.status,
 | 
				
			||||||
    });
 | 
					        max_price: bid.max_price,
 | 
				
			||||||
 | 
					        reserve_price: bid.reserve_price,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      this.eventEmitter.emit(Event.BID_STATUS, {
 | 
				
			||||||
 | 
					        bid: {
 | 
				
			||||||
 | 
					          ...bid,
 | 
				
			||||||
 | 
					          // status: 'out-bid',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        notification,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      console.log(
 | 
				
			||||||
 | 
					        '%csrc/modules/notification/notification.service.ts:131 Error',
 | 
				
			||||||
 | 
					        'color: #007acc;',
 | 
				
			||||||
 | 
					        Error,
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,12 @@
 | 
				
			||||||
 | 
					import { Controller, Get } from '@nestjs/common';
 | 
				
			||||||
 | 
					import { ScrapConfigsService } from '../../services/scrap-config.service';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Controller('scrap-configs')
 | 
				
			||||||
 | 
					export class ClientScrapConfigsController {
 | 
				
			||||||
 | 
					  constructor(private readonly scrapConfigsService: ScrapConfigsService) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Get()
 | 
				
			||||||
 | 
					  async clientGetScrapeConfigs() {
 | 
				
			||||||
 | 
					    return await this.scrapConfigsService.clientGetScrapeConfigs();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,23 @@
 | 
				
			||||||
 | 
					import { Body, Controller, Get, Post } from '@nestjs/common';
 | 
				
			||||||
 | 
					import { ScrapConfigsService } from '../../services/scrap-config.service';
 | 
				
			||||||
 | 
					import { ScrapItemsService } from '../../services/scrap-item-config.service';
 | 
				
			||||||
 | 
					import { UpsertScrapItemDto } from '../../dto/scrap-items/upsert-scrap-item.dto';
 | 
				
			||||||
 | 
					import { ScrapItem } from '../../entities/scrap-item.entity';
 | 
				
			||||||
 | 
					import { Paginate, PaginateQuery } from 'nestjs-paginate';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Controller('scrap-items')
 | 
				
			||||||
 | 
					export class ClientScrapItemsController {
 | 
				
			||||||
 | 
					  constructor(private readonly scrapItemsService: ScrapItemsService) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Post('upsert')
 | 
				
			||||||
 | 
					  async upsertScrapItems(@Body() data: UpsertScrapItemDto[]) {
 | 
				
			||||||
 | 
					    return await this.scrapItemsService.upsertScrapItemsRes(
 | 
				
			||||||
 | 
					      data as ScrapItem[],
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Get()
 | 
				
			||||||
 | 
					  async index(@Paginate() query: PaginateQuery) {
 | 
				
			||||||
 | 
					    return await this.scrapItemsService.index(query);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,23 @@
 | 
				
			||||||
 | 
					import { Body, Controller, Param, Post, Put } from '@nestjs/common';
 | 
				
			||||||
 | 
					import { CreateScrapConfigDto } from '../dto/scrap-config/create-scrap-config';
 | 
				
			||||||
 | 
					import { UpdateScrapConfigDto } from '../dto/scrap-config/update-scrap-config';
 | 
				
			||||||
 | 
					import { ScrapConfig } from '../entities/scrap-config.entity';
 | 
				
			||||||
 | 
					import { ScrapConfigsService } from '../services/scrap-config.service';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Controller('admin/scrap-configs')
 | 
				
			||||||
 | 
					export class ScrapConfigsController {
 | 
				
			||||||
 | 
					  constructor(private readonly scrapConfigsService: ScrapConfigsService) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Post()
 | 
				
			||||||
 | 
					  async create(@Body() data: CreateScrapConfigDto) {
 | 
				
			||||||
 | 
					    return await this.scrapConfigsService.create(data);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Put(':id')
 | 
				
			||||||
 | 
					  async update(
 | 
				
			||||||
 | 
					    @Param('id') id: ScrapConfig['id'],
 | 
				
			||||||
 | 
					    @Body() data: UpdateScrapConfigDto,
 | 
				
			||||||
 | 
					  ) {
 | 
				
			||||||
 | 
					    return await this.scrapConfigsService.update(id, data);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,23 @@
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  IsBoolean,
 | 
				
			||||||
 | 
					  IsNumber,
 | 
				
			||||||
 | 
					  IsOptional,
 | 
				
			||||||
 | 
					  IsString,
 | 
				
			||||||
 | 
					  IsUrl,
 | 
				
			||||||
 | 
					} from 'class-validator';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class CreateScrapConfigDto {
 | 
				
			||||||
 | 
					  @IsUrl()
 | 
				
			||||||
 | 
					  search_url: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @IsBoolean()
 | 
				
			||||||
 | 
					  @IsOptional()
 | 
				
			||||||
 | 
					  enable: boolean;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @IsString()
 | 
				
			||||||
 | 
					  @IsOptional()
 | 
				
			||||||
 | 
					  keywords: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @IsNumber()
 | 
				
			||||||
 | 
					  web_id: number;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,4 @@
 | 
				
			||||||
 | 
					import { PartialType } from '@nestjs/mapped-types';
 | 
				
			||||||
 | 
					import { CreateScrapConfigDto } from './create-scrap-config';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class UpdateScrapConfigDto extends PartialType(CreateScrapConfigDto) {}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,18 @@
 | 
				
			||||||
 | 
					import { IsNumber, IsString, IsUrl } from 'class-validator';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class UpsertScrapItemDto {
 | 
				
			||||||
 | 
					  @IsUrl()
 | 
				
			||||||
 | 
					  url: string;
 | 
				
			||||||
 | 
					  @IsString()
 | 
				
			||||||
 | 
					  image_url: string;
 | 
				
			||||||
 | 
					  @IsString()
 | 
				
			||||||
 | 
					  name: string;
 | 
				
			||||||
 | 
					  @IsString()
 | 
				
			||||||
 | 
					  keyword: string;
 | 
				
			||||||
 | 
					  @IsNumber()
 | 
				
			||||||
 | 
					  current_price: number;
 | 
				
			||||||
 | 
					  @IsNumber()
 | 
				
			||||||
 | 
					  scrap_config_id: number;
 | 
				
			||||||
 | 
					  @IsString()
 | 
				
			||||||
 | 
					  model: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,36 @@
 | 
				
			||||||
 | 
					import { WebBid } from '@/modules/bids/entities/wed-bid.entity';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  Column,
 | 
				
			||||||
 | 
					  Entity,
 | 
				
			||||||
 | 
					  JoinColumn,
 | 
				
			||||||
 | 
					  OneToMany,
 | 
				
			||||||
 | 
					  OneToOne,
 | 
				
			||||||
 | 
					  PrimaryGeneratedColumn,
 | 
				
			||||||
 | 
					  Unique,
 | 
				
			||||||
 | 
					} from 'typeorm';
 | 
				
			||||||
 | 
					import { ScrapItem } from './scrap-item.entity';
 | 
				
			||||||
 | 
					import { Timestamp } from './timestamp';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Entity('scrap-configs')
 | 
				
			||||||
 | 
					export class ScrapConfig extends Timestamp {
 | 
				
			||||||
 | 
					  @PrimaryGeneratedColumn('increment')
 | 
				
			||||||
 | 
					  id: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Column({ unique: true })
 | 
				
			||||||
 | 
					  search_url: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Column({ default: 'cisco' })
 | 
				
			||||||
 | 
					  keywords: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Column({ default: true })
 | 
				
			||||||
 | 
					  enable: boolean;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @OneToOne(() => WebBid, (web) => web.scrap_config, { onDelete: 'CASCADE' })
 | 
				
			||||||
 | 
					  @JoinColumn()
 | 
				
			||||||
 | 
					  web_bid: WebBid;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @OneToMany(() => ScrapItem, (web) => web.scrap_config, {
 | 
				
			||||||
 | 
					    onDelete: 'CASCADE',
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					  scrap_items: ScrapItem[];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,44 @@
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  Column,
 | 
				
			||||||
 | 
					  Entity,
 | 
				
			||||||
 | 
					  JoinColumn,
 | 
				
			||||||
 | 
					  ManyToOne,
 | 
				
			||||||
 | 
					  PrimaryGeneratedColumn,
 | 
				
			||||||
 | 
					  Unique,
 | 
				
			||||||
 | 
					} from 'typeorm';
 | 
				
			||||||
 | 
					import { ScrapConfig } from './scrap-config.entity';
 | 
				
			||||||
 | 
					import { Timestamp } from './timestamp';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Entity('scrap-items')
 | 
				
			||||||
 | 
					@Unique(['model', 'scrap_config_id'])
 | 
				
			||||||
 | 
					export class ScrapItem extends Timestamp {
 | 
				
			||||||
 | 
					  @PrimaryGeneratedColumn('increment')
 | 
				
			||||||
 | 
					  id: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Column()
 | 
				
			||||||
 | 
					  name: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Column({ type: 'text' })
 | 
				
			||||||
 | 
					  url: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Column()
 | 
				
			||||||
 | 
					  model: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Column({ nullable: true, default: null, type: 'text' })
 | 
				
			||||||
 | 
					  image_url: string | null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Column({ nullable: true, default: null })
 | 
				
			||||||
 | 
					  keyword: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Column({ nullable: true, default: null, type: 'float' })
 | 
				
			||||||
 | 
					  current_price: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Column()
 | 
				
			||||||
 | 
					  scrap_config_id: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @ManyToOne(() => ScrapConfig, (web) => web.scrap_items, {
 | 
				
			||||||
 | 
					    onDelete: 'CASCADE',
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					  @JoinColumn({ name: 'scrap_config_id' })
 | 
				
			||||||
 | 
					  scrap_config: ScrapConfig;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,8 @@
 | 
				
			||||||
 | 
					import { CreateDateColumn, UpdateDateColumn } from 'typeorm';
 | 
				
			||||||
 | 
					export abstract class Timestamp {
 | 
				
			||||||
 | 
					  @CreateDateColumn({ type: 'timestamp', name: 'created_at' })
 | 
				
			||||||
 | 
					  created_at: Date;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @UpdateDateColumn({ type: 'timestamp', name: 'updated_at' })
 | 
				
			||||||
 | 
					  updated_at: Date;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,28 @@
 | 
				
			||||||
 | 
					import { Module } from '@nestjs/common';
 | 
				
			||||||
 | 
					import { TypeOrmModule } from '@nestjs/typeorm';
 | 
				
			||||||
 | 
					import { ScrapConfig } from './entities/scrap-config.entity';
 | 
				
			||||||
 | 
					import { ScrapItem } from './entities/scrap-item.entity';
 | 
				
			||||||
 | 
					import { ScrapConfigsService } from './services/scrap-config.service';
 | 
				
			||||||
 | 
					import { ScrapConfigsController } from './controllers/scrap-config.controller';
 | 
				
			||||||
 | 
					import { TasksService } from './services/tasks.service';
 | 
				
			||||||
 | 
					import { ScrapItemsService } from './services/scrap-item-config.service';
 | 
				
			||||||
 | 
					import { MailsModule } from '../mails/mails.module';
 | 
				
			||||||
 | 
					import { BidsModule } from '../bids/bids.module';
 | 
				
			||||||
 | 
					import { ClientScrapConfigsController } from './controllers/client/scrap-configs.controller';
 | 
				
			||||||
 | 
					import { ClientScrapItemsController } from './controllers/client/scrap-items.controller';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Module({
 | 
				
			||||||
 | 
					  imports: [
 | 
				
			||||||
 | 
					    TypeOrmModule.forFeature([ScrapConfig, ScrapItem]),
 | 
				
			||||||
 | 
					    MailsModule,
 | 
				
			||||||
 | 
					    BidsModule,
 | 
				
			||||||
 | 
					  ],
 | 
				
			||||||
 | 
					  providers: [ScrapConfigsService, TasksService, ScrapItemsService],
 | 
				
			||||||
 | 
					  exports: [ScrapConfigsService, TasksService, ScrapItemsService],
 | 
				
			||||||
 | 
					  controllers: [
 | 
				
			||||||
 | 
					    ScrapConfigsController,
 | 
				
			||||||
 | 
					    ClientScrapConfigsController,
 | 
				
			||||||
 | 
					    ClientScrapItemsController,
 | 
				
			||||||
 | 
					  ],
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					export class ScrapsModule {}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,54 @@
 | 
				
			||||||
 | 
					import AppResponse from '@/response/app-response';
 | 
				
			||||||
 | 
					import { Injectable } from '@nestjs/common';
 | 
				
			||||||
 | 
					import { InjectRepository } from '@nestjs/typeorm';
 | 
				
			||||||
 | 
					import { plainToClass } from 'class-transformer';
 | 
				
			||||||
 | 
					import { IsNull, Not, Repository } from 'typeorm';
 | 
				
			||||||
 | 
					import { CreateScrapConfigDto } from '../dto/scrap-config/create-scrap-config';
 | 
				
			||||||
 | 
					import { UpdateScrapConfigDto } from '../dto/scrap-config/update-scrap-config';
 | 
				
			||||||
 | 
					import { ScrapConfig } from '../entities/scrap-config.entity';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Injectable()
 | 
				
			||||||
 | 
					export class ScrapConfigsService {
 | 
				
			||||||
 | 
					  constructor(
 | 
				
			||||||
 | 
					    @InjectRepository(ScrapConfig)
 | 
				
			||||||
 | 
					    readonly scrapConfigRepo: Repository<ScrapConfig>,
 | 
				
			||||||
 | 
					  ) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async clientGetScrapeConfigs() {
 | 
				
			||||||
 | 
					    const data = await this.scrapConfigRepo.find({
 | 
				
			||||||
 | 
					      where: {
 | 
				
			||||||
 | 
					        search_url: Not(IsNull()),
 | 
				
			||||||
 | 
					        keywords: Not(IsNull()),
 | 
				
			||||||
 | 
					        enable: true,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      relations: {
 | 
				
			||||||
 | 
					        web_bid: true,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return AppResponse.toResponse(plainToClass(ScrapConfig, data));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async create(data: CreateScrapConfigDto) {
 | 
				
			||||||
 | 
					    const result = await this.scrapConfigRepo.save({
 | 
				
			||||||
 | 
					      search_url: data.search_url,
 | 
				
			||||||
 | 
					      keywords: data.keywords,
 | 
				
			||||||
 | 
					      web_bid: { id: data.web_id },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!result) return AppResponse.toResponse(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return AppResponse.toResponse(true);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async update(
 | 
				
			||||||
 | 
					    id: ScrapConfig['id'],
 | 
				
			||||||
 | 
					    { web_id, ...data }: UpdateScrapConfigDto,
 | 
				
			||||||
 | 
					  ) {
 | 
				
			||||||
 | 
					    const result = await this.scrapConfigRepo.update(id, { ...data });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!result.affected) return AppResponse.toResponse(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return AppResponse.toResponse(true);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,106 @@
 | 
				
			||||||
 | 
					import { WebBidsService } from '@/modules/bids/services/web-bids.service';
 | 
				
			||||||
 | 
					import AppResponse from '@/response/app-response';
 | 
				
			||||||
 | 
					import { BadRequestException, Injectable } from '@nestjs/common';
 | 
				
			||||||
 | 
					import { InjectRepository } from '@nestjs/typeorm';
 | 
				
			||||||
 | 
					import { paginate, PaginateQuery } from 'nestjs-paginate';
 | 
				
			||||||
 | 
					import { Repository } from 'typeorm';
 | 
				
			||||||
 | 
					import { ScrapItem } from '../entities/scrap-item.entity';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Injectable()
 | 
				
			||||||
 | 
					export class ScrapItemsService {
 | 
				
			||||||
 | 
					  constructor(
 | 
				
			||||||
 | 
					    @InjectRepository(ScrapItem)
 | 
				
			||||||
 | 
					    readonly scrapItemRepo: Repository<ScrapItem>,
 | 
				
			||||||
 | 
					    private readonly webService: WebBidsService,
 | 
				
			||||||
 | 
					  ) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async upsertScrapItems(items: ScrapItem[]) {
 | 
				
			||||||
 | 
					    if (!items.length) return { inserted: 0, updated: 0 };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Lấy keys để query bản ghi đã tồn tại
 | 
				
			||||||
 | 
					    const keys = items.map((d) => ({
 | 
				
			||||||
 | 
					      model: d.model,
 | 
				
			||||||
 | 
					      scrap_config_id: d.scrap_config_id,
 | 
				
			||||||
 | 
					    }));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Tìm các bản ghi đã có trong DB
 | 
				
			||||||
 | 
					    const existingItems = await this.scrapItemRepo.findBy(keys);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const toUpdate = [];
 | 
				
			||||||
 | 
					    const toInsert = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    items.forEach((itemData) => {
 | 
				
			||||||
 | 
					      const exist = existingItems.find(
 | 
				
			||||||
 | 
					        (e) =>
 | 
				
			||||||
 | 
					          e.model === itemData.model &&
 | 
				
			||||||
 | 
					          e.scrap_config_id === itemData.scrap_config_id,
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (exist) {
 | 
				
			||||||
 | 
					        toUpdate.push({
 | 
				
			||||||
 | 
					          ...exist,
 | 
				
			||||||
 | 
					          ...itemData,
 | 
				
			||||||
 | 
					          updated_at: new Date(),
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        toInsert.push(itemData);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Thực hiện insert
 | 
				
			||||||
 | 
					    if (toInsert.length) {
 | 
				
			||||||
 | 
					      await this.scrapItemRepo.insert(toInsert);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Thực hiện update
 | 
				
			||||||
 | 
					    if (toUpdate.length) {
 | 
				
			||||||
 | 
					      await this.scrapItemRepo.save(toUpdate);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      inserted: toInsert.length,
 | 
				
			||||||
 | 
					      updated: toUpdate.length,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async upsertScrapItemsRes(items: ScrapItem[]) {
 | 
				
			||||||
 | 
					    const rs = await this.upsertScrapItems(items);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!rs) throw new BadRequestException(AppResponse.toResponse(null));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return AppResponse.toResponse(rs);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async index(query: PaginateQuery) {
 | 
				
			||||||
 | 
					    const { data, meta } = await paginate(query, this.scrapItemRepo, {
 | 
				
			||||||
 | 
					      sortableColumns: ['id'],
 | 
				
			||||||
 | 
					      searchableColumns: ['id'],
 | 
				
			||||||
 | 
					      defaultLimit: 15,
 | 
				
			||||||
 | 
					      filterableColumns: {
 | 
				
			||||||
 | 
					        'scrap_config.web_bid.origin_url': true,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      defaultSortBy: [['updated_at', 'DESC']],
 | 
				
			||||||
 | 
					      maxLimit: 100,
 | 
				
			||||||
 | 
					      relations: { scrap_config: { web_bid: true } },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const sources = await this.webService.webBidRepo.find({
 | 
				
			||||||
 | 
					      select: { origin_url: true },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const newData = data.map(({ scrap_config, ...item }) => {
 | 
				
			||||||
 | 
					      return {
 | 
				
			||||||
 | 
					        ...item,
 | 
				
			||||||
 | 
					        source: scrap_config.web_bid.origin_url,
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return AppResponse.toPaginationAny({
 | 
				
			||||||
 | 
					      data: newData,
 | 
				
			||||||
 | 
					      meta,
 | 
				
			||||||
 | 
					      bonus: {
 | 
				
			||||||
 | 
					        sources: sources.map((item) => item.origin_url),
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,104 @@
 | 
				
			||||||
 | 
					import { ConfigsService } from '@/modules/bids/services/configs.service';
 | 
				
			||||||
 | 
					import { DashboardService } from '@/modules/bids/services/dashboard.service';
 | 
				
			||||||
 | 
					import { MailsService } from '@/modules/mails/services/mails.service';
 | 
				
			||||||
 | 
					import { delay } from '@/ultils';
 | 
				
			||||||
 | 
					import { Injectable, Logger } from '@nestjs/common';
 | 
				
			||||||
 | 
					import { Cron, CronExpression } from '@nestjs/schedule';
 | 
				
			||||||
 | 
					import * as moment from 'moment';
 | 
				
			||||||
 | 
					import { Between } from 'typeorm';
 | 
				
			||||||
 | 
					import { ScrapItemsService } from './scrap-item-config.service';
 | 
				
			||||||
 | 
					import { HotItemApi } from '@/modules/bids/apis/hot-item.api';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Injectable()
 | 
				
			||||||
 | 
					export class TasksService {
 | 
				
			||||||
 | 
					  private readonly logger = new Logger(TasksService.name);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  constructor(
 | 
				
			||||||
 | 
					    private readonly scrapItemsService: ScrapItemsService,
 | 
				
			||||||
 | 
					    private readonly mailsService: MailsService,
 | 
				
			||||||
 | 
					    private readonly configsSerivce: ConfigsService,
 | 
				
			||||||
 | 
					    private readonly dashboardService: DashboardService,
 | 
				
			||||||
 | 
					    private readonly hotItemApi: HotItemApi,
 | 
				
			||||||
 | 
					  ) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async runProcessAndSendReport(processName: string) {
 | 
				
			||||||
 | 
					    const mails = (await this.configsSerivce.getConfig('MAIL_SCRAP_REPORT'))
 | 
				
			||||||
 | 
					      ?.value;
 | 
				
			||||||
 | 
					    if (!mails) {
 | 
				
			||||||
 | 
					      console.warn('No mails configured for report. Skipping.');
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Nếu process đang chạy, không chạy lại
 | 
				
			||||||
 | 
					    const initialStatus =
 | 
				
			||||||
 | 
					      await this.dashboardService.getStatusProcessByName(processName);
 | 
				
			||||||
 | 
					    if (initialStatus === 'online') {
 | 
				
			||||||
 | 
					      console.log(
 | 
				
			||||||
 | 
					        `Process ${processName} is already running. Skipping execution.`,
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Reset và chạy process
 | 
				
			||||||
 | 
					    await this.dashboardService.resetProcessByName(processName);
 | 
				
			||||||
 | 
					    console.log(`Process ${processName} started.`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Đợi process kết thúc, có timeout
 | 
				
			||||||
 | 
					    const maxAttempts = 60; // 10 phút
 | 
				
			||||||
 | 
					    let attempts = 0;
 | 
				
			||||||
 | 
					    let status = 'online';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    while (status === 'online' && attempts < maxAttempts) {
 | 
				
			||||||
 | 
					      await delay(10000); // 10 giây
 | 
				
			||||||
 | 
					      status = await this.dashboardService.getStatusProcessByName(processName);
 | 
				
			||||||
 | 
					      attempts++;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (status === 'online') {
 | 
				
			||||||
 | 
					      console.warn(
 | 
				
			||||||
 | 
					        `Process ${processName} still running after timeout. Skipping report.`,
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Khi process kết thúc => gửi mail
 | 
				
			||||||
 | 
					    const startOfDay = new Date();
 | 
				
			||||||
 | 
					    startOfDay.setHours(0, 0, 0, 0);
 | 
				
			||||||
 | 
					    const endOfDay = new Date();
 | 
				
			||||||
 | 
					    endOfDay.setHours(23, 59, 59, 999);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const data = await this.scrapItemsService.scrapItemRepo.find({
 | 
				
			||||||
 | 
					        where: {
 | 
				
			||||||
 | 
					          updated_at: Between(startOfDay, endOfDay),
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        relations: { scrap_config: { web_bid: true } },
 | 
				
			||||||
 | 
					        order: { updated_at: 'DESC' },
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const hotItems = await this.hotItemApi.listHotItem();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const { hasHotItem, html } = this.mailsService.generateProductTableHTML(
 | 
				
			||||||
 | 
					        data,
 | 
				
			||||||
 | 
					        hotItems,
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await this.mailsService.sendHtmlMailJob({
 | 
				
			||||||
 | 
					        to: mails,
 | 
				
			||||||
 | 
					        subject: `${hasHotItem ? '[HOT ITEMS] ' : ''}Auction Items Matching Your Keywords – Daily Update ${moment().format('YYYY-MM-DD HH:mm')}`,
 | 
				
			||||||
 | 
					        html: html,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      console.log('Report mail sent successfully.');
 | 
				
			||||||
 | 
					    } catch (err) {
 | 
				
			||||||
 | 
					      console.error('Failed to generate or send report:', err);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Thực hiện gọi scrape vào lúc 8h59 (giờ ÚC) mỗi ngày
 | 
				
			||||||
 | 
					  @Cron('59 5 * * *')
 | 
				
			||||||
 | 
					  async handleScraps() {
 | 
				
			||||||
 | 
					    const processName = 'scrape-data-keyword';
 | 
				
			||||||
 | 
					    await this.runProcessAndSendReport(processName);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -25,7 +25,7 @@ export default class AppResponse {
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public static toPagination<M>(
 | 
					  public static toPagination<M>(
 | 
				
			||||||
    { data, meta }: Paginated<M>,
 | 
					    { data, meta, bonus }: Paginated<M> & { bonus?: Record<string, any> },
 | 
				
			||||||
    transform = true,
 | 
					    transform = true,
 | 
				
			||||||
    dtoClass?: new () => M,
 | 
					    dtoClass?: new () => M,
 | 
				
			||||||
  ) {
 | 
					  ) {
 | 
				
			||||||
| 
						 | 
					@ -45,6 +45,33 @@ export default class AppResponse {
 | 
				
			||||||
        last_page: meta.totalPages,
 | 
					        last_page: meta.totalPages,
 | 
				
			||||||
        per_page: meta.itemsPerPage,
 | 
					        per_page: meta.itemsPerPage,
 | 
				
			||||||
        total: meta.totalItems,
 | 
					        total: meta.totalItems,
 | 
				
			||||||
 | 
					        ...bonus,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public static toPaginationAny(
 | 
				
			||||||
 | 
					    { data, meta, bonus }: any,
 | 
				
			||||||
 | 
					    transform = true,
 | 
				
			||||||
 | 
					    dtoClass?: new () => any,
 | 
				
			||||||
 | 
					  ) {
 | 
				
			||||||
 | 
					    const transformedData =
 | 
				
			||||||
 | 
					      transform && dtoClass
 | 
				
			||||||
 | 
					        ? data.map((item) => plainToClass(dtoClass, item))
 | 
				
			||||||
 | 
					        : data;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return this.toResponse(transformedData, {
 | 
				
			||||||
 | 
					      bonus: {
 | 
				
			||||||
 | 
					        current_page: meta.currentPage,
 | 
				
			||||||
 | 
					        from: meta.currentPage * meta.itemsPerPage - (meta.itemsPerPage - 1),
 | 
				
			||||||
 | 
					        to:
 | 
				
			||||||
 | 
					          meta.currentPage * meta.itemsPerPage > meta.totalItems
 | 
				
			||||||
 | 
					            ? meta.totalItems
 | 
				
			||||||
 | 
					            : meta.currentPage * meta.itemsPerPage,
 | 
				
			||||||
 | 
					        last_page: meta.totalPages,
 | 
				
			||||||
 | 
					        per_page: meta.itemsPerPage,
 | 
				
			||||||
 | 
					        total: meta.totalItems,
 | 
				
			||||||
 | 
					        ...bonus,
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -10,6 +10,10 @@ export const excludeAuth = [
 | 
				
			||||||
    path: 'admin/auth/forgot-password',
 | 
					    path: 'admin/auth/forgot-password',
 | 
				
			||||||
    method: RequestMethod.POST,
 | 
					    method: RequestMethod.POST,
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    path: '/admin/bids/record/(.*)',
 | 
				
			||||||
 | 
					    method: RequestMethod.GET,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
] as (string | RouteInfo)[];
 | 
					] as (string | RouteInfo)[];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const clientExcludeAuth = [
 | 
					export const clientExcludeAuth = [
 | 
				
			||||||
| 
						 | 
					@ -18,6 +22,18 @@ export const clientExcludeAuth = [
 | 
				
			||||||
    path: 'auth/generate-token',
 | 
					    path: 'auth/generate-token',
 | 
				
			||||||
    method: RequestMethod.POST,
 | 
					    method: RequestMethod.POST,
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    path: 'extensions/policy',
 | 
				
			||||||
 | 
					    method: RequestMethod.GET,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    path: 'bids/hook-action',
 | 
				
			||||||
 | 
					    method: RequestMethod.POST,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    path: 'scrap-items',
 | 
				
			||||||
 | 
					    method: RequestMethod.GET,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
] as (string | RouteInfo)[];
 | 
					] as (string | RouteInfo)[];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const excludeAuthor = [
 | 
					export const excludeAuthor = [
 | 
				
			||||||
| 
						 | 
					@ -34,4 +50,8 @@ export const excludeAuthor = [
 | 
				
			||||||
    path: 'admin/auth/me',
 | 
					    path: 'admin/auth/me',
 | 
				
			||||||
    method: RequestMethod.GET,
 | 
					    method: RequestMethod.GET,
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    path: '/admin/bids/record/(.*)',
 | 
				
			||||||
 | 
					    method: RequestMethod.GET,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
] as (string | RouteInfo)[];
 | 
					] as (string | RouteInfo)[];
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,3 +1,7 @@
 | 
				
			||||||
 | 
					import { Bid } from '@/modules/bids/entities/bid.entity';
 | 
				
			||||||
 | 
					import { ScrapItem } from '@/modules/scraps/entities/scrap-item.entity';
 | 
				
			||||||
 | 
					import * as moment from 'moment';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function extractModelId(url: string): string | null {
 | 
					export function extractModelId(url: string): string | null {
 | 
				
			||||||
  switch (extractDomain(url)) {
 | 
					  switch (extractDomain(url)) {
 | 
				
			||||||
    case 'https://www.grays.com': {
 | 
					    case 'https://www.grays.com': {
 | 
				
			||||||
| 
						 | 
					@ -16,6 +20,10 @@ export function extractModelId(url: string): string | null {
 | 
				
			||||||
      const model = url.split('/').pop();
 | 
					      const model = url.split('/').pop();
 | 
				
			||||||
      return model ? model : null;
 | 
					      return model ? model : null;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    case 'https://www.allbids.com.au': {
 | 
				
			||||||
 | 
					      const match = url.match(/-(\d+)(?:[\?#]|$)/);
 | 
				
			||||||
 | 
					      return match ? match[1] : null;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -25,6 +33,31 @@ export function subtractMinutes(timeStr: string, minutes: number) {
 | 
				
			||||||
  return date.toISOString(); // Trả về dạng chuẩn ISO
 | 
					  return date.toISOString(); // Trả về dạng chuẩn ISO
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function extractDomainSmart(url: string) {
 | 
				
			||||||
 | 
					  const PUBLIC_SUFFIXES = ['com.au', 'co.uk', 'com.vn', 'org.au', 'gov.uk'];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const hostname = new URL(url).hostname.replace(/^www\./, ''); // remove "www."
 | 
				
			||||||
 | 
					    const parts = hostname.split('.');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for (let i = 0; i < PUBLIC_SUFFIXES.length; i++) {
 | 
				
			||||||
 | 
					      if (hostname.endsWith(PUBLIC_SUFFIXES[i])) {
 | 
				
			||||||
 | 
					        return parts[parts.length - PUBLIC_SUFFIXES[i].split('.').length - 1];
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return parts[parts.length - 2];
 | 
				
			||||||
 | 
					  } catch (e) {
 | 
				
			||||||
 | 
					    return url;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function subtractSeconds(time: string, seconds: number) {
 | 
				
			||||||
 | 
					  const date = new Date(time);
 | 
				
			||||||
 | 
					  date.setSeconds(date.getSeconds() - seconds);
 | 
				
			||||||
 | 
					  return date.toUTCString();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function isTimeReached(targetTime: string) {
 | 
					export function isTimeReached(targetTime: string) {
 | 
				
			||||||
  const targetDate = new Date(targetTime);
 | 
					  const targetDate = new Date(targetTime);
 | 
				
			||||||
  const now = new Date();
 | 
					  const now = new Date();
 | 
				
			||||||
| 
						 | 
					@ -32,6 +65,19 @@ export function isTimeReached(targetTime: string) {
 | 
				
			||||||
  return now >= targetDate;
 | 
					  return now >= targetDate;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function isTimePassedByMinutes(
 | 
				
			||||||
 | 
					  targetTime: string,
 | 
				
			||||||
 | 
					  minutes: number,
 | 
				
			||||||
 | 
					): boolean {
 | 
				
			||||||
 | 
					  const targetDate = new Date(targetTime);
 | 
				
			||||||
 | 
					  const now = new Date();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const diffMs = now.getTime() - targetDate.getTime(); // hiệu số milliseconds
 | 
				
			||||||
 | 
					  const diffMinutes = diffMs / (1000 * 60);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return diffMinutes >= minutes;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function extractDomain(url: string): string | null {
 | 
					export function extractDomain(url: string): string | null {
 | 
				
			||||||
  try {
 | 
					  try {
 | 
				
			||||||
    const parsedUrl = new URL(url);
 | 
					    const parsedUrl = new URL(url);
 | 
				
			||||||
| 
						 | 
					@ -101,3 +147,117 @@ export function verifyCode(content: string) {
 | 
				
			||||||
  // Nếu không tìm thấy mã xác minh, trả về null
 | 
					  // Nếu không tìm thấy mã xác minh, trả về null
 | 
				
			||||||
  return null;
 | 
					  return null;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function shouldResetTool(
 | 
				
			||||||
 | 
					  bids: Bid[],
 | 
				
			||||||
 | 
					  lastResetTime: Date | null,
 | 
				
			||||||
 | 
					  now: Date = new Date(),
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					  const ONE_MINUTE = 60 * 1000;
 | 
				
			||||||
 | 
					  const ONE_HOUR = 60 * ONE_MINUTE;
 | 
				
			||||||
 | 
					  const TWO_HOURS = 2 * ONE_HOUR;
 | 
				
			||||||
 | 
					  const TWENTY_MINUTES = 20 * ONE_MINUTE;
 | 
				
			||||||
 | 
					  const FIVE_MINUTES = 5 * ONE_MINUTE;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Nếu đã reset trong 1 giờ gần đây => không reset
 | 
				
			||||||
 | 
					  if (lastResetTime && now.getTime() - lastResetTime.getTime() < ONE_HOUR) {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      shouldReset: false,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 1. Kiểm tra bid gần nhất có close_time trong vòng 20 phút tới
 | 
				
			||||||
 | 
					  const futureBids = bids
 | 
				
			||||||
 | 
					    .filter((b) => b.close_time)
 | 
				
			||||||
 | 
					    .map((b) => ({
 | 
				
			||||||
 | 
					      ...b,
 | 
				
			||||||
 | 
					      closeTime: new Date(b.close_time!),
 | 
				
			||||||
 | 
					    }))
 | 
				
			||||||
 | 
					    .filter((b) => b.closeTime.getTime() > now.getTime())
 | 
				
			||||||
 | 
					    .sort((a, b) => a.closeTime.getTime() - b.closeTime.getTime());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const closest = futureBids[0];
 | 
				
			||||||
 | 
					  const hasBidCloseSoon =
 | 
				
			||||||
 | 
					    closest && closest.closeTime.getTime() - now.getTime() <= TWENTY_MINUTES;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (hasBidCloseSoon) {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      shouldReset: true,
 | 
				
			||||||
 | 
					      reason: 'Bid close_time is within 20 minutes',
 | 
				
			||||||
 | 
					      bidId: closest.id,
 | 
				
			||||||
 | 
					      closeTime: closest.close_time,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 2. Kiểm tra bid chưa có lot_id hoặc close_time, được tạo > 5 phút và cách reset trước > 2 tiếng
 | 
				
			||||||
 | 
					  // Đồng thời không có bid nào sắp close trong 20 phút tới
 | 
				
			||||||
 | 
					  if (!hasBidCloseSoon) {
 | 
				
			||||||
 | 
					    for (const bid of bids) {
 | 
				
			||||||
 | 
					      const createdAt = new Date(bid.created_at);
 | 
				
			||||||
 | 
					      if (
 | 
				
			||||||
 | 
					        (!bid.lot_id || !bid.close_time) &&
 | 
				
			||||||
 | 
					        now.getTime() - createdAt.getTime() > FIVE_MINUTES &&
 | 
				
			||||||
 | 
					        (!lastResetTime || now.getTime() - lastResetTime.getTime() > TWO_HOURS)
 | 
				
			||||||
 | 
					      ) {
 | 
				
			||||||
 | 
					        return {
 | 
				
			||||||
 | 
					          shouldReset: true,
 | 
				
			||||||
 | 
					          reason:
 | 
				
			||||||
 | 
					            'Bid is missing info and older than 5 mins, last reset > 2h, and no urgent bids',
 | 
				
			||||||
 | 
					          bidId: bid.id,
 | 
				
			||||||
 | 
					          closeTime: bid.close_time,
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    shouldReset: false,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function extractNumber(str: string) {
 | 
				
			||||||
 | 
					  const match = str.match(/\d+(\.\d+)?/);
 | 
				
			||||||
 | 
					  return match ? parseFloat(match[0]) : null;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function formatEndTime(
 | 
				
			||||||
 | 
					  closeTime: string | Date,
 | 
				
			||||||
 | 
					  extended: boolean,
 | 
				
			||||||
 | 
					): string {
 | 
				
			||||||
 | 
					  return `${moment(closeTime).format('YYYY-MM-DD HH:mm')} (${extended ? 'extended' : 'no extension'})`;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const delay = (ms: number) =>
 | 
				
			||||||
 | 
					  new Promise((resolve) => setTimeout(resolve, ms));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const isHotItemFn = (item: ScrapItem, hotItems: any[]) => {
 | 
				
			||||||
 | 
					  return hotItems.some((obj) =>
 | 
				
			||||||
 | 
					    item.name.toLowerCase().includes(obj.name.toLowerCase()),
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function parseVideoFileName(fileName: string) {
 | 
				
			||||||
 | 
					  // Loại bỏ phần mở rộng .mp4
 | 
				
			||||||
 | 
					  const nameWithoutExt = fileName.replace(/\.mp4$/, '');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Tách phần ID đầu tiên
 | 
				
			||||||
 | 
					  const firstUnderscoreIndex = nameWithoutExt.indexOf('_');
 | 
				
			||||||
 | 
					  const id = nameWithoutExt.slice(0, firstUnderscoreIndex);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Tách phần timestamp cuối cùng
 | 
				
			||||||
 | 
					  const lastUnderscoreIndex = nameWithoutExt.lastIndexOf('_');
 | 
				
			||||||
 | 
					  const timestamp = nameWithoutExt.slice(lastUnderscoreIndex + 1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Tên sản phẩm nằm giữa 2 phần còn lại
 | 
				
			||||||
 | 
					  const name = nameWithoutExt.slice(
 | 
				
			||||||
 | 
					    firstUnderscoreIndex + 1,
 | 
				
			||||||
 | 
					    lastUnderscoreIndex,
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    bid_id: Number(id),
 | 
				
			||||||
 | 
					    name,
 | 
				
			||||||
 | 
					    original_name: fileName,
 | 
				
			||||||
 | 
					    timestamp: Number(timestamp),
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,9 +1,18 @@
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
  "moduleFileExtensions": ["js", "json", "ts"],
 | 
					  "moduleFileExtensions": ["ts", "js", "json"],
 | 
				
			||||||
  "rootDir": ".",
 | 
					  "rootDir": ".",
 | 
				
			||||||
  "testEnvironment": "node",
 | 
					  "testEnvironment": "node",
 | 
				
			||||||
  "testRegex": ".e2e-spec.ts$",
 | 
					  "preset": "ts-jest",
 | 
				
			||||||
  "transform": {
 | 
					  "transform": {
 | 
				
			||||||
    "^.+\\.(t|j)s$": "ts-jest"
 | 
					    "^.+\\.(t|j)s$": "ts-jest"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "testMatch": ["**/?(*.)+(spec|test).ts"],
 | 
				
			||||||
 | 
					  "moduleNameMapper": {
 | 
				
			||||||
 | 
					    "^@/(.*)$": "<rootDir>/src/$1"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "globals": {
 | 
				
			||||||
 | 
					    "ts-jest": {
 | 
				
			||||||
 | 
					      "tsconfig": "<rootDir>/tsconfig.json"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -13,11 +13,13 @@ import browser from "./system/browser.js";
 | 
				
			||||||
import configs from "./system/config.js";
 | 
					import configs from "./system/config.js";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  delay,
 | 
					  delay,
 | 
				
			||||||
  findNearestClosingChild,
 | 
					  extractModelId,
 | 
				
			||||||
  isTimeReached,
 | 
					  isTimeReached,
 | 
				
			||||||
 | 
					  randomDelayWithMeta,
 | 
				
			||||||
  safeClosePage,
 | 
					  safeClosePage,
 | 
				
			||||||
  subtractSeconds,
 | 
					  subtractSeconds,
 | 
				
			||||||
} from "./system/utils.js";
 | 
					} from "./system/utils.js";
 | 
				
			||||||
 | 
					import CONSTANTS from "./system/constants.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
global.IS_CLEANING = true;
 | 
					global.IS_CLEANING = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -146,6 +148,11 @@ const tracking = async () => {
 | 
				
			||||||
      Promise.allSettled(
 | 
					      Promise.allSettled(
 | 
				
			||||||
        productTabs.map(async (productTab) => {
 | 
					        productTabs.map(async (productTab) => {
 | 
				
			||||||
          console.log(`📌 Processing Product ID: ${productTab.id}`);
 | 
					          console.log(`📌 Processing Product ID: ${productTab.id}`);
 | 
				
			||||||
 | 
					          console.log(
 | 
				
			||||||
 | 
					            `🔍 [${
 | 
				
			||||||
 | 
					              productTab.id
 | 
				
			||||||
 | 
					            }] Current URL: ${await productTab.page_context?.url?.()}`
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          // Xác định parent context
 | 
					          // Xác định parent context
 | 
				
			||||||
          if (!productTab.parent_browser_context) {
 | 
					          if (!productTab.parent_browser_context) {
 | 
				
			||||||
| 
						 | 
					@ -162,7 +169,7 @@ const tracking = async () => {
 | 
				
			||||||
          // Thời điểm tracking liên tục
 | 
					          // Thời điểm tracking liên tục
 | 
				
			||||||
          const earlyTrackingTime = subtractSeconds(
 | 
					          const earlyTrackingTime = subtractSeconds(
 | 
				
			||||||
            productTab.close_time,
 | 
					            productTab.close_time,
 | 
				
			||||||
            productTab?.web_bid?.early_tracking_seconds || 0
 | 
					            productTab.getEarlyTrackingSeconds() || 0
 | 
				
			||||||
          );
 | 
					          );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          // Check không mở tab nếu chưa đến giờ
 | 
					          // Check không mở tab nếu chưa đến giờ
 | 
				
			||||||
| 
						 | 
					@ -184,6 +191,8 @@ const tracking = async () => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          // Kiểm tra URL và điều hướng nếu cần
 | 
					          // Kiểm tra URL và điều hướng nếu cần
 | 
				
			||||||
          if ((await productTab.page_context.url()) !== productTab.url) {
 | 
					          if ((await productTab.page_context.url()) !== productTab.url) {
 | 
				
			||||||
 | 
					            if (global[`IS_PLACE_BID-${productTab.id}`]) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            console.log(
 | 
					            console.log(
 | 
				
			||||||
              `🔄 Redirecting to new URL for Product ID: ${productTab.id}`
 | 
					              `🔄 Redirecting to new URL for Product ID: ${productTab.id}`
 | 
				
			||||||
            );
 | 
					            );
 | 
				
			||||||
| 
						 | 
					@ -219,8 +228,6 @@ const tracking = async () => {
 | 
				
			||||||
            return;
 | 
					            return;
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          // Thực thi hành động
 | 
					 | 
				
			||||||
          console.log(`🚀 Executing action for Product ID: ${productTab.id}`);
 | 
					 | 
				
			||||||
          await productTab.action();
 | 
					          await productTab.action();
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
| 
						 | 
					@ -272,21 +279,6 @@ const clearLazyTab = async () => {
 | 
				
			||||||
    // product tabs
 | 
					    // product tabs
 | 
				
			||||||
    const productTabs = _.flatMap(MANAGER_BIDS, "children");
 | 
					    const productTabs = _.flatMap(MANAGER_BIDS, "children");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // for (const item of [...productTabs, ...MANAGER_BIDS]) {
 | 
					 | 
				
			||||||
    //   if (!item.page_context) continue;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    //   try {
 | 
					 | 
				
			||||||
    //     const avalableResult = await isPageAvailable(item.page_context);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    //     if (!avalableResult) {
 | 
					 | 
				
			||||||
    //       await safeClosePage(item);
 | 
					 | 
				
			||||||
    //     }
 | 
					 | 
				
			||||||
    //   } catch (e) {
 | 
					 | 
				
			||||||
    //     console.warn("⚠️ Error checking page_context.title()", e.message);
 | 
					 | 
				
			||||||
    //     await safeClosePage(item);
 | 
					 | 
				
			||||||
    //   }
 | 
					 | 
				
			||||||
    // }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    for (const page of pages) {
 | 
					    for (const page of pages) {
 | 
				
			||||||
      try {
 | 
					      try {
 | 
				
			||||||
        if (page.isClosed()) continue; // Trang đã đóng thì bỏ qua
 | 
					        if (page.isClosed()) continue; // Trang đã đóng thì bỏ qua
 | 
				
			||||||
| 
						 | 
					@ -302,12 +294,12 @@ const clearLazyTab = async () => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          const earlyTrackingTime = subtractSeconds(
 | 
					          const earlyTrackingTime = subtractSeconds(
 | 
				
			||||||
            productTab.close_time,
 | 
					            productTab.close_time,
 | 
				
			||||||
            productTab?.web_bid?.early_tracking_seconds || 0
 | 
					            productTab.getEarlyTrackingSeconds() || 0
 | 
				
			||||||
          );
 | 
					          );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          if (!isTimeReached(earlyTrackingTime)) {
 | 
					          if (!isTimeReached(earlyTrackingTime)) {
 | 
				
			||||||
            await safeClosePage(productTab);
 | 
					            await safeClosePage(productTab);
 | 
				
			||||||
            console.log(`🛑 Unused page detected: ${pageUrl}`);
 | 
					            console.log(`🛑 Unused page detectedd: ${pageUrl}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            continue;
 | 
					            continue;
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
| 
						 | 
					@ -315,11 +307,32 @@ const clearLazyTab = async () => {
 | 
				
			||||||
          continue;
 | 
					          continue;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const modelProductTab = extractModelId(pageUrl);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (modelProductTab) {
 | 
				
			||||||
 | 
					          const productWatingUpdate = productTabs.find(
 | 
				
			||||||
 | 
					            (item) =>
 | 
				
			||||||
 | 
					              item.model === modelProductTab &&
 | 
				
			||||||
 | 
					              isTimeReached(item.close_time) &&
 | 
				
			||||||
 | 
					              item.status === "biding"
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          if (productWatingUpdate) {
 | 
				
			||||||
 | 
					            console.log("Waiting product update to close");
 | 
				
			||||||
 | 
					            continue;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // remove all listents
 | 
					        // remove all listents
 | 
				
			||||||
        page.removeAllListeners();
 | 
					        page.removeAllListeners();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        console.log(`🛑 Unused page detected: ${pageUrl}`);
 | 
					        console.log(`🛑 Unused page detected: ${pageUrl}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (CONSTANTS.KEEP_LIVE_URLS.some((item) => pageUrl.includes(item))) {
 | 
				
			||||||
 | 
					          console.log("Url include keep live urls: " + pageUrl);
 | 
				
			||||||
 | 
					          continue;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const bidData = MANAGER_BIDS.filter((item) => item.page_context)
 | 
					        const bidData = MANAGER_BIDS.filter((item) => item.page_context)
 | 
				
			||||||
          .map((i) => ({
 | 
					          .map((i) => ({
 | 
				
			||||||
            current_url: i.page_context.url(),
 | 
					            current_url: i.page_context.url(),
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
Some files were not shown because too many files have changed in this diff Show More
		Loading…
	
		Reference in New Issue