Compare commits
10 Commits
de791f87ce
...
fb9a9dfba2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fb9a9dfba2 | ||
|
|
326b9c8ec1 | ||
|
|
de4c725554 | ||
|
|
5044aa7573 | ||
|
|
d26feb396f | ||
|
|
a45f4b151c | ||
|
|
f8aa8eb595 | ||
|
|
47f09b3c4e | ||
|
|
754244345a | ||
|
|
f38056fc3f |
513
package-lock.json
generated
513
package-lock.json
generated
@ -54,6 +54,7 @@
|
|||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"mermaid": "^11.12.2",
|
"mermaid": "^11.12.2",
|
||||||
|
"minio": "^8.0.6",
|
||||||
"ng-otp-input": "^2.0.9",
|
"ng-otp-input": "^2.0.9",
|
||||||
"ng2-charts": "^8.0.0",
|
"ng2-charts": "^8.0.0",
|
||||||
"ngx-countup": "^13.2.0",
|
"ngx-countup": "^13.2.0",
|
||||||
@ -74,6 +75,8 @@
|
|||||||
"@types/jquery": "^3.5.33",
|
"@types/jquery": "^3.5.33",
|
||||||
"@types/leaflet": "^1.9.21",
|
"@types/leaflet": "^1.9.21",
|
||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
|
"@types/node": "^25.0.3",
|
||||||
|
"baseline-browser-mapping": "^2.9.11",
|
||||||
"jasmine-core": "~5.12.0",
|
"jasmine-core": "~5.12.0",
|
||||||
"karma": "~6.4.4",
|
"karma": "~6.4.4",
|
||||||
"karma-chrome-launcher": "~3.2.0",
|
"karma-chrome-launcher": "~3.2.0",
|
||||||
@ -4697,9 +4700,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "24.9.1",
|
"version": "25.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz",
|
||||||
"integrity": "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==",
|
"integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -4746,6 +4749,13 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BSD-2-Clause"
|
"license": "BSD-2-Clause"
|
||||||
},
|
},
|
||||||
|
"node_modules/@zxing/text-encoding": {
|
||||||
|
"version": "0.9.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@zxing/text-encoding/-/text-encoding-0.9.0.tgz",
|
||||||
|
"integrity": "sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==",
|
||||||
|
"license": "(Unlicense OR Apache-2.0)",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"node_modules/abbrev": {
|
"node_modules/abbrev": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.1.tgz",
|
||||||
@ -4953,6 +4963,27 @@
|
|||||||
"url": "https://github.com/sponsors/jonschlinkert"
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/async": {
|
||||||
|
"version": "3.2.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
|
||||||
|
"integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/available-typed-arrays": {
|
||||||
|
"version": "1.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
|
||||||
|
"integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"possible-typed-array-names": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/balanced-match": {
|
"node_modules/balanced-match": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||||
@ -4971,9 +5002,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/baseline-browser-mapping": {
|
"node_modules/baseline-browser-mapping": {
|
||||||
"version": "2.8.19",
|
"version": "2.9.11",
|
||||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.19.tgz",
|
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.11.tgz",
|
||||||
"integrity": "sha512-zoKGUdu6vb2jd3YOq0nnhEDQVbPcHhco3UImJrv5dSkvxTc2pl2WjOPsjZXDwPDSl5eghIMuY3R6J9NDKF3KcQ==",
|
"integrity": "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
"baseline-browser-mapping": "dist/cli.js"
|
"baseline-browser-mapping": "dist/cli.js"
|
||||||
@ -5012,6 +5043,29 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/block-stream2": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/block-stream2/-/block-stream2-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-suhjmLI57Ewpmq00qaygS8UgEq2ly2PCItenIyhMqVjo4t4pGzqMvfgJuX8iWTeSDdfSSqS6j38fL4ToNL7Pfg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"readable-stream": "^3.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/block-stream2/node_modules/readable-stream": {
|
||||||
|
"version": "3.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||||
|
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"inherits": "^2.0.3",
|
||||||
|
"string_decoder": "^1.1.1",
|
||||||
|
"util-deprecate": "^1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/body-parser": {
|
"node_modules/body-parser": {
|
||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz",
|
||||||
@ -5096,6 +5150,12 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/browser-or-node": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/browser-or-node/-/browser-or-node-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-8CVjaLJGuSKMVTxJ2DpBl5XnlNDiT4cQFeuCJJrvJmts9YrTZDizTX7PjC2s6W4x+MBGZeEY6dGMrF04/6Hgqg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/browserslist": {
|
"node_modules/browserslist": {
|
||||||
"version": "4.27.0",
|
"version": "4.27.0",
|
||||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.27.0.tgz",
|
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.27.0.tgz",
|
||||||
@ -5129,6 +5189,15 @@
|
|||||||
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/buffer-crc32": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/buffer-from": {
|
"node_modules/buffer-from": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||||
@ -5261,11 +5330,28 @@
|
|||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/call-bind": {
|
||||||
|
"version": "1.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
|
||||||
|
"integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind-apply-helpers": "^1.0.0",
|
||||||
|
"es-define-property": "^1.0.0",
|
||||||
|
"get-intrinsic": "^1.2.4",
|
||||||
|
"set-function-length": "^1.2.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/call-bind-apply-helpers": {
|
"node_modules/call-bind-apply-helpers": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||||
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"es-errors": "^1.3.0",
|
"es-errors": "^1.3.0",
|
||||||
@ -5279,7 +5365,6 @@
|
|||||||
"version": "1.0.4",
|
"version": "1.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
|
||||||
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
|
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"call-bind-apply-helpers": "^1.0.2",
|
"call-bind-apply-helpers": "^1.0.2",
|
||||||
@ -6451,6 +6536,32 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/decode-uri-component": {
|
||||||
|
"version": "0.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz",
|
||||||
|
"integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/define-data-property": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
|
||||||
|
"integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-define-property": "^1.0.0",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"gopd": "^1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/delaunator": {
|
"node_modules/delaunator": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz",
|
||||||
@ -6590,7 +6701,6 @@
|
|||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"call-bind-apply-helpers": "^1.0.1",
|
"call-bind-apply-helpers": "^1.0.1",
|
||||||
@ -6821,7 +6931,6 @@
|
|||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
@ -6831,7 +6940,6 @@
|
|||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
@ -6841,7 +6949,6 @@
|
|||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||||
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"es-errors": "^1.3.0"
|
"es-errors": "^1.3.0"
|
||||||
@ -7057,6 +7164,24 @@
|
|||||||
],
|
],
|
||||||
"license": "BSD-3-Clause"
|
"license": "BSD-3-Clause"
|
||||||
},
|
},
|
||||||
|
"node_modules/fast-xml-parser": {
|
||||||
|
"version": "4.5.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.3.tgz",
|
||||||
|
"integrity": "sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/NaturalIntelligence"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"strnum": "^1.1.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"fxparser": "src/cli/cli.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fdir": {
|
"node_modules/fdir": {
|
||||||
"version": "6.5.0",
|
"version": "6.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||||
@ -7087,6 +7212,15 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/filter-obj": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/finalhandler": {
|
"node_modules/finalhandler": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz",
|
||||||
@ -7139,6 +7273,21 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/for-each": {
|
||||||
|
"version": "0.3.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
|
||||||
|
"integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"is-callable": "^1.2.7"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/foreground-child": {
|
"node_modules/foreground-child": {
|
||||||
"version": "3.3.1",
|
"version": "3.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
|
||||||
@ -7230,7 +7379,6 @@
|
|||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
@ -7245,6 +7393,15 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/generator-function": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/gensync": {
|
"node_modules/gensync": {
|
||||||
"version": "1.0.0-beta.2",
|
"version": "1.0.0-beta.2",
|
||||||
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
|
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
|
||||||
@ -7279,7 +7436,6 @@
|
|||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||||
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"call-bind-apply-helpers": "^1.0.2",
|
"call-bind-apply-helpers": "^1.0.2",
|
||||||
@ -7304,7 +7460,6 @@
|
|||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||||
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"dunder-proto": "^1.0.1",
|
"dunder-proto": "^1.0.1",
|
||||||
@ -7360,7 +7515,6 @@
|
|||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
@ -7392,11 +7546,22 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/has-property-descriptors": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-define-property": "^1.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/has-symbols": {
|
"node_modules/has-symbols": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
@ -7409,7 +7574,6 @@
|
|||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"has-symbols": "^1.0.3"
|
"has-symbols": "^1.0.3"
|
||||||
@ -7425,7 +7589,6 @@
|
|||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||||
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"function-bind": "^1.1.2"
|
"function-bind": "^1.1.2"
|
||||||
@ -7700,6 +7863,22 @@
|
|||||||
"node": ">= 0.10"
|
"node": ">= 0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-arguments": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bound": "^1.0.2",
|
||||||
|
"has-tostringtag": "^1.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-binary-path": {
|
"node_modules/is-binary-path": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
||||||
@ -7713,6 +7892,18 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-callable": {
|
||||||
|
"version": "1.2.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
|
||||||
|
"integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-core-module": {
|
"node_modules/is-core-module": {
|
||||||
"version": "2.16.1",
|
"version": "2.16.1",
|
||||||
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
|
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
|
||||||
@ -7752,6 +7943,25 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-generator-function": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bound": "^1.0.4",
|
||||||
|
"generator-function": "^2.0.0",
|
||||||
|
"get-proto": "^1.0.1",
|
||||||
|
"has-tostringtag": "^1.0.2",
|
||||||
|
"safe-regex-test": "^1.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-glob": {
|
"node_modules/is-glob": {
|
||||||
"version": "4.0.3",
|
"version": "4.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
||||||
@ -7799,7 +8009,6 @@
|
|||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
|
||||||
"integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==",
|
"integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"call-bound": "^1.0.2",
|
"call-bound": "^1.0.2",
|
||||||
@ -7814,6 +8023,21 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-typed-array": {
|
||||||
|
"version": "1.1.15",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz",
|
||||||
|
"integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"which-typed-array": "^1.1.16"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-unicode-supported": {
|
"node_modules/is-unicode-supported": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz",
|
||||||
@ -8917,7 +9141,6 @@
|
|||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
@ -9075,6 +9298,67 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/minio": {
|
||||||
|
"version": "8.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/minio/-/minio-8.0.6.tgz",
|
||||||
|
"integrity": "sha512-sOeh2/b/XprRmEtYsnNRFtOqNRTPDvYtMWh+spWlfsuCV/+IdxNeKVUMKLqI7b5Dr07ZqCPuaRGU/rB9pZYVdQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"async": "^3.2.4",
|
||||||
|
"block-stream2": "^2.1.0",
|
||||||
|
"browser-or-node": "^2.1.1",
|
||||||
|
"buffer-crc32": "^1.0.0",
|
||||||
|
"eventemitter3": "^5.0.1",
|
||||||
|
"fast-xml-parser": "^4.4.1",
|
||||||
|
"ipaddr.js": "^2.0.1",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
|
"mime-types": "^2.1.35",
|
||||||
|
"query-string": "^7.1.3",
|
||||||
|
"stream-json": "^1.8.0",
|
||||||
|
"through2": "^4.0.2",
|
||||||
|
"web-encoding": "^1.1.5",
|
||||||
|
"xml2js": "^0.5.0 || ^0.6.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^16 || ^18 || >=20"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/minio/node_modules/eventemitter3": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/minio/node_modules/ipaddr.js": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/minio/node_modules/mime-db": {
|
||||||
|
"version": "1.52.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||||
|
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/minio/node_modules/mime-types": {
|
||||||
|
"version": "2.1.35",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||||
|
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-db": "1.52.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/minipass": {
|
"node_modules/minipass": {
|
||||||
"version": "7.1.2",
|
"version": "7.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
|
||||||
@ -10182,6 +10466,15 @@
|
|||||||
"points-on-curve": "0.2.0"
|
"points-on-curve": "0.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/possible-typed-array-names": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
"version": "8.5.6",
|
"version": "8.5.6",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
||||||
@ -10320,6 +10613,24 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/query-string": {
|
||||||
|
"version": "7.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz",
|
||||||
|
"integrity": "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"decode-uri-component": "^0.2.2",
|
||||||
|
"filter-obj": "^1.1.0",
|
||||||
|
"split-on-first": "^1.0.0",
|
||||||
|
"strict-uri-encode": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/quill": {
|
"node_modules/quill": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/quill/-/quill-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/quill/-/quill-2.0.3.tgz",
|
||||||
@ -10639,7 +10950,6 @@
|
|||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz",
|
||||||
"integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==",
|
"integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"call-bound": "^1.0.2",
|
"call-bound": "^1.0.2",
|
||||||
@ -10680,6 +10990,12 @@
|
|||||||
"@parcel/watcher": "^2.4.1"
|
"@parcel/watcher": "^2.4.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/sax": {
|
||||||
|
"version": "1.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/sax/-/sax-1.4.3.tgz",
|
||||||
|
"integrity": "sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ==",
|
||||||
|
"license": "BlueOak-1.0.0"
|
||||||
|
},
|
||||||
"node_modules/semver": {
|
"node_modules/semver": {
|
||||||
"version": "7.7.2",
|
"version": "7.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
|
||||||
@ -10732,6 +11048,23 @@
|
|||||||
"node": ">= 18"
|
"node": ">= 18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/set-function-length": {
|
||||||
|
"version": "1.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
|
||||||
|
"integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"define-data-property": "^1.1.4",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"function-bind": "^1.1.2",
|
||||||
|
"get-intrinsic": "^1.2.4",
|
||||||
|
"gopd": "^1.0.1",
|
||||||
|
"has-property-descriptors": "^1.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/setimmediate": {
|
"node_modules/setimmediate": {
|
||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
|
||||||
@ -11179,6 +11512,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "CC0-1.0"
|
"license": "CC0-1.0"
|
||||||
},
|
},
|
||||||
|
"node_modules/split-on-first": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ssri": {
|
"node_modules/ssri": {
|
||||||
"version": "12.0.0",
|
"version": "12.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/ssri/-/ssri-12.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/ssri/-/ssri-12.0.0.tgz",
|
||||||
@ -11215,6 +11557,21 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/stream-chain": {
|
||||||
|
"version": "2.2.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/stream-chain/-/stream-chain-2.2.5.tgz",
|
||||||
|
"integrity": "sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/stream-json": {
|
||||||
|
"version": "1.9.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/stream-json/-/stream-json-1.9.1.tgz",
|
||||||
|
"integrity": "sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"stream-chain": "^2.2.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/streamroller": {
|
"node_modules/streamroller": {
|
||||||
"version": "3.1.5",
|
"version": "3.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.1.5.tgz",
|
||||||
@ -11230,6 +11587,15 @@
|
|||||||
"node": ">=8.0"
|
"node": ">=8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/strict-uri-encode": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/string_decoder": {
|
"node_modules/string_decoder": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||||
@ -11357,6 +11723,18 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/strnum": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/NaturalIntelligence"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/stylis": {
|
"node_modules/stylis": {
|
||||||
"version": "4.3.6",
|
"version": "4.3.6",
|
||||||
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz",
|
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz",
|
||||||
@ -11490,6 +11868,29 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/through2": {
|
||||||
|
"version": "4.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz",
|
||||||
|
"integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"readable-stream": "3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/through2/node_modules/readable-stream": {
|
||||||
|
"version": "3.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||||
|
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"inherits": "^2.0.3",
|
||||||
|
"string_decoder": "^1.1.1",
|
||||||
|
"util-deprecate": "^1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tinyexec": {
|
"node_modules/tinyexec": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz",
|
||||||
@ -11743,6 +12144,19 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/util": {
|
||||||
|
"version": "0.12.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz",
|
||||||
|
"integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"inherits": "^2.0.3",
|
||||||
|
"is-arguments": "^1.0.4",
|
||||||
|
"is-generator-function": "^1.0.7",
|
||||||
|
"is-typed-array": "^1.1.3",
|
||||||
|
"which-typed-array": "^1.1.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/util-deprecate": {
|
"node_modules/util-deprecate": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
@ -11985,6 +12399,18 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
|
"node_modules/web-encoding": {
|
||||||
|
"version": "1.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/web-encoding/-/web-encoding-1.1.5.tgz",
|
||||||
|
"integrity": "sha512-HYLeVCdJ0+lBYV2FvNZmv3HJ2Nt0QYXqZojk3d9FJOLkwnuhzM9tmamh8d7HPM8QqjKH8DeHkFTx+CFlWpZZDA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"util": "^0.12.3"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@zxing/text-encoding": "0.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/which": {
|
"node_modules/which": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||||
@ -12001,6 +12427,27 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/which-typed-array": {
|
||||||
|
"version": "1.1.19",
|
||||||
|
"resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz",
|
||||||
|
"integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"available-typed-arrays": "^1.0.7",
|
||||||
|
"call-bind": "^1.0.8",
|
||||||
|
"call-bound": "^1.0.4",
|
||||||
|
"for-each": "^0.3.5",
|
||||||
|
"get-proto": "^1.0.1",
|
||||||
|
"gopd": "^1.2.0",
|
||||||
|
"has-tostringtag": "^1.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/wrap-ansi": {
|
"node_modules/wrap-ansi": {
|
||||||
"version": "6.2.0",
|
"version": "6.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
|
||||||
@ -12206,6 +12653,28 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/xml2js": {
|
||||||
|
"version": "0.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz",
|
||||||
|
"integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"sax": ">=0.6.0",
|
||||||
|
"xmlbuilder": "~11.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/xmlbuilder": {
|
||||||
|
"version": "11.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
|
||||||
|
"integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/y18n": {
|
"node_modules/y18n": {
|
||||||
"version": "5.0.8",
|
"version": "5.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
||||||
|
|||||||
@ -57,6 +57,7 @@
|
|||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"mermaid": "^11.12.2",
|
"mermaid": "^11.12.2",
|
||||||
|
"minio": "^8.0.6",
|
||||||
"ng-otp-input": "^2.0.9",
|
"ng-otp-input": "^2.0.9",
|
||||||
"ng2-charts": "^8.0.0",
|
"ng2-charts": "^8.0.0",
|
||||||
"ngx-countup": "^13.2.0",
|
"ngx-countup": "^13.2.0",
|
||||||
@ -77,6 +78,8 @@
|
|||||||
"@types/jquery": "^3.5.33",
|
"@types/jquery": "^3.5.33",
|
||||||
"@types/leaflet": "^1.9.21",
|
"@types/leaflet": "^1.9.21",
|
||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
|
"@types/node": "^25.0.3",
|
||||||
|
"baseline-browser-mapping": "^2.9.11",
|
||||||
"jasmine-core": "~5.12.0",
|
"jasmine-core": "~5.12.0",
|
||||||
"karma": "~6.4.4",
|
"karma": "~6.4.4",
|
||||||
"karma-chrome-launcher": "~3.2.0",
|
"karma-chrome-launcher": "~3.2.0",
|
||||||
|
|||||||
616
src/app/app.scss
616
src/app/app.scss
@ -646,3 +646,619 @@
|
|||||||
font-size: 0.8125rem;
|
font-size: 0.8125rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== STYLES POUR LA GESTION DES LOGOS ====================
|
||||||
|
// merchant-config.component.scss
|
||||||
|
|
||||||
|
// Variables
|
||||||
|
$logo-size-sm: 48px;
|
||||||
|
$logo-size-md: 80px;
|
||||||
|
$logo-size-lg: 120px;
|
||||||
|
$logo-size-xl: 200px;
|
||||||
|
|
||||||
|
$border-radius-sm: 0.25rem;
|
||||||
|
$border-radius-md: 0.5rem;
|
||||||
|
$border-radius-lg: 0.75rem;
|
||||||
|
|
||||||
|
$transition-base: all 0.3s ease;
|
||||||
|
|
||||||
|
// ==================== UPLOAD DE LOGO ====================
|
||||||
|
.logo-upload-section {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
|
||||||
|
.logo-upload-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-preview-area {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-preview {
|
||||||
|
position: relative;
|
||||||
|
width: 200px;
|
||||||
|
height: 200px;
|
||||||
|
border: 2px solid #e9ecef;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: #f8f9fa;
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-remove-preview {
|
||||||
|
position: absolute;
|
||||||
|
top: 0.5rem;
|
||||||
|
right: 0.5rem;
|
||||||
|
background: rgba(220, 53, 69, 0.9);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(220, 53, 69, 1);
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
i {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-placeholder {
|
||||||
|
width: 200px;
|
||||||
|
height: 200px;
|
||||||
|
border: 2px dashed #dee2e6;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #6c757d;
|
||||||
|
background: #f8f9fa;
|
||||||
|
|
||||||
|
i {
|
||||||
|
opacity: 0.5;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-upload-actions {
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
.btn-select-logo {
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-progress {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.75rem;
|
||||||
|
background: #e7f3ff;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
|
||||||
|
.spinner-border {
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== ÉDITION DE LOGO ====================
|
||||||
|
.logo-edit-section {
|
||||||
|
.logo-edit-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-display-area {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-preview {
|
||||||
|
position: relative;
|
||||||
|
width: 150px;
|
||||||
|
height: 150px;
|
||||||
|
border: 2px solid #e9ecef;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: #f8f9fa;
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-badge {
|
||||||
|
position: absolute;
|
||||||
|
top: 0.5rem;
|
||||||
|
left: 0.5rem;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
|
&.badge-new {
|
||||||
|
background: #28a745;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.badge-current {
|
||||||
|
background: #007bff;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-remove-preview {
|
||||||
|
position: absolute;
|
||||||
|
top: 0.5rem;
|
||||||
|
right: 0.5rem;
|
||||||
|
background: rgba(220, 53, 69, 0.9);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(220, 53, 69, 1);
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-placeholder {
|
||||||
|
width: 150px;
|
||||||
|
height: 150px;
|
||||||
|
border: 2px dashed #dee2e6;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #6c757d;
|
||||||
|
background: #f8f9fa;
|
||||||
|
|
||||||
|
i {
|
||||||
|
opacity: 0.5;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-edit-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
.btn-change-logo,
|
||||||
|
.btn-remove-logo {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== AFFICHAGE DES LOGOS DANS LA LISTE ====================
|
||||||
|
.merchant-logo {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.merchant-logo-large {
|
||||||
|
max-width: 200px;
|
||||||
|
max-height: 200px;
|
||||||
|
object-fit: contain;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
border: 2px solid #e9ecef;
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-container {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
|
.merchant-logo {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== ÉTATS DE CHARGEMENT ====================
|
||||||
|
.logo-loading {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border: 3px solid rgba(0, 0, 0, 0.1);
|
||||||
|
border-top-color: #007bff;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to {
|
||||||
|
transform: translate(-50%, -50%) rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== RESPONSIVE ====================
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.logo-upload-section,
|
||||||
|
.logo-edit-section {
|
||||||
|
.logo-preview,
|
||||||
|
.logo-placeholder {
|
||||||
|
width: 150px;
|
||||||
|
height: 150px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.merchant-logo-large {
|
||||||
|
max-width: 150px;
|
||||||
|
max-height: 150px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== ANIMATIONS ====================
|
||||||
|
.logo-preview img,
|
||||||
|
.merchant-logo {
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== ERREURS ====================
|
||||||
|
.alert-danger {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
|
||||||
|
i {
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.merchant-logo-container {
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.merchant-logo-large {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
border: 3px solid rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.merchant-logo-large-placeholder {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border: 3px solid rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-header {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-card {
|
||||||
|
text-align: center;
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-icon {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-number {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #495057;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-label {
|
||||||
|
color: #6c757d;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.merchant-logo-container {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-header {
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-header h2 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Styles pour les logos dans la liste des marchands (TABLE) */
|
||||||
|
.merchant-logo-list {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
min-width: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.merchant-logo-list img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.merchant-logo-list-placeholder {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
min-width: 40px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2px solid #f0f0f0;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Gardez vos styles existants pour le profil */
|
||||||
|
.merchant-logo-container {
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.merchant-logo-large {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
border: 3px solid rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.merchant-logo-large-placeholder {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border: 3px solid rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.merchant-logo-list {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
min-width: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.merchant-logo-list-placeholder {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
min-width: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.merchant-logo-container {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==================== STYLES DU LOGO ==================== */
|
||||||
|
|
||||||
|
/* Conteneur principal */
|
||||||
|
.logo-card {
|
||||||
|
position: relative;
|
||||||
|
width: 220px;
|
||||||
|
height: 220px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Conteneur d'affichage du logo */
|
||||||
|
.logo-display-container {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 16px;
|
||||||
|
background: linear-gradient(145deg, #ffffff, #f0f0f0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Logo marchand */
|
||||||
|
.merchant-logo {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
transition: transform 0.5s ease;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.merchant-logo.logo-loaded {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-display-container:hover .merchant-logo {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Placeholder par défaut */
|
||||||
|
.default-logo-placeholder {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-initials {
|
||||||
|
font-size: 4rem;
|
||||||
|
font-weight: bold;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Overlay au hover */
|
||||||
|
.logo-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-display-container:hover .logo-overlay {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay-content {
|
||||||
|
text-align: center;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay-icon {
|
||||||
|
font-size: 2rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay-text {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Badge de statut */
|
||||||
|
.status-badge {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loader */
|
||||||
|
.logo-loader {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Informations sous le logo */
|
||||||
|
.logo-info {
|
||||||
|
max-width: 220px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.merchant-name {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Actions */
|
||||||
|
.logo-actions .btn {
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
border-radius: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-actions .btn:hover {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animation de chargement */
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; }
|
||||||
|
to { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.merchant-logo {
|
||||||
|
animation: fadeIn 0.5s ease forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* États */
|
||||||
|
.logo-loading .logo-display-container {
|
||||||
|
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: loading 1.5s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes loading {
|
||||||
|
0% { background-position: 200% 0; }
|
||||||
|
100% { background-position: -200% 0; }
|
||||||
|
}
|
||||||
@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"folders": [
|
|
||||||
{
|
|
||||||
"path": "../../../../../dcb-user-service"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "../../../.."
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"settings": {}
|
|
||||||
}
|
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import { Injectable, inject, EventEmitter } from '@angular/core';
|
import { Injectable, inject, EventEmitter } from '@angular/core';
|
||||||
import { HttpClient, HttpErrorResponse, HttpHeaders } from '@angular/common/http';
|
import { HttpClient, HttpErrorResponse, HttpHeaders } from '@angular/common/http';
|
||||||
import { environment } from '@environments/environment';
|
import { environment } from '@environments/environment';
|
||||||
import { BehaviorSubject, Observable, throwError, tap, catchError, finalize, of, filter, take } from 'rxjs';
|
import { BehaviorSubject, Observable, throwError, tap, catchError, finalize, of, filter, take, map } from 'rxjs';
|
||||||
import { firstValueFrom } from 'rxjs';
|
import { firstValueFrom } from 'rxjs';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -325,13 +325,12 @@ export class AuthService {
|
|||||||
return this.http.get<any>(
|
return this.http.get<any>(
|
||||||
`${environment.iamApiUrl}/auth/profile`
|
`${environment.iamApiUrl}/auth/profile`
|
||||||
).pipe(
|
).pipe(
|
||||||
tap(apiResponse => {
|
map(apiResponse => {
|
||||||
// Déterminer le type d'utilisateur
|
|
||||||
const userType = this.determineUserType(apiResponse);
|
const userType = this.determineUserType(apiResponse);
|
||||||
// Mapper vers le modèle User
|
|
||||||
const userProfile = this.mapToUserModel(apiResponse, userType);
|
const userProfile = this.mapToUserModel(apiResponse, userType);
|
||||||
|
|
||||||
this.userProfile$.next(userProfile);
|
this.userProfile$.next(userProfile);
|
||||||
|
return userProfile;
|
||||||
}),
|
}),
|
||||||
catchError(error => {
|
catchError(error => {
|
||||||
console.error('❌ Erreur chargement profil:', error);
|
console.error('❌ Erreur chargement profil:', error);
|
||||||
|
|||||||
@ -96,8 +96,7 @@ export class MenuService {
|
|||||||
{ label: 'Configurations', isTitle: true },
|
{ label: 'Configurations', isTitle: true },
|
||||||
{ label: 'Merchant Config', icon: 'lucideStore', url: '/merchant-config' },
|
{ label: 'Merchant Config', icon: 'lucideStore', url: '/merchant-config' },
|
||||||
|
|
||||||
{ label: 'Support & Profil', isTitle: true },
|
{ label: 'Profil', isTitle: true },
|
||||||
{ label: 'Support', icon: 'lucideLifeBuoy', url: '/support' },
|
|
||||||
{ label: 'Mon Profil', icon: 'lucideUser', url: '/profile' },
|
{ label: 'Mon Profil', icon: 'lucideUser', url: '/profile' },
|
||||||
|
|
||||||
{ label: 'Informations', isTitle: true },
|
{ label: 'Informations', isTitle: true },
|
||||||
@ -111,8 +110,7 @@ export class MenuService {
|
|||||||
return [
|
return [
|
||||||
{ label: 'Welcome back!', isHeader: true },
|
{ label: 'Welcome back!', isHeader: true },
|
||||||
{ label: 'Profile', icon: 'tablerUserCircle', url: '/profile' },
|
{ label: 'Profile', icon: 'tablerUserCircle', url: '/profile' },
|
||||||
{ label: 'Account Settings', icon: 'tablerSettings2', url: '/settings' },
|
{ label: 'Aide', icon: 'tablerHeadset', url: '/help' },
|
||||||
{ label: 'Support Center', icon: 'tablerHeadset', url: '/support' },
|
|
||||||
{ isDivider: true },
|
{ isDivider: true },
|
||||||
{
|
{
|
||||||
label: 'Déconnexion',
|
label: 'Déconnexion',
|
||||||
|
|||||||
405
src/app/core/services/minio.service.ts
Normal file
405
src/app/core/services/minio.service.ts
Normal file
@ -0,0 +1,405 @@
|
|||||||
|
import { Injectable, inject } from '@angular/core';
|
||||||
|
import { HttpClient, HttpHeaders, HttpEventType, HttpResponse } from '@angular/common/http';
|
||||||
|
import { Observable, throwError, of, Subject } from 'rxjs';
|
||||||
|
import { map, catchError, tap, filter } from 'rxjs/operators';
|
||||||
|
import { environment } from '@environments/environment';
|
||||||
|
|
||||||
|
export interface ImageUploadResponse {
|
||||||
|
message : string;
|
||||||
|
success: boolean;
|
||||||
|
merchant: {
|
||||||
|
id: string,
|
||||||
|
name: string
|
||||||
|
};
|
||||||
|
data: {
|
||||||
|
fileName: string;
|
||||||
|
url: string;
|
||||||
|
publicUrl: string;
|
||||||
|
downloadUrl: string;
|
||||||
|
size: number;
|
||||||
|
contentType: string;
|
||||||
|
uploadedAt: Date;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ImageValidationResult {
|
||||||
|
valid: boolean;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UploadProgress {
|
||||||
|
loaded: number;
|
||||||
|
total: number;
|
||||||
|
percentage: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LogoUrlResponse {
|
||||||
|
success: boolean;
|
||||||
|
data: {
|
||||||
|
fileName: string;
|
||||||
|
url: string;
|
||||||
|
merchantId: string;
|
||||||
|
merchantName: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LogoUrlOptions {
|
||||||
|
signed?: boolean;
|
||||||
|
expirySeconds?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class MinioService {
|
||||||
|
private http = inject(HttpClient);
|
||||||
|
private baseUrl = `${environment.configApiUrl}/images`;
|
||||||
|
|
||||||
|
// Sujet pour les événements de progression
|
||||||
|
private uploadProgressSubject = new Subject<UploadProgress>();
|
||||||
|
public uploadProgress$ = this.uploadProgressSubject.asObservable();
|
||||||
|
|
||||||
|
// Types MIME autorisés
|
||||||
|
private readonly ALLOWED_MIME_TYPES = [
|
||||||
|
'image/jpeg',
|
||||||
|
'image/jpg',
|
||||||
|
'image/png',
|
||||||
|
'image/gif',
|
||||||
|
'image/webp',
|
||||||
|
'image/svg+xml'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Taille maximale : 5MB
|
||||||
|
private readonly MAX_FILE_SIZE = 5 * 1024 * 1024;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Valide un fichier image
|
||||||
|
*/
|
||||||
|
validateImageFile(file: File): ImageValidationResult {
|
||||||
|
// Vérifier le type MIME
|
||||||
|
if (!this.ALLOWED_MIME_TYPES.includes(file.type)) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
error: `Type de fichier non autorisé. Formats acceptés : JPG, PNG, GIF, WebP, SVG`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier la taille
|
||||||
|
if (file.size > this.MAX_FILE_SIZE) {
|
||||||
|
const maxSizeMB = this.MAX_FILE_SIZE / (1024 * 1024);
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
error: `Fichier trop volumineux. Taille maximum : ${maxSizeMB}MB`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { valid: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MÉTHODE PRINCIPALE - Upload via le serveur
|
||||||
|
* préparation, upload et vérification
|
||||||
|
*/
|
||||||
|
uploadMerchantLogo( merchantId: number, merchantName: string, file: File): Observable<ImageUploadResponse> {
|
||||||
|
// Validation
|
||||||
|
const validation = this.validateImageFile(file);
|
||||||
|
if (!validation.valid) {
|
||||||
|
return throwError(() => new Error(validation.error));
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
|
||||||
|
// Ajouter des métadonnées optionnelles
|
||||||
|
if (merchantId) {
|
||||||
|
formData.append('merchantId', merchantId.toString());
|
||||||
|
}
|
||||||
|
if (merchantName) {
|
||||||
|
formData.append('merchantName', merchantName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Headers
|
||||||
|
const headers = new HttpHeaders({
|
||||||
|
'Accept': 'application/json'
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.http.post<ImageUploadResponse>(
|
||||||
|
`${this.baseUrl}/merchants/${merchantId}/logos/upload`,
|
||||||
|
formData,
|
||||||
|
{
|
||||||
|
headers,
|
||||||
|
reportProgress: true,
|
||||||
|
observe: 'events'
|
||||||
|
}
|
||||||
|
).pipe(
|
||||||
|
|
||||||
|
filter(event => event.type === HttpEventType.Response),
|
||||||
|
|
||||||
|
// Extraire le body
|
||||||
|
map(event => event.body as ImageUploadResponse),
|
||||||
|
|
||||||
|
catchError(error => {
|
||||||
|
console.error('❌ Error uploading image:', error);
|
||||||
|
return throwError(() => new Error(
|
||||||
|
error.error?.message ||
|
||||||
|
error.error?.error ||
|
||||||
|
error.message ||
|
||||||
|
'Erreur lors de l\'upload du logo'
|
||||||
|
));
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère l'URL (presigned) d'un logo marchand
|
||||||
|
*/
|
||||||
|
getMerchantLogoUrl(
|
||||||
|
merchantId: string,
|
||||||
|
fileName: string,
|
||||||
|
options: LogoUrlOptions = {}
|
||||||
|
): Observable<LogoUrlResponse> {
|
||||||
|
if (!merchantId || !fileName || fileName.trim() === '') {
|
||||||
|
return throwError(() => new Error('Paramètres invalides'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const params: any = {
|
||||||
|
fileName
|
||||||
|
};
|
||||||
|
|
||||||
|
if (options.signed) {
|
||||||
|
params.signed = 'true';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.expirySeconds) {
|
||||||
|
params.expiry = options.expirySeconds.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.http.get<LogoUrlResponse>(
|
||||||
|
`${this.baseUrl}/merchants/${merchantId}/logos/url`,
|
||||||
|
{ params }
|
||||||
|
).pipe(
|
||||||
|
map(response => {
|
||||||
|
if (response?.success && response.data?.url) {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
throw new Error('Logo non trouvé');
|
||||||
|
}),
|
||||||
|
catchError(error => {
|
||||||
|
console.error('❌ Error getting logo URL:', error);
|
||||||
|
return throwError(() => new Error(
|
||||||
|
error.error?.message ||
|
||||||
|
error.message ||
|
||||||
|
'Logo non trouvé'
|
||||||
|
));
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère les informations complètes d'une image
|
||||||
|
*/
|
||||||
|
getImageInfo(fileName: string): Observable<ImageUploadResponse> {
|
||||||
|
if (!fileName || fileName.trim() === '') {
|
||||||
|
return throwError(() => new Error('Nom de fichier invalide'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.http.get<{
|
||||||
|
success: boolean;
|
||||||
|
data?: ImageUploadResponse
|
||||||
|
}>(`${this.baseUrl}/info/${encodeURIComponent(fileName)}`).pipe(
|
||||||
|
map(response => {
|
||||||
|
if (response.success && response.data) {
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
throw new Error('Image not found or error in response');
|
||||||
|
}),
|
||||||
|
catchError(this.handleError('get image info'))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supprime un logo
|
||||||
|
*/
|
||||||
|
deleteMerchantLogo(
|
||||||
|
merchantId: string,
|
||||||
|
fileName: string
|
||||||
|
): Observable<void> {
|
||||||
|
if (!merchantId || !fileName || fileName.trim() === '') {
|
||||||
|
return throwError(() => new Error('Paramètres invalides ou Nom de fichier invalide'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const params: any = {
|
||||||
|
fileName
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.http.delete<{
|
||||||
|
success: boolean;
|
||||||
|
message: string
|
||||||
|
}>(
|
||||||
|
`${this.baseUrl}/merchants/${merchantId}/logos/url`,
|
||||||
|
{ params }
|
||||||
|
).pipe(
|
||||||
|
map(response => {
|
||||||
|
if (response.success) {
|
||||||
|
return void 0;
|
||||||
|
}
|
||||||
|
throw new Error(response.message || 'Erreur lors de la suppression');
|
||||||
|
}),
|
||||||
|
catchError(error => {
|
||||||
|
console.error('❌ Error deleting logo:', error);
|
||||||
|
return throwError(() => new Error(
|
||||||
|
error.error?.message || 'Erreur lors de la suppression du logo'
|
||||||
|
));
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Liste les images de l'utilisateur courant
|
||||||
|
*/
|
||||||
|
getMyImages(): Observable<ImageUploadResponse[]> {
|
||||||
|
return this.http.get<{
|
||||||
|
success: boolean;
|
||||||
|
count: number;
|
||||||
|
images: ImageUploadResponse[]
|
||||||
|
}>(`${this.baseUrl}/my-images`).pipe(
|
||||||
|
map(response => {
|
||||||
|
if (response.success) {
|
||||||
|
return response.images || [];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}),
|
||||||
|
catchError(error => {
|
||||||
|
console.error('❌ Error getting user images:', error);
|
||||||
|
return of([]);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prévisualise une image avant upload
|
||||||
|
*/
|
||||||
|
previewImage(file: File): Promise<string> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
|
||||||
|
reader.onload = (e: ProgressEvent<FileReader>) => {
|
||||||
|
if (e.target?.result) {
|
||||||
|
resolve(e.target.result as string);
|
||||||
|
} else {
|
||||||
|
reject(new Error('Impossible de lire le fichier'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
reader.onerror = () => {
|
||||||
|
reject(new Error('Erreur lors de la lecture du fichier'));
|
||||||
|
};
|
||||||
|
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formate la taille d'un fichier
|
||||||
|
*/
|
||||||
|
formatFileSize(bytes: number): string {
|
||||||
|
if (bytes === 0) return '0 B';
|
||||||
|
const k = 1024;
|
||||||
|
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si un nom de fichier est valide
|
||||||
|
*/
|
||||||
|
isValidFileName(fileName: string): boolean {
|
||||||
|
return typeof fileName === 'string' && fileName.trim().length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extrait le nom de fichier depuis le chemin complet
|
||||||
|
*/
|
||||||
|
extractFileNameFromPath(filePath: string): string {
|
||||||
|
return filePath.split('/').pop() || filePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée une URL d'objet blob pour prévisualisation
|
||||||
|
*/
|
||||||
|
createObjectUrl(file: File): string {
|
||||||
|
return URL.createObjectURL(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Révoque une URL d'objet blob
|
||||||
|
*/
|
||||||
|
revokeObjectUrl(url: string): void {
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Réinitialise la progression
|
||||||
|
*/
|
||||||
|
resetUploadProgress(): void {
|
||||||
|
this.uploadProgressSubject.next({ loaded: 0, total: 0, percentage: 0 });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gestionnaire d'erreurs générique
|
||||||
|
*/
|
||||||
|
private handleError(operation: string) {
|
||||||
|
return (error: any): Observable<never> => {
|
||||||
|
console.error(`❌ Error in ${operation}:`, error);
|
||||||
|
|
||||||
|
let errorMessage = 'Une erreur est survenue';
|
||||||
|
|
||||||
|
if (error.error instanceof ErrorEvent) {
|
||||||
|
// Erreur côté client
|
||||||
|
errorMessage = error.error.message;
|
||||||
|
} else {
|
||||||
|
// Erreur côté serveur
|
||||||
|
errorMessage = error.error?.message ||
|
||||||
|
error.error?.error ||
|
||||||
|
error.message ||
|
||||||
|
`Error ${error.status}: ${error.statusText}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return throwError(() => new Error(errorMessage));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si le type MIME est une image
|
||||||
|
*/
|
||||||
|
isImageMimeType(mimeType: string): boolean {
|
||||||
|
return this.ALLOWED_MIME_TYPES.includes(mimeType);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Génère un nom de fichier sécurisé côté client (optionnel)
|
||||||
|
*/
|
||||||
|
generateSafeFileName(originalName: string, userId?: string): string {
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const randomString = Math.random().toString(36).substring(2, 10);
|
||||||
|
const extension = originalName.includes('.')
|
||||||
|
? originalName.substring(originalName.lastIndexOf('.'))
|
||||||
|
: '';
|
||||||
|
|
||||||
|
const baseName = originalName.includes('.')
|
||||||
|
? originalName.substring(0, originalName.lastIndexOf('.'))
|
||||||
|
: originalName;
|
||||||
|
|
||||||
|
const safeBaseName = baseName
|
||||||
|
.normalize('NFD')
|
||||||
|
.replace(/[\u0300-\u036f]/g, '')
|
||||||
|
.replace(/[^a-zA-Z0-9-]/g, '_')
|
||||||
|
.replace(/_+/g, '_')
|
||||||
|
.toLowerCase()
|
||||||
|
.substring(0, 100);
|
||||||
|
|
||||||
|
const prefix = userId ? `${userId}/` : '';
|
||||||
|
|
||||||
|
return `${prefix}${timestamp}_${randomString}_${safeBaseName}${extension}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -15,14 +15,41 @@
|
|||||||
<!-- États normal et erreur avec @if -->
|
<!-- États normal et erreur avec @if -->
|
||||||
@if (!isLoading) {
|
@if (!isLoading) {
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<img
|
@if (user){
|
||||||
[src]="getUserAvatar()"
|
@if (merchant){
|
||||||
class="rounded-circle me-2"
|
@if (merchant.logo && merchant.logo.trim() !== '') {
|
||||||
width="36"
|
<img
|
||||||
height="36"
|
[src]="getMerchantLogoUrl(merchant.id, merchant.logo, merchant.name) | async"
|
||||||
alt="user-image"
|
[alt]="merchant.name + ' logo'"
|
||||||
(error)="onAvatarError($event)"
|
class="rounded-circle me-2"
|
||||||
/>
|
width="36"
|
||||||
|
height="36"
|
||||||
|
loading="lazy"
|
||||||
|
(error)="onLogoError($event, merchant.name)"
|
||||||
|
/>
|
||||||
|
} @else {
|
||||||
|
<img
|
||||||
|
[src]="getDefaultLogoUrl(merchant.name)"
|
||||||
|
[alt]="merchant.name + ' logo'"
|
||||||
|
class="rounded-circle me-2"
|
||||||
|
width="36"
|
||||||
|
height="36"
|
||||||
|
loading="lazy"
|
||||||
|
(error)="onDefaultLogoError($event)"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
}@else {
|
||||||
|
<img
|
||||||
|
[src]="getDefaultLogoUrl(user.username)"
|
||||||
|
[alt]="user.username + ' logo'"
|
||||||
|
class="rounded-circle me-2"
|
||||||
|
width="36"
|
||||||
|
height="36"
|
||||||
|
loading="lazy"
|
||||||
|
(error)="onDefaultLogoError($event)"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
}
|
||||||
<div>
|
<div>
|
||||||
<h5 class="my-0 fw-semibold">
|
<h5 class="my-0 fw-semibold">
|
||||||
{{ getDisplayName() || 'Utilisateur' }}
|
{{ getDisplayName() || 'Utilisateur' }}
|
||||||
|
|||||||
@ -3,23 +3,45 @@ import { NgbCollapseModule } from '@ng-bootstrap/ng-bootstrap';
|
|||||||
import { userDropdownItems } from '@layouts/components/data';
|
import { userDropdownItems } from '@layouts/components/data';
|
||||||
import { AuthService } from '@/app/core/services/auth.service';
|
import { AuthService } from '@/app/core/services/auth.service';
|
||||||
import { User, UserRole } from '@core/models/dcb-bo-hub-user.model';
|
import { User, UserRole } from '@core/models/dcb-bo-hub-user.model';
|
||||||
import { Subject, takeUntil, distinctUntilChanged, filter, startWith } from 'rxjs';
|
import { Subject, takeUntil, distinctUntilChanged, filter, startWith, catchError, map, Observable, of, Subscription } from 'rxjs';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { Merchant } from '@core/models/merchant-config.model';
|
||||||
|
import { MinioService } from '@core/services/minio.service';
|
||||||
|
import { MerchantConfigService } from '@modules/merchant-config/merchant-config.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-user-profile',
|
selector: 'app-user-profile',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [NgbCollapseModule],
|
imports: [NgbCollapseModule,CommonModule],
|
||||||
templateUrl: './user-profile.component.html',
|
templateUrl: './user-profile.component.html',
|
||||||
})
|
})
|
||||||
export class UserProfileComponent implements OnInit, OnDestroy {
|
export class UserProfileComponent implements OnInit, OnDestroy {
|
||||||
private authService = inject(AuthService);
|
private authService = inject(AuthService);
|
||||||
private cdr = inject(ChangeDetectorRef);
|
private merchantConfigService = inject(MerchantConfigService);
|
||||||
|
private subscription?: Subscription;
|
||||||
|
private minioService = inject(MinioService);
|
||||||
|
private cdRef = inject(ChangeDetectorRef);
|
||||||
private destroy$ = new Subject<void>();
|
private destroy$ = new Subject<void>();
|
||||||
|
|
||||||
user: User | null = null;
|
// Cache des URLs de logos
|
||||||
|
private logoUrlCache = new Map<string, string>();
|
||||||
|
// Ajouter un cache pour les logos non trouvés
|
||||||
|
private logoErrorCache = new Set<string>();
|
||||||
|
// Cache
|
||||||
|
private merchantCache: { data: Merchant, timestamp: number } | null = null;
|
||||||
|
private readonly CACHE_TTL = 2 * 60 * 1000; // 2 minutes
|
||||||
|
// Permissions
|
||||||
|
currentUserRole: any = null;
|
||||||
|
isHubUser = false;
|
||||||
|
|
||||||
|
merchant: Merchant | null = null;
|
||||||
|
user: User | undefined;
|
||||||
|
merchanPartnerId: string | undefined
|
||||||
|
|
||||||
|
// États
|
||||||
isLoading = true;
|
isLoading = true;
|
||||||
hasError = false;
|
hasError = false;
|
||||||
|
hasSuccess = '';
|
||||||
currentProfileLoaded = false;
|
currentProfileLoaded = false;
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
@ -45,9 +67,9 @@ export class UserProfileComponent implements OnInit, OnDestroy {
|
|||||||
// Le profil sera chargé via la subscription
|
// Le profil sera chargé via la subscription
|
||||||
} else {
|
} else {
|
||||||
console.log('🔐 User not authenticated');
|
console.log('🔐 User not authenticated');
|
||||||
this.user = null;
|
this.user = undefined;
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
this.cdr.detectChanges();
|
this.cdRef.detectChanges();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -76,22 +98,31 @@ export class UserProfileComponent implements OnInit, OnDestroy {
|
|||||||
if (profile) {
|
if (profile) {
|
||||||
console.log('📥 User profile updated:', profile.username);
|
console.log('📥 User profile updated:', profile.username);
|
||||||
this.user = profile;
|
this.user = profile;
|
||||||
|
|
||||||
|
this.currentUserRole = this.extractUserRole(profile);
|
||||||
|
this.isHubUser = this.checkIfHubUser();
|
||||||
|
|
||||||
|
if (!this.isHubUser) {
|
||||||
|
this.merchanPartnerId = profile?.merchantPartnerId;
|
||||||
|
this.loadMerchantProfile()
|
||||||
|
}
|
||||||
|
|
||||||
this.currentProfileLoaded = true;
|
this.currentProfileLoaded = true;
|
||||||
} else {
|
} else {
|
||||||
console.log('📭 User profile cleared');
|
console.log('📭 User profile cleared');
|
||||||
this.user = null;
|
this.user = undefined;
|
||||||
this.currentProfileLoaded = false;
|
this.currentProfileLoaded = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
this.hasError = false;
|
this.hasError = false;
|
||||||
this.cdr.detectChanges();
|
this.cdRef.detectChanges();
|
||||||
},
|
},
|
||||||
error: (error) => {
|
error: (error) => {
|
||||||
console.error('❌ Error in profile subscription:', error);
|
console.error('❌ Error in profile subscription:', error);
|
||||||
this.hasError = true;
|
this.hasError = true;
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
this.cdr.detectChanges();
|
this.cdRef.detectChanges();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -115,10 +146,10 @@ export class UserProfileComponent implements OnInit, OnDestroy {
|
|||||||
} else {
|
} else {
|
||||||
// Si l'utilisateur s'est déconnecté
|
// Si l'utilisateur s'est déconnecté
|
||||||
console.log('👋 User logged out');
|
console.log('👋 User logged out');
|
||||||
this.user = null;
|
this.user = undefined;
|
||||||
this.currentProfileLoaded = false;
|
this.currentProfileLoaded = false;
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
this.cdr.detectChanges();
|
this.cdRef.detectChanges();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -130,21 +161,20 @@ export class UserProfileComponent implements OnInit, OnDestroy {
|
|||||||
loadUserProfile(): void {
|
loadUserProfile(): void {
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
this.hasError = false;
|
this.hasError = false;
|
||||||
this.cdr.detectChanges();
|
this.cdRef.detectChanges();
|
||||||
|
|
||||||
this.authService.loadUserProfile()
|
this.authService.loadUserProfile()
|
||||||
.pipe(takeUntil(this.destroy$))
|
.pipe(takeUntil(this.destroy$))
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: (profile) => {
|
next: (profile) => {
|
||||||
// Note: le profil sera automatiquement mis à jour via la subscription getUserProfile()
|
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
this.cdr.detectChanges();
|
this.cdRef.detectChanges();
|
||||||
},
|
},
|
||||||
error: (error) => {
|
error: (error) => {
|
||||||
console.error('❌ Failed to load user profile:', error);
|
console.error('❌ Failed to load user profile:', error);
|
||||||
this.hasError = true;
|
this.hasError = true;
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
this.cdr.detectChanges();
|
this.cdRef.detectChanges();
|
||||||
|
|
||||||
// Essayer de rafraîchir le token si erreur 401
|
// Essayer de rafraîchir le token si erreur 401
|
||||||
if (error.status === 401) {
|
if (error.status === 401) {
|
||||||
@ -162,6 +192,179 @@ export class UserProfileComponent implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Charge le profil COMPLET du merchant
|
||||||
|
*/
|
||||||
|
loadMerchantProfile() {
|
||||||
|
if (this.shouldUseCache()) {
|
||||||
|
this.merchant = this.merchantCache!.data;
|
||||||
|
this.isLoading = false;
|
||||||
|
this.cdRef.detectChanges();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isLoading = true;
|
||||||
|
this.hasError = false;
|
||||||
|
|
||||||
|
console.log("📥 Chargement du profil complet du merchant:", this.merchanPartnerId);
|
||||||
|
|
||||||
|
this.merchantConfigService.getMerchantById(Number(this.merchanPartnerId))
|
||||||
|
.pipe(takeUntil(this.destroy$))
|
||||||
|
.subscribe({
|
||||||
|
next: (merchant) => {
|
||||||
|
this.merchant = merchant;
|
||||||
|
|
||||||
|
// Mise en cache
|
||||||
|
this.merchantCache = {
|
||||||
|
data: merchant,
|
||||||
|
timestamp: Date.now()
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log("✅ Profil merchant chargé:", merchant);
|
||||||
|
this.isLoading = false;
|
||||||
|
this.cdRef.detectChanges();
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('❌ Error loading merchant profile:', error);
|
||||||
|
this.hasError = true;
|
||||||
|
this.isLoading = false;
|
||||||
|
this.cdRef.detectChanges();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ==================== AFFICHAGE DU LOGO ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère l'URL du logo avec fallback automatique
|
||||||
|
*/
|
||||||
|
getMerchantLogoUrl(
|
||||||
|
merchanPartnerId: number | undefined,
|
||||||
|
logoFileName: string,
|
||||||
|
merchantName: string
|
||||||
|
): Observable<string> {
|
||||||
|
|
||||||
|
const newMerchantId = String(merchanPartnerId);
|
||||||
|
|
||||||
|
const cacheKey = `${merchanPartnerId}_${logoFileName}`;
|
||||||
|
|
||||||
|
// Vérifier si le logo est en cache d'erreur
|
||||||
|
if (this.logoErrorCache.has(cacheKey)) {
|
||||||
|
const defaultLogo = this.getDefaultLogoUrl(merchantName);
|
||||||
|
return of(defaultLogo);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier le cache normal
|
||||||
|
if (this.logoUrlCache.has(cacheKey)) {
|
||||||
|
return of(this.logoUrlCache.get(cacheKey)!);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Récupérer l'URL depuis l'API avec la nouvelle structure
|
||||||
|
return this.minioService.getMerchantLogoUrl(
|
||||||
|
newMerchantId,
|
||||||
|
logoFileName,
|
||||||
|
{ signed: true, expirySeconds: 3600 }
|
||||||
|
).pipe(
|
||||||
|
map(response => {
|
||||||
|
// Extraire l'URL de la réponse
|
||||||
|
const url = response.data.url ;
|
||||||
|
|
||||||
|
// Mettre en cache avec la clé composite
|
||||||
|
this.logoUrlCache.set(cacheKey, url);
|
||||||
|
|
||||||
|
return url;
|
||||||
|
}),
|
||||||
|
catchError(error => {
|
||||||
|
console.warn(`⚠️ Logo not found for merchant ${merchanPartnerId}: ${logoFileName}`, error);
|
||||||
|
|
||||||
|
// En cas d'erreur, ajouter au cache d'erreur
|
||||||
|
this.logoErrorCache.add(cacheKey);
|
||||||
|
|
||||||
|
// Générer un logo par défaut
|
||||||
|
const defaultLogo = this.getDefaultLogoUrl(merchantName);
|
||||||
|
|
||||||
|
// Mettre le logo par défaut dans le cache normal aussi
|
||||||
|
this.logoUrlCache.set(cacheKey, defaultLogo);
|
||||||
|
|
||||||
|
return of(defaultLogo);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Génère une URL de logo par défaut basée sur les initiales
|
||||||
|
*/
|
||||||
|
getDefaultLogoUrl(merchantName: string): string {
|
||||||
|
// Créer des initiales significatives
|
||||||
|
const initials = this.extractInitials(merchantName);
|
||||||
|
|
||||||
|
// Palette de couleurs agréables
|
||||||
|
const colors = [
|
||||||
|
'667eea', // Violet
|
||||||
|
'764ba2', // Violet foncé
|
||||||
|
'f56565', // Rouge
|
||||||
|
'4299e1', // Bleu
|
||||||
|
'48bb78', // Vert
|
||||||
|
'ed8936', // Orange
|
||||||
|
'FF6B6B', // Rouge clair
|
||||||
|
'4ECDC4', // Turquoise
|
||||||
|
'45B7D1', // Bleu clair
|
||||||
|
'96CEB4' // Vert menthe
|
||||||
|
];
|
||||||
|
|
||||||
|
const colorIndex = merchantName.length % colors.length;
|
||||||
|
const backgroundColor = colors[colorIndex];
|
||||||
|
|
||||||
|
// Taille fixe à 80px (l'API génère un carré de cette taille)
|
||||||
|
// L'image sera redimensionnée à 40px via CSS
|
||||||
|
return `https://ui-avatars.com/api/?name=${encodeURIComponent(initials)}&background=${backgroundColor}&color=FFFFFF&size=80`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gère les erreurs de chargement des logos MinIO
|
||||||
|
*/
|
||||||
|
onLogoError(event: Event, merchantName: string): void {
|
||||||
|
const img = event.target as HTMLImageElement;
|
||||||
|
|
||||||
|
if (!img) return;
|
||||||
|
|
||||||
|
console.warn('Logo MinIO failed to load, using default for:', merchantName);
|
||||||
|
|
||||||
|
img.onerror = null;
|
||||||
|
img.src = this.getDefaultLogoUrl(merchantName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gère les erreurs de chargement des logos par défaut
|
||||||
|
*/
|
||||||
|
onDefaultLogoError(event: Event | string): void {
|
||||||
|
if (!(event instanceof Event)) {
|
||||||
|
console.error('Default logo error (non-event):', event);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const img = event.target as HTMLImageElement | null;
|
||||||
|
if (!img) return;
|
||||||
|
|
||||||
|
console.error('Default logo also failed to load, using fallback SVG');
|
||||||
|
|
||||||
|
// SVG local
|
||||||
|
img.onerror = null; // éviter boucle infinie
|
||||||
|
img.src = 'assets/images/default-merchant-logo.svg';
|
||||||
|
|
||||||
|
// Dernier recours
|
||||||
|
img.onerror = (e) => {
|
||||||
|
if (!(e instanceof Event)) return;
|
||||||
|
const fallbackImg = e.target as HTMLImageElement | null;
|
||||||
|
if (!fallbackImg) return;
|
||||||
|
|
||||||
|
fallbackImg.onerror = null;
|
||||||
|
fallbackImg.src = this.generateFallbackDataUrl();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Méthode pour réessayer le chargement en cas d'erreur
|
* Méthode pour réessayer le chargement en cas d'erreur
|
||||||
*/
|
*/
|
||||||
@ -234,19 +437,91 @@ export class UserProfileComponent implements OnInit, OnDestroy {
|
|||||||
return roleClassMap[this.user.role] || 'badge bg-secondary';
|
return roleClassMap[this.user.role] || 'badge bg-secondary';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private extractUserRole(user: any): any {
|
||||||
* Obtient l'URL de l'avatar de l'utilisateur
|
const userRoles = this.authService.getCurrentUserRoles();
|
||||||
*/
|
return userRoles && userRoles.length > 0 ? userRoles[0] : null;
|
||||||
getUserAvatar(): string {
|
}
|
||||||
return `assets/images/users/user-2.jpg`;
|
|
||||||
|
private checkIfHubUser(): boolean {
|
||||||
|
if (!this.currentUserRole) return false;
|
||||||
|
|
||||||
|
const hubRoles = [
|
||||||
|
UserRole.DCB_ADMIN,
|
||||||
|
UserRole.DCB_SUPPORT
|
||||||
|
];
|
||||||
|
|
||||||
|
return hubRoles.includes(this.currentUserRole);
|
||||||
|
}
|
||||||
|
|
||||||
|
private shouldUseCache(): boolean {
|
||||||
|
if (!this.merchantCache) return false;
|
||||||
|
|
||||||
|
const cacheAge = Date.now() - this.merchantCache.timestamp;
|
||||||
|
return cacheAge < this.CACHE_TTL && this.merchantCache.data !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private clearCache(): void {
|
||||||
|
this.merchantCache = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gère les erreurs de chargement d'avatar
|
* Extrait les initiales de manière intelligente
|
||||||
*/
|
*/
|
||||||
onAvatarError(event: Event): void {
|
private extractInitials(name: string): string {
|
||||||
const img = event.target as HTMLImageElement;
|
if (!name || name.trim() === '') {
|
||||||
img.src = 'assets/images/users/user-2.jpg';
|
return '??';
|
||||||
img.onerror = null;
|
}
|
||||||
|
|
||||||
|
// Nettoyer le nom
|
||||||
|
const cleanedName = name.trim().toUpperCase();
|
||||||
|
|
||||||
|
// Extraire les mots
|
||||||
|
const words = cleanedName.split(/\s+/);
|
||||||
|
|
||||||
|
// Si un seul mot, prendre les deux premières lettres
|
||||||
|
if (words.length === 1) {
|
||||||
|
return words[0].substring(0, 2) || '??';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prendre la première lettre des deux premiers mots
|
||||||
|
const initials = words
|
||||||
|
.slice(0, 2) // Prendre les 2 premiers mots
|
||||||
|
.map(word => word[0] || '')
|
||||||
|
.join('');
|
||||||
|
|
||||||
|
return initials || name.substring(0, 2).toUpperCase() || '??';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Génère un fallback SVG en data URL
|
||||||
|
*/
|
||||||
|
private generateFallbackDataUrl(): string {
|
||||||
|
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40">
|
||||||
|
<rect width="40" height="40" fill="#667eea" rx="20"/>
|
||||||
|
<text x="20" y="22" text-anchor="middle" fill="white" font-family="Arial" font-size="14" font-weight="bold">?</text>
|
||||||
|
</svg>`;
|
||||||
|
|
||||||
|
return 'data:image/svg+xml;base64,' + btoa(svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== GESTION DES ERREURS ====================
|
||||||
|
|
||||||
|
private getErrorMessage(error: any): string {
|
||||||
|
if (error.error?.message) {
|
||||||
|
return error.error.message;
|
||||||
|
}
|
||||||
|
if (error.status === 400) {
|
||||||
|
return 'Données invalides. Vérifiez les informations saisies.';
|
||||||
|
}
|
||||||
|
if (error.status === 403) {
|
||||||
|
return 'Vous n\'avez pas les permissions nécessaires pour cette action';
|
||||||
|
}
|
||||||
|
if (error.status === 404) {
|
||||||
|
return 'Utilisateur non trouvé';
|
||||||
|
}
|
||||||
|
if (error.status === 409) {
|
||||||
|
return 'Cet email est déjà utilisé par un autre utilisateur';
|
||||||
|
}
|
||||||
|
return 'Une erreur est survenue. Veuillez réessayer.';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -4,12 +4,41 @@
|
|||||||
ngbDropdownToggle
|
ngbDropdownToggle
|
||||||
class="topbar-link dropdown-toggle drop-arrow-none px-2"
|
class="topbar-link dropdown-toggle drop-arrow-none px-2"
|
||||||
>
|
>
|
||||||
<img
|
@if (user){
|
||||||
src="assets/images/users/user-2.jpg"
|
@if (merchant){
|
||||||
width="32"
|
@if (merchant.logo && merchant.logo.trim() !== '') {
|
||||||
class="rounded-circle d-flex"
|
<img
|
||||||
alt="user-image"
|
[src]="getMerchantLogoUrl(merchant.id, merchant.logo, merchant.name) | async"
|
||||||
/>
|
[alt]="merchant.name + ' logo'"
|
||||||
|
class="rounded-circle me-2"
|
||||||
|
width="36"
|
||||||
|
height="36"
|
||||||
|
loading="lazy"
|
||||||
|
(error)="onLogoError($event, merchant.name)"
|
||||||
|
/>
|
||||||
|
} @else {
|
||||||
|
<img
|
||||||
|
[src]="getDefaultLogoUrl(merchant.name)"
|
||||||
|
[alt]="merchant.name + ' logo'"
|
||||||
|
class="rounded-circle me-2"
|
||||||
|
width="36"
|
||||||
|
height="36"
|
||||||
|
loading="lazy"
|
||||||
|
(error)="onDefaultLogoError($event)"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
}@else {
|
||||||
|
<img
|
||||||
|
[src]="getDefaultLogoUrl(user.username)"
|
||||||
|
[alt]="user.username + ' logo'"
|
||||||
|
class="rounded-circle me-2"
|
||||||
|
width="36"
|
||||||
|
height="36"
|
||||||
|
loading="lazy"
|
||||||
|
(error)="onDefaultLogoError($event)"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
}
|
||||||
</button>
|
</button>
|
||||||
<div ngbDropdownMenu class="dropdown-menu dropdown-menu-end">
|
<div ngbDropdownMenu class="dropdown-menu dropdown-menu-end">
|
||||||
@for (item of menuItems; track $index; let i = $index) {
|
@for (item of menuItems; track $index; let i = $index) {
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { Component, inject, OnInit, OnDestroy } from '@angular/core'
|
import { Component, inject, OnInit, OnDestroy, ChangeDetectorRef } from '@angular/core'
|
||||||
import { AuthService } from '@core/services/auth.service'
|
import { AuthService } from '@core/services/auth.service'
|
||||||
import { MenuService } from '@core/services/menu.service'
|
import { MenuService } from '@core/services/menu.service'
|
||||||
import {
|
import {
|
||||||
@ -9,7 +9,12 @@ import {
|
|||||||
import { RouterLink } from '@angular/router'
|
import { RouterLink } from '@angular/router'
|
||||||
import { NgIcon } from '@ng-icons/core'
|
import { NgIcon } from '@ng-icons/core'
|
||||||
import { UserDropdownItemType } from '@/app/types/layout'
|
import { UserDropdownItemType } from '@/app/types/layout'
|
||||||
import { Subscription } from 'rxjs'
|
import { catchError, map, Observable, of, Subject, Subscription, takeUntil } from 'rxjs'
|
||||||
|
import { MinioService } from '@core/services/minio.service'
|
||||||
|
import { Merchant } from '@core/models/merchant-config.model'
|
||||||
|
import { MerchantConfigService } from '@modules/merchant-config/merchant-config.service'
|
||||||
|
import { User, UserRole } from '@core/models/dcb-bo-hub-user.model'
|
||||||
|
import { CommonModule } from '@angular/common'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-user-profile-topbar',
|
selector: 'app-user-profile-topbar',
|
||||||
@ -19,17 +24,46 @@ import { Subscription } from 'rxjs'
|
|||||||
NgbDropdownToggle,
|
NgbDropdownToggle,
|
||||||
RouterLink,
|
RouterLink,
|
||||||
NgIcon,
|
NgIcon,
|
||||||
|
CommonModule,
|
||||||
],
|
],
|
||||||
templateUrl: './user-profile.html',
|
templateUrl: './user-profile.html',
|
||||||
})
|
})
|
||||||
export class UserProfile implements OnInit, OnDestroy {
|
export class UserProfile implements OnInit, OnDestroy {
|
||||||
private authService = inject(AuthService)
|
private authService = inject(AuthService);
|
||||||
private menuService = inject(MenuService)
|
private merchantConfigService = inject(MerchantConfigService);
|
||||||
private subscription?: Subscription
|
private menuService = inject(MenuService);
|
||||||
|
private subscription?: Subscription;
|
||||||
|
private minioService = inject(MinioService);
|
||||||
|
private cdRef = inject(ChangeDetectorRef);
|
||||||
|
private destroy$ = new Subject<void>();
|
||||||
|
|
||||||
|
// Cache des URLs de logos
|
||||||
|
private logoUrlCache = new Map<string, string>();
|
||||||
|
// Ajouter un cache pour les logos non trouvés
|
||||||
|
private logoErrorCache = new Set<string>();
|
||||||
|
// Cache
|
||||||
|
private merchantCache: { data: Merchant, timestamp: number } | null = null;
|
||||||
|
private readonly CACHE_TTL = 2 * 60 * 1000; // 2 minutes
|
||||||
|
|
||||||
|
// Permissions
|
||||||
|
currentUserRole: any = null;
|
||||||
|
isHubUser = false;
|
||||||
|
|
||||||
|
merchant: Merchant | null = null;
|
||||||
|
|
||||||
|
user: User | undefined;
|
||||||
|
|
||||||
|
// États
|
||||||
|
loading = false;
|
||||||
|
error = '';
|
||||||
|
success = '';
|
||||||
|
|
||||||
|
|
||||||
menuItems: UserDropdownItemType[] = []
|
menuItems: UserDropdownItemType[] = []
|
||||||
|
merchanPartnerId: string | undefined
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
|
this.loadUserProfile()
|
||||||
this.loadDropdownItems()
|
this.loadDropdownItems()
|
||||||
|
|
||||||
this.subscription = this.authService.onAuthState().subscribe(() => {
|
this.subscription = this.authService.onAuthState().subscribe(() => {
|
||||||
@ -37,11 +71,305 @@ export class UserProfile implements OnInit, OnDestroy {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy(): void {
|
||||||
|
this.destroy$.next();
|
||||||
|
this.destroy$.complete();
|
||||||
this.subscription?.unsubscribe()
|
this.subscription?.unsubscribe()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== CHARGEMENT DES DONNÉES ====================
|
||||||
|
|
||||||
|
loadUserProfile() {
|
||||||
|
this.loading = true;
|
||||||
|
this.error = '';
|
||||||
|
|
||||||
|
this.authService.loadUserProfile()
|
||||||
|
.pipe(takeUntil(this.destroy$))
|
||||||
|
.subscribe({
|
||||||
|
next: (profile) => {
|
||||||
|
this.user = profile;
|
||||||
|
console.log("Profile User : " + profile?.role);
|
||||||
|
this.currentUserRole = this.extractUserRole(profile);
|
||||||
|
this.isHubUser = this.checkIfHubUser();
|
||||||
|
|
||||||
|
if (!this.isHubUser) {
|
||||||
|
this.merchanPartnerId = profile.merchantPartnerId;
|
||||||
|
this.loadMerchantProfile()
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loading = false;
|
||||||
|
this.cdRef.detectChanges();
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
this.error = 'Erreur lors du chargement de votre profil';
|
||||||
|
this.loading = false;
|
||||||
|
this.cdRef.detectChanges();
|
||||||
|
console.error('Error loading user profile:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Charge le profil COMPLET du merchant
|
||||||
|
*/
|
||||||
|
loadMerchantProfile() {
|
||||||
|
if (this.shouldUseCache()) {
|
||||||
|
this.merchant = this.merchantCache!.data;
|
||||||
|
this.loading = false;
|
||||||
|
this.cdRef.detectChanges();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loading = true;
|
||||||
|
this.error = '';
|
||||||
|
|
||||||
|
console.log("📥 Chargement du profil complet du merchant:", this.merchanPartnerId);
|
||||||
|
|
||||||
|
this.merchantConfigService.getMerchantById(Number(this.merchanPartnerId))
|
||||||
|
.pipe(takeUntil(this.destroy$))
|
||||||
|
.subscribe({
|
||||||
|
next: (merchant) => {
|
||||||
|
this.merchant = merchant;
|
||||||
|
|
||||||
|
// Mise en cache
|
||||||
|
this.merchantCache = {
|
||||||
|
data: merchant,
|
||||||
|
timestamp: Date.now()
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log("✅ Profil merchant chargé:", merchant);
|
||||||
|
this.loading = false;
|
||||||
|
this.cdRef.detectChanges();
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('❌ Error loading merchant profile:', error);
|
||||||
|
this.error = this.getErrorMessage(error);
|
||||||
|
this.loading = false;
|
||||||
|
this.cdRef.detectChanges();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== AFFICHAGE DU LOGO ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère l'URL du logo avec fallback automatique
|
||||||
|
*/
|
||||||
|
getMerchantLogoUrl(
|
||||||
|
merchanPartnerId: number | undefined,
|
||||||
|
logoFileName: string,
|
||||||
|
merchantName: string
|
||||||
|
): Observable<string> {
|
||||||
|
|
||||||
|
const newMerchantId = String(merchanPartnerId);
|
||||||
|
|
||||||
|
const cacheKey = `${merchanPartnerId}_${logoFileName}`;
|
||||||
|
|
||||||
|
// Vérifier si le logo est en cache d'erreur
|
||||||
|
if (this.logoErrorCache.has(cacheKey)) {
|
||||||
|
const defaultLogo = this.getDefaultLogoUrl(merchantName);
|
||||||
|
return of(defaultLogo);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier le cache normal
|
||||||
|
if (this.logoUrlCache.has(cacheKey)) {
|
||||||
|
return of(this.logoUrlCache.get(cacheKey)!);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Récupérer l'URL depuis l'API avec la nouvelle structure
|
||||||
|
return this.minioService.getMerchantLogoUrl(
|
||||||
|
newMerchantId,
|
||||||
|
logoFileName,
|
||||||
|
{ signed: true, expirySeconds: 3600 }
|
||||||
|
).pipe(
|
||||||
|
map(response => {
|
||||||
|
// Extraire l'URL de la réponse
|
||||||
|
const url = response.data.url ;
|
||||||
|
|
||||||
|
// Mettre en cache avec la clé composite
|
||||||
|
this.logoUrlCache.set(cacheKey, url);
|
||||||
|
|
||||||
|
return url;
|
||||||
|
}),
|
||||||
|
catchError(error => {
|
||||||
|
console.warn(`⚠️ Logo not found for merchant ${merchanPartnerId}: ${logoFileName}`, error);
|
||||||
|
|
||||||
|
// En cas d'erreur, ajouter au cache d'erreur
|
||||||
|
this.logoErrorCache.add(cacheKey);
|
||||||
|
|
||||||
|
// Générer un logo par défaut
|
||||||
|
const defaultLogo = this.getDefaultLogoUrl(merchantName);
|
||||||
|
|
||||||
|
// Mettre le logo par défaut dans le cache normal aussi
|
||||||
|
this.logoUrlCache.set(cacheKey, defaultLogo);
|
||||||
|
|
||||||
|
return of(defaultLogo);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Génère une URL de logo par défaut basée sur les initiales
|
||||||
|
*/
|
||||||
|
getDefaultLogoUrl(merchantName: string): string {
|
||||||
|
// Créer des initiales significatives
|
||||||
|
const initials = this.extractInitials(merchantName);
|
||||||
|
|
||||||
|
// Palette de couleurs agréables
|
||||||
|
const colors = [
|
||||||
|
'667eea', // Violet
|
||||||
|
'764ba2', // Violet foncé
|
||||||
|
'f56565', // Rouge
|
||||||
|
'4299e1', // Bleu
|
||||||
|
'48bb78', // Vert
|
||||||
|
'ed8936', // Orange
|
||||||
|
'FF6B6B', // Rouge clair
|
||||||
|
'4ECDC4', // Turquoise
|
||||||
|
'45B7D1', // Bleu clair
|
||||||
|
'96CEB4' // Vert menthe
|
||||||
|
];
|
||||||
|
|
||||||
|
const colorIndex = merchantName.length % colors.length;
|
||||||
|
const backgroundColor = colors[colorIndex];
|
||||||
|
|
||||||
|
// Taille fixe à 80px (l'API génère un carré de cette taille)
|
||||||
|
// L'image sera redimensionnée à 40px via CSS
|
||||||
|
return `https://ui-avatars.com/api/?name=${encodeURIComponent(initials)}&background=${backgroundColor}&color=FFFFFF&size=80`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gère les erreurs de chargement des logos MinIO
|
||||||
|
*/
|
||||||
|
onLogoError(event: Event, merchantName: string): void {
|
||||||
|
const img = event.target as HTMLImageElement;
|
||||||
|
|
||||||
|
if (!img) return;
|
||||||
|
|
||||||
|
console.warn('Logo MinIO failed to load, using default for:', merchantName);
|
||||||
|
|
||||||
|
img.onerror = null;
|
||||||
|
img.src = this.getDefaultLogoUrl(merchantName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gère les erreurs de chargement des logos par défaut
|
||||||
|
*/
|
||||||
|
onDefaultLogoError(event: Event | string): void {
|
||||||
|
if (!(event instanceof Event)) {
|
||||||
|
console.error('Default logo error (non-event):', event);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const img = event.target as HTMLImageElement | null;
|
||||||
|
if (!img) return;
|
||||||
|
|
||||||
|
console.error('Default logo also failed to load, using fallback SVG');
|
||||||
|
|
||||||
|
// SVG local
|
||||||
|
img.onerror = null; // éviter boucle infinie
|
||||||
|
img.src = 'assets/images/default-merchant-logo.svg';
|
||||||
|
|
||||||
|
// Dernier recours
|
||||||
|
img.onerror = (e) => {
|
||||||
|
if (!(e instanceof Event)) return;
|
||||||
|
const fallbackImg = e.target as HTMLImageElement | null;
|
||||||
|
if (!fallbackImg) return;
|
||||||
|
|
||||||
|
fallbackImg.onerror = null;
|
||||||
|
fallbackImg.src = this.generateFallbackDataUrl();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private extractUserRole(user: any): any {
|
||||||
|
const userRoles = this.authService.getCurrentUserRoles();
|
||||||
|
return userRoles && userRoles.length > 0 ? userRoles[0] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private checkIfHubUser(): boolean {
|
||||||
|
if (!this.currentUserRole) return false;
|
||||||
|
|
||||||
|
const hubRoles = [
|
||||||
|
UserRole.DCB_ADMIN,
|
||||||
|
UserRole.DCB_SUPPORT
|
||||||
|
];
|
||||||
|
|
||||||
|
return hubRoles.includes(this.currentUserRole);
|
||||||
|
}
|
||||||
|
|
||||||
private loadDropdownItems() {
|
private loadDropdownItems() {
|
||||||
this.menuItems = this.menuService.getUserDropdownItems()
|
this.menuItems = this.menuService.getUserDropdownItems()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private shouldUseCache(): boolean {
|
||||||
|
if (!this.merchantCache) return false;
|
||||||
|
|
||||||
|
const cacheAge = Date.now() - this.merchantCache.timestamp;
|
||||||
|
return cacheAge < this.CACHE_TTL && this.merchantCache.data !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private clearCache(): void {
|
||||||
|
this.merchantCache = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extrait les initiales de manière intelligente
|
||||||
|
*/
|
||||||
|
private extractInitials(name: string): string {
|
||||||
|
if (!name || name.trim() === '') {
|
||||||
|
return '??';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nettoyer le nom
|
||||||
|
const cleanedName = name.trim().toUpperCase();
|
||||||
|
|
||||||
|
// Extraire les mots
|
||||||
|
const words = cleanedName.split(/\s+/);
|
||||||
|
|
||||||
|
// Si un seul mot, prendre les deux premières lettres
|
||||||
|
if (words.length === 1) {
|
||||||
|
return words[0].substring(0, 2) || '??';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prendre la première lettre des deux premiers mots
|
||||||
|
const initials = words
|
||||||
|
.slice(0, 2) // Prendre les 2 premiers mots
|
||||||
|
.map(word => word[0] || '')
|
||||||
|
.join('');
|
||||||
|
|
||||||
|
return initials || name.substring(0, 2).toUpperCase() || '??';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Génère un fallback SVG en data URL
|
||||||
|
*/
|
||||||
|
private generateFallbackDataUrl(): string {
|
||||||
|
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40">
|
||||||
|
<rect width="40" height="40" fill="#667eea" rx="20"/>
|
||||||
|
<text x="20" y="22" text-anchor="middle" fill="white" font-family="Arial" font-size="14" font-weight="bold">?</text>
|
||||||
|
</svg>`;
|
||||||
|
|
||||||
|
return 'data:image/svg+xml;base64,' + btoa(svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== GESTION DES ERREURS ====================
|
||||||
|
|
||||||
|
private getErrorMessage(error: any): string {
|
||||||
|
if (error.error?.message) {
|
||||||
|
return error.error.message;
|
||||||
|
}
|
||||||
|
if (error.status === 400) {
|
||||||
|
return 'Données invalides. Vérifiez les informations saisies.';
|
||||||
|
}
|
||||||
|
if (error.status === 403) {
|
||||||
|
return 'Vous n\'avez pas les permissions nécessaires pour cette action';
|
||||||
|
}
|
||||||
|
if (error.status === 404) {
|
||||||
|
return 'Utilisateur non trouvé';
|
||||||
|
}
|
||||||
|
if (error.status === 409) {
|
||||||
|
return 'Cet email est déjà utilisé par un autre utilisateur';
|
||||||
|
}
|
||||||
|
return 'Une erreur est survenue. Veuillez réessayer.';
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -307,7 +307,7 @@ export class DcbReportingDashboard implements OnInit, OnDestroy, AfterViewInit {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private accessService: DashboardAccessService,
|
private accessService: DashboardAccessService,
|
||||||
private cdr: ChangeDetectorRef
|
private cdRef: ChangeDetectorRef
|
||||||
) {
|
) {
|
||||||
Chart.register(...registerables);
|
Chart.register(...registerables);
|
||||||
}
|
}
|
||||||
@ -326,12 +326,14 @@ export class DcbReportingDashboard implements OnInit, OnDestroy, AfterViewInit {
|
|||||||
console.log('✅ Dashboard: waitForReady() a émis - Initialisation...');
|
console.log('✅ Dashboard: waitForReady() a émis - Initialisation...');
|
||||||
this.dashboardInitialized = true;
|
this.dashboardInitialized = true;
|
||||||
this.initializeDashboard();
|
this.initializeDashboard();
|
||||||
|
this.cdRef.detectChanges();
|
||||||
},
|
},
|
||||||
error: (err) => {
|
error: (err) => {
|
||||||
console.error('❌ Dashboard: Erreur dans waitForReady():', err);
|
console.error('❌ Dashboard: Erreur dans waitForReady():', err);
|
||||||
// Gérer l'erreur - peut-être rediriger vers une page d'erreur
|
// Gérer l'erreur - peut-être rediriger vers une page d'erreur
|
||||||
this.addAlert('danger', 'Erreur d\'initialisation',
|
this.addAlert('danger', 'Erreur d\'initialisation',
|
||||||
'Impossible de charger les informations d\'accès', 'Maintenant');
|
'Impossible de charger les informations d\'accès', 'Maintenant');
|
||||||
|
this.cdRef.detectChanges();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@ -413,7 +415,7 @@ export class DcbReportingDashboard implements OnInit, OnDestroy, AfterViewInit {
|
|||||||
this.accessService.getAvailableMerchants().subscribe({
|
this.accessService.getAvailableMerchants().subscribe({
|
||||||
next: (merchants) => {
|
next: (merchants) => {
|
||||||
this.allowedMerchants = merchants;
|
this.allowedMerchants = merchants;
|
||||||
this.cdr.detectChanges();
|
this.cdRef.detectChanges();
|
||||||
},
|
},
|
||||||
error: (err) => {
|
error: (err) => {
|
||||||
console.error('Erreur lors du chargement des merchants:', err);
|
console.error('Erreur lors du chargement des merchants:', err);
|
||||||
@ -445,14 +447,14 @@ export class DcbReportingDashboard implements OnInit, OnDestroy, AfterViewInit {
|
|||||||
console.log('Données globales chargées avec succès');
|
console.log('Données globales chargées avec succès');
|
||||||
this.loading.globalData = false;
|
this.loading.globalData = false;
|
||||||
this.calculateStats();
|
this.calculateStats();
|
||||||
this.cdr.detectChanges();
|
this.cdRef.detectChanges();
|
||||||
setTimeout(() => this.updateAllCharts(), 100);
|
setTimeout(() => this.updateAllCharts(), 100);
|
||||||
},
|
},
|
||||||
error: (err) => {
|
error: (err) => {
|
||||||
console.error('Erreur lors du chargement des données globales:', err);
|
console.error('Erreur lors du chargement des données globales:', err);
|
||||||
this.loading.globalData = false;
|
this.loading.globalData = false;
|
||||||
this.addAlert('danger', 'Erreur de chargement', 'Impossible de charger les données globales', 'Maintenant');
|
this.addAlert('danger', 'Erreur de chargement', 'Impossible de charger les données globales', 'Maintenant');
|
||||||
this.cdr.detectChanges();
|
this.cdRef.detectChanges();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@ -485,7 +487,7 @@ export class DcbReportingDashboard implements OnInit, OnDestroy, AfterViewInit {
|
|||||||
console.log(`Données du merchant ${merchantId} chargées avec succès`);
|
console.log(`Données du merchant ${merchantId} chargées avec succès`);
|
||||||
this.loading.merchantData = false;
|
this.loading.merchantData = false;
|
||||||
this.calculateStats();
|
this.calculateStats();
|
||||||
this.cdr.detectChanges();
|
this.cdRef.detectChanges();
|
||||||
setTimeout(() => this.updateAllCharts(), 100);
|
setTimeout(() => this.updateAllCharts(), 100);
|
||||||
},
|
},
|
||||||
error: (err) => {
|
error: (err) => {
|
||||||
@ -493,7 +495,7 @@ export class DcbReportingDashboard implements OnInit, OnDestroy, AfterViewInit {
|
|||||||
this.loading.merchantData = false;
|
this.loading.merchantData = false;
|
||||||
this.addAlert('danger', 'Erreur de chargement',
|
this.addAlert('danger', 'Erreur de chargement',
|
||||||
`Impossible de charger les données du merchant ${merchantId}`, 'Maintenant');
|
`Impossible de charger les données du merchant ${merchantId}`, 'Maintenant');
|
||||||
this.cdr.detectChanges();
|
this.cdRef.detectChanges();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@ -730,12 +732,12 @@ export class DcbReportingDashboard implements OnInit, OnDestroy, AfterViewInit {
|
|||||||
(ctx as any).chart = newChart;
|
(ctx as any).chart = newChart;
|
||||||
|
|
||||||
this.loading.chart = false;
|
this.loading.chart = false;
|
||||||
this.cdr.detectChanges();
|
this.cdRef.detectChanges();
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Erreur lors de la création du graphique principal:', error);
|
console.error('Erreur lors de la création du graphique principal:', error);
|
||||||
this.loading.chart = false;
|
this.loading.chart = false;
|
||||||
this.cdr.detectChanges();
|
this.cdRef.detectChanges();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1109,13 +1111,13 @@ export class DcbReportingDashboard implements OnInit, OnDestroy, AfterViewInit {
|
|||||||
this.updateOverallHealth();
|
this.updateOverallHealth();
|
||||||
this.generateHealthAlerts();
|
this.generateHealthAlerts();
|
||||||
this.loading.healthCheck = false;
|
this.loading.healthCheck = false;
|
||||||
this.cdr.detectChanges();
|
this.cdRef.detectChanges();
|
||||||
}),
|
}),
|
||||||
catchError(err => {
|
catchError(err => {
|
||||||
console.error('Erreur lors du health check:', err);
|
console.error('Erreur lors du health check:', err);
|
||||||
this.addAlert('danger', 'Erreur de vérification', 'Impossible de vérifier la santé des services', 'Maintenant');
|
this.addAlert('danger', 'Erreur de vérification', 'Impossible de vérifier la santé des services', 'Maintenant');
|
||||||
this.loading.healthCheck = false;
|
this.loading.healthCheck = false;
|
||||||
this.cdr.detectChanges();
|
this.cdRef.detectChanges();
|
||||||
return of(null);
|
return of(null);
|
||||||
})
|
})
|
||||||
).subscribe()
|
).subscribe()
|
||||||
@ -1274,7 +1276,7 @@ export class DcbReportingDashboard implements OnInit, OnDestroy, AfterViewInit {
|
|||||||
this.metricDropdown.close();
|
this.metricDropdown.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.cdr.detectChanges();
|
this.cdRef.detectChanges();
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.updateMainChart();
|
this.updateMainChart();
|
||||||
@ -1284,7 +1286,7 @@ export class DcbReportingDashboard implements OnInit, OnDestroy, AfterViewInit {
|
|||||||
changePeriod(period: ReportPeriod): void {
|
changePeriod(period: ReportPeriod): void {
|
||||||
this.dataSelection.period = period;
|
this.dataSelection.period = period;
|
||||||
|
|
||||||
this.cdr.detectChanges();
|
this.cdRef.detectChanges();
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.updateMainChart();
|
this.updateMainChart();
|
||||||
@ -1294,7 +1296,7 @@ export class DcbReportingDashboard implements OnInit, OnDestroy, AfterViewInit {
|
|||||||
changeChartType(type: ChartType): void {
|
changeChartType(type: ChartType): void {
|
||||||
this.dataSelection.chartType = type;
|
this.dataSelection.chartType = type;
|
||||||
|
|
||||||
this.cdr.detectChanges();
|
this.cdRef.detectChanges();
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.updateMainChart();
|
this.updateMainChart();
|
||||||
@ -1302,7 +1304,7 @@ export class DcbReportingDashboard implements OnInit, OnDestroy, AfterViewInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
refreshChartData(): void {
|
refreshChartData(): void {
|
||||||
this.cdr.detectChanges();
|
this.cdRef.detectChanges();
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.updateAllCharts();
|
this.updateAllCharts();
|
||||||
}, 50);
|
}, 50);
|
||||||
|
|||||||
@ -68,13 +68,26 @@ export interface ReportParams {
|
|||||||
endDate?: string;
|
endDate?: string;
|
||||||
merchantPartnerId?: number;
|
merchantPartnerId?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HealthCheckStatus {
|
export interface HealthCheckStatus {
|
||||||
service: string;
|
service: string;
|
||||||
url: string;
|
url: string;
|
||||||
status: 'UP' | 'DOWN';
|
status: 'UP' | 'DOWN';
|
||||||
statusCode: number;
|
statusCode: number;
|
||||||
checkedAt: string;
|
checkedAt: string;
|
||||||
|
responseTime: string;
|
||||||
|
uptime?: number;
|
||||||
|
note?: string;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HealthCheckResponse {
|
||||||
|
summary: {
|
||||||
|
total: number;
|
||||||
|
up: number;
|
||||||
|
down: number;
|
||||||
|
timestamp: string;
|
||||||
|
};
|
||||||
|
details: HealthCheckStatus[];
|
||||||
}
|
}
|
||||||
|
|
||||||
// ChartDataNormalized : normalisation des données pour tous types de chart
|
// ChartDataNormalized : normalisation des données pour tous types de chart
|
||||||
|
|||||||
@ -150,7 +150,7 @@ export class DashboardAccessService {
|
|||||||
if (access.isHubUser) {
|
if (access.isHubUser) {
|
||||||
return this.merchantService.getAllMerchants().pipe(
|
return this.merchantService.getAllMerchants().pipe(
|
||||||
map(merchants => {
|
map(merchants => {
|
||||||
const available: AllowedMerchant[] = merchants.map(m => ({
|
const available: AllowedMerchant[] = merchants.items.map(m => ({
|
||||||
id: m.id,
|
id: m.id,
|
||||||
name: m.name
|
name: m.name
|
||||||
}));
|
}));
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import {
|
|||||||
SubscriptionReport,
|
SubscriptionReport,
|
||||||
SyncResponse,
|
SyncResponse,
|
||||||
HealthCheckStatus,
|
HealthCheckStatus,
|
||||||
|
HealthCheckResponse,
|
||||||
ChartDataNormalized
|
ChartDataNormalized
|
||||||
} from '../models/dcb-reporting.models';
|
} from '../models/dcb-reporting.models';
|
||||||
import { environment } from '@environments/environment';
|
import { environment } from '@environments/environment';
|
||||||
@ -282,7 +283,7 @@ export class ReportService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------
|
// ---------------------
|
||||||
// Health checks (rest of the code remains the same)
|
// Health checks
|
||||||
// ---------------------
|
// ---------------------
|
||||||
|
|
||||||
private checkApiAvailability(
|
private checkApiAvailability(
|
||||||
@ -316,6 +317,8 @@ export class ReportService {
|
|||||||
timeout(this.DEFAULT_TIMEOUT),
|
timeout(this.DEFAULT_TIMEOUT),
|
||||||
map((resp: HttpResponse<any>) => {
|
map((resp: HttpResponse<any>) => {
|
||||||
const finalResponseTime = Date.now() - startTime;
|
const finalResponseTime = Date.now() - startTime;
|
||||||
|
const body: any = resp.body;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
service,
|
service,
|
||||||
url,
|
url,
|
||||||
@ -323,6 +326,7 @@ export class ReportService {
|
|||||||
statusCode: resp.status,
|
statusCode: resp.status,
|
||||||
checkedAt: new Date().toISOString(),
|
checkedAt: new Date().toISOString(),
|
||||||
responseTime: `${finalResponseTime}ms`,
|
responseTime: `${finalResponseTime}ms`,
|
||||||
|
uptime: body?.uptime,
|
||||||
note: 'Used GET fallback'
|
note: 'Used GET fallback'
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
@ -397,16 +401,17 @@ export class ReportService {
|
|||||||
/**
|
/**
|
||||||
* Health check global de toutes les APIs
|
* Health check global de toutes les APIs
|
||||||
* Scanne chaque URL d'API directement
|
* Scanne chaque URL d'API directement
|
||||||
*/
|
*/
|
||||||
|
private buildHealthUrl(baseUrl: string): string {
|
||||||
|
return `${baseUrl.replace(/\/$/, '')}/health`;
|
||||||
|
}
|
||||||
|
|
||||||
globalHealthCheck(): Observable<HealthCheckStatus[]> {
|
globalHealthCheck(): Observable<HealthCheckStatus[]> {
|
||||||
const healthChecks: Observable<HealthCheckStatus>[] = [];
|
return forkJoin(
|
||||||
|
Object.entries(this.apiEndpoints).map(([service, url]) =>
|
||||||
// Vérifiez chaque service avec sa racine
|
this.checkApiAvailability(service, this.buildHealthUrl(url))
|
||||||
Object.entries(this.apiEndpoints).forEach(([service, url]) => {
|
)
|
||||||
healthChecks.push(this.checkApiAvailability(service, url));
|
);
|
||||||
});
|
|
||||||
|
|
||||||
return forkJoin(healthChecks);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -438,10 +443,7 @@ export class ReportService {
|
|||||||
/**
|
/**
|
||||||
* Health check détaillé avec métriques
|
* Health check détaillé avec métriques
|
||||||
*/
|
*/
|
||||||
detailedHealthCheck(): Observable<{
|
detailedHealthCheck(): Observable<HealthCheckResponse> {
|
||||||
summary: { total: number; up: number; down: number; timestamp: string };
|
|
||||||
details: HealthCheckStatus[];
|
|
||||||
}> {
|
|
||||||
return this.globalHealthCheck().pipe(
|
return this.globalHealthCheck().pipe(
|
||||||
map(results => {
|
map(results => {
|
||||||
const adjustedResults = results.map(result => ({
|
const adjustedResults = results.map(result => ({
|
||||||
@ -462,7 +464,24 @@ export class ReportService {
|
|||||||
details: adjustedResults
|
details: adjustedResults
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
catchError(err => this.handleError(err))
|
catchError(err => this.handleHealthError(err))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gestion des erreurs
|
||||||
|
*/
|
||||||
|
private handleHealthError(error: any): Observable<HealthCheckResponse> {
|
||||||
|
console.error('Health check error:', error);
|
||||||
|
|
||||||
|
return of({
|
||||||
|
summary: {
|
||||||
|
total: 0,
|
||||||
|
up: 0,
|
||||||
|
down: 0,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
},
|
||||||
|
details: []
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -8,7 +8,6 @@ import { Subject, takeUntil } from 'rxjs';
|
|||||||
import { HubUsersService } from './hub-users.service';
|
import { HubUsersService } from './hub-users.service';
|
||||||
import { RoleManagementService } from '@core/services/hub-users-roles-management.service';
|
import { RoleManagementService } from '@core/services/hub-users-roles-management.service';
|
||||||
import { AuthService } from '@core/services/auth.service';
|
import { AuthService } from '@core/services/auth.service';
|
||||||
import { MerchantSyncService } from './merchant-sync-orchestrator.service';
|
|
||||||
import { PageTitle } from '@app/components/page-title/page-title';
|
import { PageTitle } from '@app/components/page-title/page-title';
|
||||||
import { HubUsersList } from './hub-users-list/hub-users-list';
|
import { HubUsersList } from './hub-users-list/hub-users-list';
|
||||||
import { HubUserProfile } from './hub-users-profile/hub-users-profile';
|
import { HubUserProfile } from './hub-users-profile/hub-users-profile';
|
||||||
@ -39,7 +38,6 @@ export class HubUsersManagement implements OnInit, OnDestroy {
|
|||||||
private modalService = inject(NgbModal);
|
private modalService = inject(NgbModal);
|
||||||
private authService = inject(AuthService);
|
private authService = inject(AuthService);
|
||||||
private hubUsersService = inject(HubUsersService);
|
private hubUsersService = inject(HubUsersService);
|
||||||
private merchantSyncService = inject(MerchantSyncService);
|
|
||||||
protected roleService = inject(RoleManagementService);
|
protected roleService = inject(RoleManagementService);
|
||||||
private cdRef = inject(ChangeDetectorRef);
|
private cdRef = inject(ChangeDetectorRef);
|
||||||
private destroy$ = new Subject<void>();
|
private destroy$ = new Subject<void>();
|
||||||
@ -476,45 +474,6 @@ export class HubUsersManagement implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Crée un marchand dans MerchantConfig (séparément de l'utilisateur)
|
|
||||||
*/
|
|
||||||
createMerchant() {
|
|
||||||
// Validation du formulaire marchand
|
|
||||||
const merchantValidation = this.validateMerchantForm();
|
|
||||||
if (!merchantValidation.isValid) {
|
|
||||||
this.createUserError = merchantValidation.error!;
|
|
||||||
console.error('❌ Merchant form validation failed:', merchantValidation.error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.creatingMerchant = true;
|
|
||||||
|
|
||||||
console.log('📤 Creating merchant in MerchantConfig...');
|
|
||||||
|
|
||||||
this.merchantSyncService.createMerchantInConfigOnly(this.newMerchant)
|
|
||||||
.pipe(takeUntil(this.destroy$))
|
|
||||||
.subscribe({
|
|
||||||
next: (merchantConfig) => {
|
|
||||||
console.log('✅ Merchant created in MerchantConfig:', merchantConfig);
|
|
||||||
this.creatingMerchant = false;
|
|
||||||
|
|
||||||
// Optionnel: proposer d'associer un utilisateur au marchand créé
|
|
||||||
console.log(`✅ Merchant ID: ${merchantConfig.id} - Name: ${merchantConfig.name}`);
|
|
||||||
|
|
||||||
this.modalService.dismissAll();
|
|
||||||
this.resetMerchantForm();
|
|
||||||
this.cdRef.detectChanges();
|
|
||||||
},
|
|
||||||
error: (error) => {
|
|
||||||
console.error('❌ Error creating merchant in MerchantConfig:', error);
|
|
||||||
this.creatingMerchant = false;
|
|
||||||
this.createUserError = this.getMerchantErrorMessage(error);
|
|
||||||
this.cdRef.detectChanges();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Vérifie si le rôle est un rôle partenaire
|
* Vérifie si le rôle est un rôle partenaire
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -1,507 +0,0 @@
|
|||||||
import { Injectable, inject } from '@angular/core';
|
|
||||||
import { Observable, forkJoin, map, switchMap, catchError, of, throwError } from 'rxjs';
|
|
||||||
import {
|
|
||||||
CreateUserDto,
|
|
||||||
User,
|
|
||||||
UserType,
|
|
||||||
UserRole,
|
|
||||||
} from '@core/models/dcb-bo-hub-user.model';
|
|
||||||
|
|
||||||
import { MerchantUsersService} from '@modules/hub-users-management/merchant-users.service';
|
|
||||||
|
|
||||||
import { MerchantConfigService } from '@modules/merchant-config/merchant-config.service';
|
|
||||||
|
|
||||||
import {
|
|
||||||
CreateMerchantDto,
|
|
||||||
UpdateMerchantDto,
|
|
||||||
Merchant,
|
|
||||||
MerchantUser,
|
|
||||||
AddUserToMerchantDto,
|
|
||||||
UpdateUserRoleDto
|
|
||||||
} from '@core/models/merchant-config.model';
|
|
||||||
|
|
||||||
export interface MerchantSyncResult {
|
|
||||||
merchantConfig: Merchant;
|
|
||||||
keycloakUser?: User; // Utilisateur associé (optionnel)
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MerchantUserSyncResult {
|
|
||||||
keycloakUser: User;
|
|
||||||
merchantConfigUser?: MerchantUser;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MerchantSyncStatus {
|
|
||||||
existsInKeycloak: boolean;
|
|
||||||
existsInMerchantConfig: boolean;
|
|
||||||
usersSynced: boolean;
|
|
||||||
syncedUserCount: number;
|
|
||||||
totalUserCount: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UserMerchantAssociation {
|
|
||||||
userId: string;
|
|
||||||
merchantConfigId: string;
|
|
||||||
role: UserRole; // Rôle dans MerchantConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
|
||||||
export class MerchantSyncService {
|
|
||||||
private merchantUsersService = inject(MerchantUsersService);
|
|
||||||
private merchantConfigService = inject(MerchantConfigService);
|
|
||||||
|
|
||||||
// ==================== CONSTANTES ====================
|
|
||||||
|
|
||||||
private readonly KEYCLOAK_MERCHANT_ROLES = [
|
|
||||||
UserRole.DCB_PARTNER_ADMIN,
|
|
||||||
UserRole.DCB_PARTNER_MANAGER,
|
|
||||||
UserRole.DCB_PARTNER_SUPPORT
|
|
||||||
];
|
|
||||||
|
|
||||||
private readonly MERCHANT_CONFIG_ROLES = [
|
|
||||||
UserRole.MERCHANT_CONFIG_ADMIN,
|
|
||||||
UserRole.MERCHANT_CONFIG_MANAGER,
|
|
||||||
UserRole.MERCHANT_CONFIG_TECHNICAL,
|
|
||||||
UserRole.MERCHANT_CONFIG_VIEWER
|
|
||||||
];
|
|
||||||
|
|
||||||
private readonly ROLE_MAPPING: Map<UserRole, UserRole> = new Map([
|
|
||||||
// Keycloak -> Merchant Config
|
|
||||||
[UserRole.DCB_PARTNER_ADMIN, UserRole.MERCHANT_CONFIG_ADMIN],
|
|
||||||
[UserRole.DCB_PARTNER_MANAGER, UserRole.MERCHANT_CONFIG_MANAGER],
|
|
||||||
[UserRole.DCB_PARTNER_SUPPORT, UserRole.MERCHANT_CONFIG_VIEWER],
|
|
||||||
|
|
||||||
// Merchant Config -> Keycloak
|
|
||||||
[UserRole.MERCHANT_CONFIG_ADMIN, UserRole.DCB_PARTNER_ADMIN],
|
|
||||||
[UserRole.MERCHANT_CONFIG_MANAGER, UserRole.DCB_PARTNER_MANAGER],
|
|
||||||
[UserRole.MERCHANT_CONFIG_TECHNICAL, UserRole.DCB_PARTNER_SUPPORT],
|
|
||||||
[UserRole.MERCHANT_CONFIG_VIEWER, UserRole.DCB_PARTNER_SUPPORT]
|
|
||||||
]);
|
|
||||||
|
|
||||||
// ==================== MÉTHODES DE BASE ====================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* CREATE - Créer un merchant uniquement dans MerchantConfig
|
|
||||||
*/
|
|
||||||
createMerchantInConfigOnly(merchantData: CreateMerchantDto): Observable<Merchant> {
|
|
||||||
console.log('📝 CREATE Merchant dans MerchantConfig seulement...');
|
|
||||||
|
|
||||||
return this.merchantConfigService.createMerchant(merchantData).pipe(
|
|
||||||
map(merchant => {
|
|
||||||
console.log('✅ Merchant créé dans MerchantConfig:', merchant);
|
|
||||||
return merchant;
|
|
||||||
}),
|
|
||||||
catchError(error => {
|
|
||||||
console.error('❌ Échec création merchant dans MerchantConfig:', error);
|
|
||||||
return throwError(() => error);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* CREATE - Créer un utilisateur dans Keycloak (sans association)
|
|
||||||
*/
|
|
||||||
createKeycloakUser(
|
|
||||||
userData: {
|
|
||||||
username: string;
|
|
||||||
email: string;
|
|
||||||
password: string;
|
|
||||||
firstName: string;
|
|
||||||
lastName: string;
|
|
||||||
role: UserRole;
|
|
||||||
enabled?: boolean;
|
|
||||||
emailVerified?: boolean;
|
|
||||||
}
|
|
||||||
): Observable<User> {
|
|
||||||
console.log('📝 CREATE User dans Keycloak...');
|
|
||||||
|
|
||||||
// Déterminer le type d'utilisateur selon le rôle
|
|
||||||
const userType = this.isMerchantRole(userData.role)
|
|
||||||
? UserType.MERCHANT_PARTNER
|
|
||||||
: UserType.HUB;
|
|
||||||
|
|
||||||
const keycloakUserDto: CreateUserDto = {
|
|
||||||
username: userData.username,
|
|
||||||
email: userData.email,
|
|
||||||
firstName: userData.firstName,
|
|
||||||
lastName: userData.lastName,
|
|
||||||
password: userData.password,
|
|
||||||
userType: userType,
|
|
||||||
role: userData.role,
|
|
||||||
enabled: userData.enabled ?? true,
|
|
||||||
emailVerified: userData.emailVerified ?? false
|
|
||||||
};
|
|
||||||
|
|
||||||
// Sélectionner le service approprié selon le type d'utilisateur
|
|
||||||
const createService = this.merchantUsersService.createMerchantUser(keycloakUserDto);
|
|
||||||
|
|
||||||
return createService.pipe(
|
|
||||||
map(user => {
|
|
||||||
console.log('✅ Utilisateur créé dans Keycloak:', user);
|
|
||||||
return user;
|
|
||||||
}),
|
|
||||||
catchError(error => {
|
|
||||||
console.error('❌ Échec création utilisateur dans Keycloak:', error);
|
|
||||||
return throwError(() => error);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* READ - Récupérer un merchant depuis MerchantConfig seulement
|
|
||||||
*/
|
|
||||||
getMerchantFromConfigOnly(merchantConfigId: string): Observable<Merchant> {
|
|
||||||
console.log('🔍 READ Merchant depuis MerchantConfig seulement...');
|
|
||||||
|
|
||||||
return this.merchantConfigService.getMerchantById(Number(merchantConfigId)).pipe(
|
|
||||||
map(merchant => {
|
|
||||||
console.log('✅ Merchant récupéré depuis MerchantConfig:', merchant);
|
|
||||||
return merchant;
|
|
||||||
}),
|
|
||||||
catchError(error => {
|
|
||||||
console.error('❌ Erreur récupération merchant depuis MerchantConfig:', error);
|
|
||||||
return throwError(() => error);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* READ - Récupérer tous les merchants depuis MerchantConfig
|
|
||||||
*/
|
|
||||||
getAllMerchantsFromConfig(): Observable<Merchant[]> {
|
|
||||||
console.log('🔍 READ All Merchants depuis MerchantConfig...');
|
|
||||||
|
|
||||||
return this.merchantConfigService.getAllMerchants().pipe(
|
|
||||||
map(merchants => {
|
|
||||||
console.log(`✅ ${merchants.length} merchants récupérés depuis MerchantConfig`);
|
|
||||||
return merchants;
|
|
||||||
}),
|
|
||||||
catchError(error => {
|
|
||||||
console.error('❌ Erreur récupération merchants depuis MerchantConfig:', error);
|
|
||||||
return throwError(() => error);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== ASSOCIATION UTILISATEUR-MERCHANT ====================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Associer un utilisateur existant à un marchand
|
|
||||||
*/
|
|
||||||
associateUserToMerchant(
|
|
||||||
association: UserMerchantAssociation
|
|
||||||
): Observable<MerchantUserSyncResult> {
|
|
||||||
console.log('🔗 ASSOCIATE User to Merchant...');
|
|
||||||
|
|
||||||
return forkJoin({
|
|
||||||
user: this.getUserById(association.userId),
|
|
||||||
merchant: this.merchantConfigService.getMerchantById(Number(association.merchantConfigId))
|
|
||||||
}).pipe(
|
|
||||||
switchMap(({ user, merchant }) => {
|
|
||||||
console.log(`🔗 Associating user ${user.username} to merchant ${merchant.name}`);
|
|
||||||
|
|
||||||
// 2. Ajouter l'utilisateur à MerchantConfig
|
|
||||||
const merchantConfigUserDto: AddUserToMerchantDto = {
|
|
||||||
userId: user.id,
|
|
||||||
role: association.role,
|
|
||||||
merchantPartnerId: Number(association.merchantConfigId),
|
|
||||||
};
|
|
||||||
|
|
||||||
return this.merchantConfigService.addUserToMerchant(merchantConfigUserDto).pipe(
|
|
||||||
map(merchantConfigUser => {
|
|
||||||
console.log('✅ User added to Merchant Config:', merchantConfigUser);
|
|
||||||
return {
|
|
||||||
keycloakUser: user,
|
|
||||||
merchantConfigUser
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
catchError(error => {
|
|
||||||
console.error('❌ Error adding user to MerchantConfig:', error);
|
|
||||||
return throwError(() => error);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
catchError(error => {
|
|
||||||
console.error('❌ Error in association process:', error);
|
|
||||||
return throwError(() => error);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dissocier un utilisateur d'un marchand
|
|
||||||
*/
|
|
||||||
dissociateUserFromMerchant(
|
|
||||||
userId: string,
|
|
||||||
merchantConfigId: string
|
|
||||||
): Observable<{ success: boolean; message: string }> {
|
|
||||||
console.log('🔗 DISSOCIATE User from Merchant...');
|
|
||||||
|
|
||||||
return forkJoin({
|
|
||||||
// Retirer l'utilisateur de MerchantConfig
|
|
||||||
merchantConfigRemoval: this.merchantConfigService.removeUserFromMerchant(
|
|
||||||
Number(merchantConfigId),
|
|
||||||
userId
|
|
||||||
).pipe(catchError(() => of({ ignored: true })))
|
|
||||||
}).pipe(
|
|
||||||
map(() => ({
|
|
||||||
success: true,
|
|
||||||
message: 'Utilisateur dissocié du marchand avec succès'
|
|
||||||
})),
|
|
||||||
catchError(async (error) => ({
|
|
||||||
success: false,
|
|
||||||
message: `Erreur lors de la dissociation: ${error.message}`
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Récupérer les utilisateurs associés à un marchand
|
|
||||||
*/
|
|
||||||
getUsersByMerchant(merchantConfigId: string): Observable<User[]> {
|
|
||||||
console.log('🔍 READ Users by Merchant...');
|
|
||||||
|
|
||||||
// Récupérer les utilisateurs de MerchantConfig
|
|
||||||
return this.merchantConfigService.getMerchantUsers(Number(merchantConfigId)).pipe(
|
|
||||||
switchMap(merchantConfigUsers => {
|
|
||||||
if (merchantConfigUsers.total === 0) {
|
|
||||||
return of([]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Récupérer les détails de chaque utilisateur depuis Keycloak
|
|
||||||
const userObservables = merchantConfigUsers.items.map((mcUser: { userId: string; }) =>
|
|
||||||
this.getUserById(mcUser.userId).pipe(
|
|
||||||
catchError(() => of(null)) // Ignorer les utilisateurs non trouvés
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
return forkJoin(userObservables).pipe(
|
|
||||||
map(users => users.filter((user): user is User => user !== null))
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
map(users => {
|
|
||||||
console.log(`✅ ${users.length} utilisateurs trouvés pour merchant ${merchantConfigId}`);
|
|
||||||
return users;
|
|
||||||
}),
|
|
||||||
catchError(error => {
|
|
||||||
console.error('❌ Erreur récupération utilisateurs:', error);
|
|
||||||
return throwError(() => error);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== MÉTHODES DE GESTION ====================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* UPDATE - Mettre à jour un merchant dans MerchantConfig seulement
|
|
||||||
*/
|
|
||||||
updateMerchantInConfigOnly(
|
|
||||||
merchantConfigId: string,
|
|
||||||
updates: UpdateMerchantDto
|
|
||||||
): Observable<Merchant> {
|
|
||||||
console.log('✏️ UPDATE Merchant dans MerchantConfig seulement...');
|
|
||||||
|
|
||||||
return this.merchantConfigService.updateMerchant(
|
|
||||||
Number(merchantConfigId),
|
|
||||||
updates
|
|
||||||
).pipe(
|
|
||||||
map(merchant => {
|
|
||||||
console.log('✅ Merchant mis à jour dans MerchantConfig:', merchant);
|
|
||||||
return merchant;
|
|
||||||
}),
|
|
||||||
catchError(error => {
|
|
||||||
console.error('❌ Erreur mise à jour merchant dans MerchantConfig:', error);
|
|
||||||
return throwError(() => error);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* UPDATE - Mettre à jour un utilisateur dans Keycloak
|
|
||||||
*/
|
|
||||||
updateKeycloakUserRole(
|
|
||||||
keycloakUserId: string,
|
|
||||||
newRole: UserRole
|
|
||||||
): Observable<UpdateUserRoleDto> {
|
|
||||||
console.log('✏️ UPDATE User dans Keycloak...');
|
|
||||||
|
|
||||||
return this.getUserById(keycloakUserId).pipe(
|
|
||||||
switchMap(user => {
|
|
||||||
const updateService = this.merchantUsersService.updateMerchantUserRole(keycloakUserId, newRole);
|
|
||||||
|
|
||||||
return updateService.pipe(
|
|
||||||
map(updatedUser => {
|
|
||||||
console.log('✅ Utilisateur mis à jour dans Keycloak:', updatedUser);
|
|
||||||
return updatedUser;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
catchError(error => {
|
|
||||||
console.error('❌ Erreur mise à jour utilisateur dans Keycloak:', error);
|
|
||||||
return throwError(() => error);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* UPDATE - Changer le rôle d'un utilisateur dans MerchantConfig
|
|
||||||
*/
|
|
||||||
updateUserRoleInMerchantConfig(
|
|
||||||
merchantConfigId: string,
|
|
||||||
userId: string,
|
|
||||||
newRole: UserRole
|
|
||||||
): Observable<MerchantUser> {
|
|
||||||
console.log('✏️ UPDATE User Role dans MerchantConfig...');
|
|
||||||
|
|
||||||
const updateRoleDto: UpdateUserRoleDto = {
|
|
||||||
role: newRole
|
|
||||||
};
|
|
||||||
|
|
||||||
this.updateKeycloakUserRole(userId, newRole)
|
|
||||||
|
|
||||||
return this.merchantConfigService.updateUserRole(
|
|
||||||
Number(merchantConfigId),
|
|
||||||
userId,
|
|
||||||
updateRoleDto
|
|
||||||
).pipe(
|
|
||||||
map(merchantConfigUser => {
|
|
||||||
console.log('✅ Rôle utilisateur mis à jour dans MerchantConfig:', merchantConfigUser);
|
|
||||||
return merchantConfigUser;
|
|
||||||
}),
|
|
||||||
catchError(error => {
|
|
||||||
console.error('❌ Erreur changement rôle utilisateur dans MerchantConfig:', error);
|
|
||||||
return throwError(() => error);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* DELETE - Supprimer un merchant de MerchantConfig seulement
|
|
||||||
*/
|
|
||||||
deleteMerchantFromConfigOnly(
|
|
||||||
merchantConfigId: string
|
|
||||||
): Observable<{ success: boolean; message: string }> {
|
|
||||||
console.log('🗑️ DELETE Merchant de MerchantConfig seulement...');
|
|
||||||
|
|
||||||
return this.merchantConfigService.deleteMerchant(Number(merchantConfigId)).pipe(
|
|
||||||
map(() => ({
|
|
||||||
success: true,
|
|
||||||
message: 'Merchant supprimé de MerchantConfig avec succès'
|
|
||||||
})),
|
|
||||||
catchError(async (error) => ({
|
|
||||||
success: false,
|
|
||||||
message: `Erreur suppression merchant: ${error.message}`
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* DELETE - Supprimer un utilisateur de Keycloak
|
|
||||||
*/
|
|
||||||
deleteKeycloakUser(
|
|
||||||
userId: string
|
|
||||||
): Observable<{ success: boolean; message: string }> {
|
|
||||||
console.log('🗑️ DELETE User de Keycloak...');
|
|
||||||
|
|
||||||
return this.getUserById(userId).pipe(
|
|
||||||
switchMap(user => {
|
|
||||||
const deleteService = this.merchantUsersService.deleteMerchantUser(userId);
|
|
||||||
|
|
||||||
return deleteService.pipe(
|
|
||||||
map(() => ({
|
|
||||||
success: true,
|
|
||||||
message: 'Utilisateur supprimé de Keycloak avec succès'
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
catchError(async (error) => ({
|
|
||||||
success: false,
|
|
||||||
message: `Erreur suppression utilisateur: ${error.message}`
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== MÉTHODES DE RECHERCHE ====================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Rechercher des merchants dans MerchantConfig
|
|
||||||
*/
|
|
||||||
searchMerchantsInConfig(query: string): Observable<Merchant[]> {
|
|
||||||
console.log('🔍 SEARCH Merchants dans MerchantConfig...');
|
|
||||||
|
|
||||||
return this.merchantConfigService.getAllMerchants({ query }).pipe(
|
|
||||||
map(merchants => {
|
|
||||||
console.log(`✅ ${merchants.length} merchants trouvés avec "${query}"`);
|
|
||||||
return merchants;
|
|
||||||
}),
|
|
||||||
catchError(error => {
|
|
||||||
console.error('❌ Erreur recherche merchants dans MerchantConfig:', error);
|
|
||||||
return throwError(() => error);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Rechercher des utilisateurs dans Keycloak
|
|
||||||
*/
|
|
||||||
searchKeycloakUsers(query: string): Observable<User[]> {
|
|
||||||
console.log('🔍 SEARCH Users dans Keycloak...');
|
|
||||||
|
|
||||||
// Rechercher dans les deux types d'utilisateurs
|
|
||||||
return forkJoin({
|
|
||||||
merchantUsers: this.merchantUsersService.searchMerchantUsers({ query }).pipe(
|
|
||||||
catchError(() => of([]))
|
|
||||||
)
|
|
||||||
}).pipe(
|
|
||||||
map(({ merchantUsers }) => {
|
|
||||||
const allUsers = [
|
|
||||||
...merchantUsers
|
|
||||||
];
|
|
||||||
console.log(`✅ ${allUsers.length} utilisateurs trouvés avec "${query}"`);
|
|
||||||
return allUsers;
|
|
||||||
}),
|
|
||||||
catchError(error => {
|
|
||||||
console.error('❌ Erreur recherche utilisateurs:', error);
|
|
||||||
return throwError(() => error);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== MÉTHODES PRIVÉES ====================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Récupérer un utilisateur par ID (gère les deux types)
|
|
||||||
*/
|
|
||||||
private getUserById(userId: string): Observable<User> {
|
|
||||||
return forkJoin({
|
|
||||||
merchantUser: this.merchantUsersService.getMerchantUserById(userId).pipe(
|
|
||||||
catchError(() => of(null))
|
|
||||||
)
|
|
||||||
}).pipe(
|
|
||||||
map(({ merchantUser }) => {
|
|
||||||
if (merchantUser) return merchantUser;
|
|
||||||
throw new Error(`Utilisateur ${userId} non trouvé`);
|
|
||||||
}),
|
|
||||||
catchError(error => {
|
|
||||||
console.error(`❌ Erreur récupération utilisateur ${userId}:`, error);
|
|
||||||
return throwError(() => error);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Vérifie si le rôle est un rôle partenaire
|
|
||||||
*/
|
|
||||||
private isMerchantRole(role: UserRole): boolean {
|
|
||||||
return [
|
|
||||||
UserRole.DCB_PARTNER_ADMIN,
|
|
||||||
UserRole.DCB_PARTNER_MANAGER,
|
|
||||||
UserRole.DCB_PARTNER_SUPPORT
|
|
||||||
].includes(role);
|
|
||||||
}
|
|
||||||
|
|
||||||
private mapToMerchantConfigRole(keycloakRole: UserRole): UserRole {
|
|
||||||
const mappedRole = this.ROLE_MAPPING.get(keycloakRole);
|
|
||||||
return mappedRole || UserRole.MERCHANT_CONFIG_VIEWER;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -3,7 +3,7 @@ import { CommonModule } from '@angular/common';
|
|||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { NgIcon } from '@ng-icons/core';
|
import { NgIcon } from '@ng-icons/core';
|
||||||
import { NgbNavModule, NgbModal, NgbModalModule } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbNavModule, NgbModal, NgbModalModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { catchError, map, of, Subject, switchMap, takeUntil } from 'rxjs';
|
import { catchError, map, of, Subject, switchMap, takeUntil, throwError } from 'rxjs';
|
||||||
|
|
||||||
import { MerchantUsersService } from './merchant-users.service';
|
import { MerchantUsersService } from './merchant-users.service';
|
||||||
import { RoleManagementService } from '@core/services/hub-users-roles-management.service';
|
import { RoleManagementService } from '@core/services/hub-users-roles-management.service';
|
||||||
@ -346,23 +346,49 @@ export class MerchantUsersManagement implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private loadAllMerchants(): void {
|
private loadAllMerchants(): void {
|
||||||
|
if (this.isMerchantUser) {
|
||||||
|
console.log('⚠️ User is not a Hub user, merchant list not displayed');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.loadingMerchantPartners = true;
|
this.loadingMerchantPartners = true;
|
||||||
this.merchantPartnersError = '';
|
this.merchantPartnersError = '';
|
||||||
|
this.merchantPartners = [];
|
||||||
|
|
||||||
this.merchantConfigService.getAllMerchants()
|
const pageSize = 10; // batch size identique au backend
|
||||||
|
let currentPage = 1;
|
||||||
|
|
||||||
|
const loadNextPage = () => {
|
||||||
|
this.merchantConfigService.getAllMerchants(currentPage, pageSize)
|
||||||
.pipe(takeUntil(this.destroy$))
|
.pipe(takeUntil(this.destroy$))
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: (merchants) => {
|
next: response => {
|
||||||
this.merchantPartners = merchants;
|
const items = Array.isArray(response.items) ? response.items : [];
|
||||||
this.loadingMerchantPartners = false;
|
this.merchantPartners.push(...items);
|
||||||
console.log('✅ All merchants loaded for Hub Admin:', merchants.length);
|
|
||||||
|
const loadedCount = this.merchantPartners.length;
|
||||||
|
const totalItems = response.total;
|
||||||
|
|
||||||
|
console.log(`📥 Page ${currentPage} chargée (${loadedCount}/${totalItems} merchants)`);
|
||||||
|
|
||||||
|
if (loadedCount < totalItems) {
|
||||||
|
currentPage++;
|
||||||
|
loadNextPage();
|
||||||
|
} else {
|
||||||
|
this.loadingMerchantPartners = false;
|
||||||
|
console.log(`✅ Tous les merchants chargés: ${loadedCount}`);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
error: (error) => {
|
error: err => {
|
||||||
console.error('❌ Error loading all merchants:', error);
|
console.error('❌ Error loading merchants:', err);
|
||||||
this.merchantPartnersError = 'Erreur lors du chargement des merchants';
|
this.merchantPartnersError = 'Erreur lors du chargement des merchants';
|
||||||
this.loadingMerchantPartners = false;
|
this.loadingMerchantPartners = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
loadNextPage();
|
||||||
}
|
}
|
||||||
|
|
||||||
getCurrentUserMerchantName(): string {
|
getCurrentUserMerchantName(): string {
|
||||||
@ -571,15 +597,19 @@ export class MerchantUsersManagement implements OnInit, OnDestroy {
|
|||||||
enabled: this.newUser.enabled,
|
enabled: this.newUser.enabled,
|
||||||
emailVerified: this.newUser.emailVerified,
|
emailVerified: this.newUser.emailVerified,
|
||||||
userType: this.newUser.userType,
|
userType: this.newUser.userType,
|
||||||
merchantPartnerId: this.newUser.merchantPartnerId // Passer l'ID du merchant
|
merchantPartnerId: this.newUser.merchantPartnerId
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Variable pour stocker l'ID de l'utilisateur créé en cas de rollback
|
||||||
|
let createdUserId: string | null = null;
|
||||||
|
|
||||||
this.merchantUsersService.createMerchantUser(userDto)
|
this.merchantUsersService.createMerchantUser(userDto)
|
||||||
.pipe(
|
.pipe(
|
||||||
switchMap((createdKeycloakUser) => {
|
switchMap((createdKeycloakUser) => {
|
||||||
console.log('✅ Keycloak user created successfully:', createdKeycloakUser);
|
console.log('✅ Keycloak user created successfully:', createdKeycloakUser);
|
||||||
|
createdUserId = createdKeycloakUser.id;
|
||||||
|
|
||||||
// 2. Ajouter l'utilisateur au merchant dans MerchantConfig
|
// 2. Si c'est un utilisateur Merchant, l'ajouter au merchant dans MerchantConfig
|
||||||
if (this.isMerchantRole(this.newUser.role) && this.newUser.merchantPartnerId) {
|
if (this.isMerchantRole(this.newUser.role) && this.newUser.merchantPartnerId) {
|
||||||
const merchantPartnerId = Number(this.newUser.merchantPartnerId);
|
const merchantPartnerId = Number(this.newUser.merchantPartnerId);
|
||||||
|
|
||||||
@ -589,38 +619,94 @@ export class MerchantUsersManagement implements OnInit, OnDestroy {
|
|||||||
merchantPartnerId: merchantPartnerId
|
merchantPartnerId: merchantPartnerId
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log('📤 Adding user to merchant config:', addUserDto);
|
|
||||||
return this.merchantConfigService.addUserToMerchant(addUserDto).pipe(
|
return this.merchantConfigService.addUserToMerchant(addUserDto).pipe(
|
||||||
map((merchantConfigUser) => {
|
map((merchantConfigUser) => {
|
||||||
return {
|
return {
|
||||||
keycloakUser: createdKeycloakUser,
|
keycloakUser: createdKeycloakUser,
|
||||||
merchantConfigUser
|
merchantConfigUser,
|
||||||
|
success: true
|
||||||
};
|
};
|
||||||
|
}),
|
||||||
|
catchError((merchantError) => {
|
||||||
|
console.error('❌ Failed to add user to merchant config:', merchantError);
|
||||||
|
|
||||||
|
// ROLLBACK: Supprimer l'utilisateur Keycloak créé
|
||||||
|
if (createdUserId) {
|
||||||
|
console.log(`🔄 Rollback: Deleting Keycloak user ${createdUserId} because merchant association failed`);
|
||||||
|
|
||||||
|
return this.merchantUsersService.deleteMerchantUser(createdUserId).pipe(
|
||||||
|
switchMap(() => {
|
||||||
|
console.log(`✅ Keycloak user ${createdUserId} deleted as part of rollback`);
|
||||||
|
// On propage l'erreur originale en créant un Observable qui échoue
|
||||||
|
return throwError(() => new Error(`Failed to associate user with merchant: ${merchantError.message}. User creation rolled back.`));
|
||||||
|
}),
|
||||||
|
catchError((deleteError) => {
|
||||||
|
console.error(`❌ Failed to delete Keycloak user during rollback:`, deleteError);
|
||||||
|
return throwError(() => new Error(`Failed to associate user with merchant: ${merchantError.message}. AND failed to rollback user creation: ${deleteError.message}`));
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si createdUserId est null, on ne peut pas rollback
|
||||||
|
throw merchantError;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return of({ keycloakUser: createdKeycloakUser });
|
// Si pas d'association nécessaire (non-Merchant user), retourner directement
|
||||||
|
return of({
|
||||||
|
keycloakUser: createdKeycloakUser,
|
||||||
|
merchantConfigUser: null,
|
||||||
|
success: true
|
||||||
|
});
|
||||||
}),
|
}),
|
||||||
takeUntil(this.destroy$)
|
takeUntil(this.destroy$)
|
||||||
)
|
)
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: (result) => {
|
next: (result) => {
|
||||||
console.log('✅ Complete user creation successful:', result);
|
if (result.success) {
|
||||||
this.creatingUser = false;
|
console.log('✅ Complete user creation successful:', result);
|
||||||
this.modalService.dismissAll();
|
this.creatingUser = false;
|
||||||
this.refreshUsersList();
|
this.modalService.dismissAll();
|
||||||
this.cdRef.detectChanges();
|
this.refreshUsersList();
|
||||||
|
this.cdRef.detectChanges();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
error: (error) => {
|
error: (error) => {
|
||||||
console.error('❌ Error in user creation process:', error);
|
console.error('❌ Error in user creation process:', error);
|
||||||
this.creatingUser = false;
|
this.creatingUser = false;
|
||||||
this.createUserError = this.getErrorMessage(error);
|
|
||||||
this.cdRef.detectChanges();
|
// Déterminer le message d'erreur approprié
|
||||||
|
if (error.message.includes('rolled back')) {
|
||||||
|
// Erreur avec rollback réussi
|
||||||
|
this.createUserError = error.message;
|
||||||
|
console.log('⚠️ User creation rolled back successfully');
|
||||||
|
} else if (createdUserId && !error.message.includes('failed to rollback')) {
|
||||||
|
// L'utilisateur a été créé mais une autre erreur est survenue
|
||||||
|
// Tentative de nettoyage de l'utilisateur orphelin
|
||||||
|
console.log(`⚠️ Attempting to clean up orphaned user: ${createdUserId}`);
|
||||||
|
this.merchantUsersService.deleteMerchantUser(createdUserId).subscribe({
|
||||||
|
next: () => {
|
||||||
|
console.log(`✅ Orphaned user cleaned up: ${createdUserId}`);
|
||||||
|
this.createUserError = `User creation failed after user was created. Orphaned user ${createdUserId} has been cleaned up. Error: ${error.message}`;
|
||||||
|
this.cdRef.detectChanges();
|
||||||
|
},
|
||||||
|
error: (cleanupError) => {
|
||||||
|
console.error(`❌ Failed to cleanup orphaned user:`, cleanupError);
|
||||||
|
this.createUserError = `User creation failed and cleanup of orphaned user ${createdUserId} also failed. Please contact admin. Original error: ${error.message}`;
|
||||||
|
this.cdRef.detectChanges();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Autre erreur
|
||||||
|
this.createUserError = this.getErrorMessage(error);
|
||||||
|
this.cdRef.detectChanges();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Méthode pour trouver le merchantPartnerId de l'utilisateur connecté
|
// Méthode pour trouver le merchantPartnerId de l'utilisateur connecté
|
||||||
private findMerchantPartnerIdForCurrentUser(): void {
|
private findMerchantPartnerIdForCurrentUser(): void {
|
||||||
const currentUserId = this.authService.getCurrentUserId();
|
const currentUserId = this.authService.getCurrentUserId();
|
||||||
@ -791,16 +877,28 @@ export class MerchantUsersManagement implements OnInit, OnDestroy {
|
|||||||
if (error.error?.message) {
|
if (error.error?.message) {
|
||||||
return error.error.message;
|
return error.error.message;
|
||||||
}
|
}
|
||||||
if (error.status === 400) {
|
|
||||||
return 'Données invalides. Vérifiez les champs du formulaire.';
|
|
||||||
}
|
|
||||||
if (error.status === 409) {
|
if (error.status === 409) {
|
||||||
return 'Un utilisateur avec ce nom d\'utilisateur ou email existe déjà.';
|
return 'Un utilisateur avec ce nom d\'utilisateur ou email existe déjà.';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error.status === 403) {
|
if (error.status === 403) {
|
||||||
return 'Vous n\'avez pas les permissions nécessaires pour cette action.';
|
return 'Vous n\'avez pas les permissions nécessaires pour cette action.';
|
||||||
}
|
}
|
||||||
return 'Erreur lors de la création de l\'utilisateur. Veuillez réessayer.';
|
|
||||||
|
if (error.status === 400) {
|
||||||
|
return 'Données invalides. Vérifiez les informations saisies';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error.status === 404) {
|
||||||
|
return 'Le merchant spécifié n\'existe pas';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error.status === 0 || error.status === 503) {
|
||||||
|
return 'Service temporairement indisponible. Veuillez réessayer plus tard';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Une erreur inattendue est survenue lors de l\'Operation';
|
||||||
}
|
}
|
||||||
|
|
||||||
private getResetPasswordErrorMessage(error: any): string {
|
private getResetPasswordErrorMessage(error: any): string {
|
||||||
|
|||||||
@ -1 +0,0 @@
|
|||||||
<p>Integrations</p>
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
import { Integrations } from './integrations';
|
|
||||||
describe('Integrations', () => {});
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
import { Component } from '@angular/core';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-integrations',
|
|
||||||
templateUrl: './integrations.html',
|
|
||||||
})
|
|
||||||
export class Integrations {}
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
import { Injectable } from '@angular/core';
|
|
||||||
|
|
||||||
@Injectable({
|
|
||||||
providedIn: 'root'
|
|
||||||
})
|
|
||||||
export class IntegrationsService {
|
|
||||||
constructor() {}
|
|
||||||
}
|
|
||||||
@ -180,20 +180,40 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
@if (merchant.logo) {
|
<!-- ==================== LOGO AVEC ICÔNE ==================== -->
|
||||||
<img
|
<div class="d-flex align-items-center me-3">
|
||||||
[src]="merchant.logo"
|
<!-- Icône fixe à gauche -->
|
||||||
alt="Logo {{ merchant.name }}"
|
<div class="avatar-sm bg-primary bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center me-2">
|
||||||
class="avatar-sm rounded-circle me-2"
|
<ng-icon name="lucideStore" class="text-primary fs-12"></ng-icon>
|
||||||
onerror="this.style.display='none'"
|
</div>
|
||||||
>
|
|
||||||
}
|
<!-- Logo du marchand -->
|
||||||
<div class="avatar-sm bg-primary bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center me-2">
|
<div>
|
||||||
<ng-icon name="lucideStore" class="text-primary fs-12"></ng-icon>
|
@if (merchant.logo && merchant.logo.trim() !== '') {
|
||||||
|
<img
|
||||||
|
[src]="getMerchantLogoUrl(merchant.id, merchant.logo, merchant.name) | async"
|
||||||
|
[alt]="merchant.name + ' logo'"
|
||||||
|
class="rounded-circle"
|
||||||
|
style="width: 40px; height: 40px; object-fit: cover; border: 2px solid #f0f0f0;"
|
||||||
|
loading="lazy"
|
||||||
|
(error)="onLogoError($event, merchant.name)"
|
||||||
|
/>
|
||||||
|
} @else {
|
||||||
|
<img
|
||||||
|
[src]="getDefaultLogoUrl(merchant.name)"
|
||||||
|
[alt]="merchant.name + ' logo'"
|
||||||
|
class="rounded-circle"
|
||||||
|
style="width: 40px; height: 40px; object-fit: cover; border: 2px solid #f0f0f0;"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<strong class="d-block">{{ merchant.name }}</strong>
|
<!-- Informations du marchand -->
|
||||||
<small class="text-muted">{{ merchant.adresse }}</small>
|
<div class="min-w-0 flex-grow-1">
|
||||||
|
<strong class="d-block text-truncate">{{ merchant.name }}</strong>
|
||||||
|
<small class="text-muted text-truncate d-block">{{ merchant.adresse }}</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
@ -314,7 +334,7 @@
|
|||||||
</table>
|
</table>
|
||||||
|
|
||||||
<!-- Pagination -->
|
<!-- Pagination -->
|
||||||
@if (totalPages > 1) {
|
@if (totalPages >= 1) {
|
||||||
<div class="d-flex justify-content-between align-items-center mt-3">
|
<div class="d-flex justify-content-between align-items-center mt-3">
|
||||||
<div class="text-muted">
|
<div class="text-muted">
|
||||||
Affichage de {{ getStartIndex() }}
|
Affichage de {{ getStartIndex() }}
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { FormsModule } from '@angular/forms';
|
|||||||
import { NgIcon } from '@ng-icons/core';
|
import { NgIcon } from '@ng-icons/core';
|
||||||
import { NgbPaginationModule } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbPaginationModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { Observable, Subject, of } from 'rxjs';
|
import { Observable, Subject, of } from 'rxjs';
|
||||||
import { catchError, takeUntil } from 'rxjs/operators';
|
import { catchError, map, takeUntil, tap } from 'rxjs/operators';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Merchant,
|
Merchant,
|
||||||
@ -20,6 +20,8 @@ import { MerchantConfigService } from '../merchant-config.service';
|
|||||||
import { RoleManagementService } from '@core/services/hub-users-roles-management.service';
|
import { RoleManagementService } from '@core/services/hub-users-roles-management.service';
|
||||||
import { AuthService } from '@core/services/auth.service';
|
import { AuthService } from '@core/services/auth.service';
|
||||||
import { UiCard } from '@app/components/ui-card';
|
import { UiCard } from '@app/components/ui-card';
|
||||||
|
import { DomSanitizer } from '@angular/platform-browser';
|
||||||
|
import { MinioService } from '@core/services/minio.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-merchant-config-list',
|
selector: 'app-merchant-config-list',
|
||||||
@ -40,6 +42,13 @@ export class MerchantConfigsList implements OnInit, OnDestroy {
|
|||||||
private cdRef = inject(ChangeDetectorRef);
|
private cdRef = inject(ChangeDetectorRef);
|
||||||
private destroy$ = new Subject<void>();
|
private destroy$ = new Subject<void>();
|
||||||
|
|
||||||
|
private minioService = inject(MinioService);
|
||||||
|
|
||||||
|
// Cache des URLs de logos
|
||||||
|
private logoUrlCache = new Map<string, string>();
|
||||||
|
// Ajouter un cache pour les logos non trouvés
|
||||||
|
private logoErrorCache = new Set<string>();
|
||||||
|
|
||||||
// Configuration
|
// Configuration
|
||||||
readonly ConfigType = ConfigType;
|
readonly ConfigType = ConfigType;
|
||||||
readonly Operator = Operator;
|
readonly Operator = Operator;
|
||||||
@ -103,11 +112,6 @@ export class MerchantConfigsList implements OnInit, OnDestroy {
|
|||||||
this.loadCurrentUserPermissions();
|
this.loadCurrentUserPermissions();
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
|
||||||
this.destroy$.next();
|
|
||||||
this.destroy$.complete();
|
|
||||||
}
|
|
||||||
|
|
||||||
private loadCurrentUserPermissions() {
|
private loadCurrentUserPermissions() {
|
||||||
this.authService.getUserProfile()
|
this.authService.getUserProfile()
|
||||||
.pipe(takeUntil(this.destroy$))
|
.pipe(takeUntil(this.destroy$))
|
||||||
@ -150,7 +154,7 @@ export class MerchantConfigsList implements OnInit, OnDestroy {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
loadMerchants() {
|
loadMerchants(): void {
|
||||||
if (!this.isHubUser) {
|
if (!this.isHubUser) {
|
||||||
console.log('⚠️ User is not a Hub user, merchant list not displayed');
|
console.log('⚠️ User is not a Hub user, merchant list not displayed');
|
||||||
return;
|
return;
|
||||||
@ -159,15 +163,14 @@ export class MerchantConfigsList implements OnInit, OnDestroy {
|
|||||||
this.loading = true;
|
this.loading = true;
|
||||||
this.error = '';
|
this.error = '';
|
||||||
|
|
||||||
this.merchantConfigService.getMerchants(
|
const params = this.buildSearchParams();
|
||||||
this.currentPage,
|
const skip = (this.currentPage - 1) * this.itemsPerPage;
|
||||||
this.itemsPerPage,
|
|
||||||
this.buildSearchParams()
|
this.merchantConfigService.getAllMerchants(this.currentPage, this.itemsPerPage, params)
|
||||||
)
|
|
||||||
.pipe(
|
.pipe(
|
||||||
takeUntil(this.destroy$),
|
takeUntil(this.destroy$),
|
||||||
catchError(error => {
|
catchError(error => {
|
||||||
console.error('Error loading merchants:', error);
|
console.error('❌ Error loading merchants:', error);
|
||||||
this.error = 'Erreur lors du chargement des marchands';
|
this.error = 'Erreur lors du chargement des marchands';
|
||||||
return of({
|
return of({
|
||||||
items: [],
|
items: [],
|
||||||
@ -178,36 +181,196 @@ export class MerchantConfigsList implements OnInit, OnDestroy {
|
|||||||
} as PaginatedResponse<Merchant>);
|
} as PaginatedResponse<Merchant>);
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.subscribe({
|
.subscribe(response => {
|
||||||
next: (response) => {
|
this.allMerchants = response.items || [];
|
||||||
console.log('📊 Pagination response:', {
|
this.displayedMerchants = response.items || [];
|
||||||
page: response.page,
|
this.totalItems = response.total || 0;
|
||||||
total: response.total,
|
this.totalPages = response.totalPages || Math.ceil((response.total || 0) / this.itemsPerPage);
|
||||||
totalPages: response.totalPages,
|
|
||||||
itemsCount: response.items?.length,
|
|
||||||
limit: response.limit
|
|
||||||
});
|
|
||||||
|
|
||||||
this.allMerchants = response.items || [];
|
this.loading = false;
|
||||||
this.displayedMerchants = response.items || [];
|
this.cdRef.detectChanges();
|
||||||
this.totalItems = response.total || 0;
|
|
||||||
this.totalPages = response.totalPages || 0;
|
|
||||||
|
|
||||||
this.loading = false;
|
console.log('📊 Pagination response:', {
|
||||||
this.cdRef.detectChanges();
|
page: response.page,
|
||||||
},
|
total: response.total,
|
||||||
error: () => {
|
totalPages: this.totalPages,
|
||||||
this.error = 'Erreur lors du chargement des marchands';
|
itemsCount: response.items?.length,
|
||||||
this.loading = false;
|
limit: response.limit
|
||||||
this.allMerchants = [];
|
});
|
||||||
this.displayedMerchants = [];
|
|
||||||
this.totalItems = 0;
|
|
||||||
this.totalPages = 0;
|
|
||||||
this.cdRef.detectChanges();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== AFFICHAGE DU LOGO ====================
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère l'URL du logo avec fallback automatique
|
||||||
|
*/
|
||||||
|
getMerchantLogoUrl(
|
||||||
|
merchantId: number | undefined,
|
||||||
|
logoFileName: string,
|
||||||
|
merchantName: string
|
||||||
|
): Observable<string> {
|
||||||
|
|
||||||
|
const newMerchantId = String(merchantId);
|
||||||
|
|
||||||
|
const cacheKey = `${merchantId}_${logoFileName}`;
|
||||||
|
|
||||||
|
// Vérifier si le logo est en cache d'erreur
|
||||||
|
if (this.logoErrorCache.has(cacheKey)) {
|
||||||
|
const defaultLogo = this.getDefaultLogoUrl(merchantName);
|
||||||
|
return of(defaultLogo);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier le cache normal
|
||||||
|
if (this.logoUrlCache.has(cacheKey)) {
|
||||||
|
return of(this.logoUrlCache.get(cacheKey)!);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Récupérer l'URL depuis l'API avec la nouvelle structure
|
||||||
|
return this.minioService.getMerchantLogoUrl(
|
||||||
|
newMerchantId,
|
||||||
|
logoFileName,
|
||||||
|
{ signed: true, expirySeconds: 3600 }
|
||||||
|
).pipe(
|
||||||
|
map(response => {
|
||||||
|
// Extraire l'URL de la réponse
|
||||||
|
const url = response.data.url ;
|
||||||
|
|
||||||
|
// Mettre en cache avec la clé composite
|
||||||
|
this.logoUrlCache.set(cacheKey, url);
|
||||||
|
|
||||||
|
return url;
|
||||||
|
}),
|
||||||
|
catchError(error => {
|
||||||
|
console.warn(`⚠️ Logo not found for merchant ${merchantId}: ${logoFileName}`, error);
|
||||||
|
|
||||||
|
// En cas d'erreur, ajouter au cache d'erreur
|
||||||
|
this.logoErrorCache.add(cacheKey);
|
||||||
|
|
||||||
|
// Générer un logo par défaut
|
||||||
|
const defaultLogo = this.getDefaultLogoUrl(merchantName);
|
||||||
|
|
||||||
|
// Mettre le logo par défaut dans le cache normal aussi
|
||||||
|
this.logoUrlCache.set(cacheKey, defaultLogo);
|
||||||
|
|
||||||
|
return of(defaultLogo);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Génère une URL de logo par défaut basée sur les initiales
|
||||||
|
*/
|
||||||
|
getDefaultLogoUrl(merchantName: string): string {
|
||||||
|
// Créer des initiales significatives
|
||||||
|
const initials = this.extractInitials(merchantName);
|
||||||
|
|
||||||
|
// Palette de couleurs agréables
|
||||||
|
const colors = [
|
||||||
|
'667eea', // Violet
|
||||||
|
'764ba2', // Violet foncé
|
||||||
|
'f56565', // Rouge
|
||||||
|
'4299e1', // Bleu
|
||||||
|
'48bb78', // Vert
|
||||||
|
'ed8936', // Orange
|
||||||
|
'FF6B6B', // Rouge clair
|
||||||
|
'4ECDC4', // Turquoise
|
||||||
|
'45B7D1', // Bleu clair
|
||||||
|
'96CEB4' // Vert menthe
|
||||||
|
];
|
||||||
|
|
||||||
|
const colorIndex = merchantName.length % colors.length;
|
||||||
|
const backgroundColor = colors[colorIndex];
|
||||||
|
|
||||||
|
// Taille fixe à 80px (l'API génère un carré de cette taille)
|
||||||
|
// L'image sera redimensionnée à 40px via CSS
|
||||||
|
return `https://ui-avatars.com/api/?name=${encodeURIComponent(initials)}&background=${backgroundColor}&color=FFFFFF&size=80`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extrait les initiales de manière intelligente
|
||||||
|
*/
|
||||||
|
private extractInitials(name: string): string {
|
||||||
|
if (!name || name.trim() === '') {
|
||||||
|
return '??';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nettoyer le nom
|
||||||
|
const cleanedName = name.trim().toUpperCase();
|
||||||
|
|
||||||
|
// Extraire les mots
|
||||||
|
const words = cleanedName.split(/\s+/);
|
||||||
|
|
||||||
|
// Si un seul mot, prendre les deux premières lettres
|
||||||
|
if (words.length === 1) {
|
||||||
|
return words[0].substring(0, 2) || '??';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prendre la première lettre des deux premiers mots
|
||||||
|
const initials = words
|
||||||
|
.slice(0, 2) // Prendre les 2 premiers mots
|
||||||
|
.map(word => word[0] || '')
|
||||||
|
.join('');
|
||||||
|
|
||||||
|
return initials || name.substring(0, 2).toUpperCase() || '??';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gère les erreurs de chargement des logos MinIO
|
||||||
|
*/
|
||||||
|
onLogoError(event: Event, merchantName: string): void {
|
||||||
|
const img = event.target as HTMLImageElement;
|
||||||
|
|
||||||
|
if (!img) return;
|
||||||
|
|
||||||
|
console.warn('Logo MinIO failed to load, using default for:', merchantName);
|
||||||
|
|
||||||
|
img.onerror = null;
|
||||||
|
img.src = this.getDefaultLogoUrl(merchantName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gère les erreurs de chargement des logos par défaut
|
||||||
|
*/
|
||||||
|
onDefaultLogoError(event: Event | string): void {
|
||||||
|
if (!(event instanceof Event)) {
|
||||||
|
console.error('Default logo error (non-event):', event);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const img = event.target as HTMLImageElement | null;
|
||||||
|
if (!img) return;
|
||||||
|
|
||||||
|
console.error('Default logo also failed to load, using fallback SVG');
|
||||||
|
|
||||||
|
// SVG local
|
||||||
|
img.onerror = null; // éviter boucle infinie
|
||||||
|
img.src = 'assets/images/default-merchant-logo.svg';
|
||||||
|
|
||||||
|
// Dernier recours
|
||||||
|
img.onerror = (e) => {
|
||||||
|
if (!(e instanceof Event)) return;
|
||||||
|
const fallbackImg = e.target as HTMLImageElement | null;
|
||||||
|
if (!fallbackImg) return;
|
||||||
|
|
||||||
|
fallbackImg.onerror = null;
|
||||||
|
fallbackImg.src = this.generateFallbackDataUrl();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Génère un fallback SVG en data URL
|
||||||
|
*/
|
||||||
|
private generateFallbackDataUrl(): string {
|
||||||
|
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40">
|
||||||
|
<rect width="40" height="40" fill="#667eea" rx="20"/>
|
||||||
|
<text x="20" y="22" text-anchor="middle" fill="white" font-family="Arial" font-size="14" font-weight="bold">?</text>
|
||||||
|
</svg>`;
|
||||||
|
|
||||||
|
return 'data:image/svg+xml;base64,' + btoa(svg);
|
||||||
|
}
|
||||||
|
|
||||||
private buildSearchParams(): SearchMerchantsParams {
|
private buildSearchParams(): SearchMerchantsParams {
|
||||||
const params: SearchMerchantsParams = {};
|
const params: SearchMerchantsParams = {};
|
||||||
|
|
||||||
@ -370,4 +533,15 @@ export class MerchantConfigsList implements OnInit, OnDestroy {
|
|||||||
shouldDisplayMerchantList(): boolean {
|
shouldDisplayMerchantList(): boolean {
|
||||||
return this.isHubUser;
|
return this.isHubUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.destroy$.next();
|
||||||
|
this.destroy$.complete();
|
||||||
|
|
||||||
|
// Nettoyer les caches
|
||||||
|
this.logoUrlCache.clear();
|
||||||
|
this.logoErrorCache.clear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -7,11 +7,13 @@
|
|||||||
<h4 class="mb-1">Profil Marchand</h4>
|
<h4 class="mb-1">Profil Marchand</h4>
|
||||||
<nav aria-label="breadcrumb">
|
<nav aria-label="breadcrumb">
|
||||||
<ol class="breadcrumb mb-0">
|
<ol class="breadcrumb mb-0">
|
||||||
<li class="breadcrumb-item">
|
@if(isHubUser){
|
||||||
<a href="javascript:void(0)" (click)="goBack()" class="text-decoration-none cursor-pointer">
|
<li class="breadcrumb-item">
|
||||||
Marchands
|
<a href="javascript:void(0)" (click)="goBack()" class="text-decoration-none cursor-pointer">
|
||||||
</a>
|
Marchands
|
||||||
</li>
|
</a>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
<li class="breadcrumb-item active" aria-current="page">
|
<li class="breadcrumb-item active" aria-current="page">
|
||||||
{{ merchant?.name || 'Chargement...' }}
|
{{ merchant?.name || 'Chargement...' }}
|
||||||
</li>
|
</li>
|
||||||
@ -29,15 +31,16 @@
|
|||||||
<ng-icon name="lucideRefreshCw" class="me-1" [class.spin]="loading"></ng-icon>
|
<ng-icon name="lucideRefreshCw" class="me-1" [class.spin]="loading"></ng-icon>
|
||||||
Actualiser
|
Actualiser
|
||||||
</button>
|
</button>
|
||||||
|
@if(isHubUser){
|
||||||
<!-- Bouton retour -->
|
<!-- Bouton retour -->
|
||||||
<button
|
<button
|
||||||
class="btn btn-outline-secondary"
|
class="btn btn-outline-secondary"
|
||||||
(click)="goBack()"
|
(click)="goBack()"
|
||||||
>
|
>
|
||||||
<ng-icon name="lucideArrowLeft" class="me-1"></ng-icon>
|
<ng-icon name="lucideArrowLeft" class="me-1"></ng-icon>
|
||||||
Retour
|
Retour
|
||||||
</button>
|
</button>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -106,61 +109,128 @@
|
|||||||
<ng-template ngbNavContent>
|
<ng-template ngbNavContent>
|
||||||
<!-- Vue d'ensemble -->
|
<!-- Vue d'ensemble -->
|
||||||
<div class="p-3">
|
<div class="p-3">
|
||||||
<!-- En-tête du profil -->
|
<!-- En-tête du profil avec logo -->
|
||||||
<div class="profile-section">
|
<div class="profile-section">
|
||||||
<div class="profile-header">
|
<div class="profile-header">
|
||||||
<div class="row align-items-center">
|
<div class="row align-items-center">
|
||||||
<div class="col-md-8">
|
<!-- Logo et informations -->
|
||||||
<h2 class="mb-2">{{ merchant.name }}</h2>
|
<div class="col-md-8">
|
||||||
<p class="mb-0 opacity-75">{{ merchant.description || 'Aucune description' }}</p>
|
<div class="d-flex align-items-start">
|
||||||
</div>
|
|
||||||
<div class="col-md-4 text-md-end">
|
<!-- ==================== LOGO DU MARCHAND ==================== -->
|
||||||
@if (canEditMerchant()) {
|
<div class="merchant-logo-container me-4">
|
||||||
<button
|
<div class="logo-display">
|
||||||
class="btn btn-light"
|
@if (merchant.logo && merchant.logo.trim() !== '') {
|
||||||
(click)="editMerchant(merchant)"
|
<img
|
||||||
>
|
[src]="getMerchantLogoUrl(merchant.id, merchant.logo, merchant.name) | async"
|
||||||
<ng-icon name="lucideEdit" class="me-1"></ng-icon>
|
[alt]="merchant.name + ' logo'"
|
||||||
Modifier le profil
|
class="merchant-logo"
|
||||||
</button>
|
style="width: 300px; height: 250px; object-fit: cover; border-radius: 8px; border: 2px solid #f0f0f0;"
|
||||||
}
|
loading="lazy"
|
||||||
|
(error)="onLogoError($event, merchant.name)"
|
||||||
|
/>
|
||||||
|
} @else {
|
||||||
|
<img
|
||||||
|
[src]="getDefaultLogoUrl(merchant.name)"
|
||||||
|
[alt]="merchant.name + ' logo'"
|
||||||
|
class="merchant-logo"
|
||||||
|
style="width: 200px; height: 200px; object-fit: cover; border-radius: 8px; border: 2px solid #f0f0f0;"
|
||||||
|
loading="lazy"
|
||||||
|
(error)="onDefaultLogoError($event)"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="logo-info mt-2 text-center">
|
||||||
|
<div style="font-size: 0.85rem; color: #666;">
|
||||||
|
{{ merchant.logo ? 'Logo personnalisé' : 'Logo par défaut' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Informations du marchand -->
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<h2 class="mb-2 text-white">{{ merchant.name }}</h2>
|
||||||
|
@if (merchant.description) {
|
||||||
|
<p class="mb-0 text-white opacity-75">{{ merchant.description }}</p>
|
||||||
|
} @else {
|
||||||
|
<p class="mb-0 text-white opacity-50 fst-italic">Aucune description</p>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Informations de contact -->
|
||||||
|
<div class="mt-3 d-flex flex-wrap gap-3">
|
||||||
|
<div class="d-flex align-items-center text-white opacity-75">
|
||||||
|
<ng-icon name="lucideMapPin" size="16" class="me-2"></ng-icon>
|
||||||
|
<small>{{ merchant.adresse }}</small>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex align-items-center text-white opacity-75">
|
||||||
|
<ng-icon name="lucidePhone" size="16" class="me-2"></ng-icon>
|
||||||
|
<small>{{ merchant.phone }}</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Statistiques -->
|
<!-- Actions -->
|
||||||
<div class="p-3">
|
<div class="col-md-4 text-md-end">
|
||||||
<div class="row g-3">
|
@if (canEditMerchant()) {
|
||||||
@if (getMerchantStats(); as stats) {
|
<button
|
||||||
<div class="col-md-3">
|
class="btn btn-light"
|
||||||
<div class="stats-card">
|
(click)="editMerchant(merchant)"
|
||||||
<div class="stats-number">{{ stats.configs.total }}</div>
|
>
|
||||||
<div class="stats-label">Configurations</div>
|
<ng-icon name="lucideEdit" class="me-1"></ng-icon>
|
||||||
</div>
|
Modifier le profil
|
||||||
</div>
|
</button>
|
||||||
<div class="col-md-3">
|
|
||||||
<div class="stats-card">
|
|
||||||
<div class="stats-number">{{ stats.contacts.total }}</div>
|
|
||||||
<div class="stats-label">Contacts</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-3">
|
|
||||||
<div class="stats-card">
|
|
||||||
<div class="stats-number">{{ stats.users.total }}</div>
|
|
||||||
<div class="stats-label">Utilisateurs</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-3">
|
|
||||||
<div class="stats-card">
|
|
||||||
<div class="stats-number">{{ stats.configs.sensitive }}</div>
|
|
||||||
<div class="stats-label">Configs sensibles</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Statistiques -->
|
||||||
|
<div class="p-3">
|
||||||
|
<div class="row g-3">
|
||||||
|
@if (getMerchantStats(); as stats) {
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="stats-card">
|
||||||
|
<div class="stats-icon mb-2">
|
||||||
|
<ng-icon name="lucideSettings" class="text-primary"></ng-icon>
|
||||||
|
</div>
|
||||||
|
<div class="stats-number">{{ stats.configs.total }}</div>
|
||||||
|
<div class="stats-label">Configurations</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="stats-card">
|
||||||
|
<div class="stats-icon mb-2">
|
||||||
|
<ng-icon name="lucideUsers" class="text-info"></ng-icon>
|
||||||
|
</div>
|
||||||
|
<div class="stats-number">{{ stats.contacts.total }}</div>
|
||||||
|
<div class="stats-label">Contacts</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="stats-card">
|
||||||
|
<div class="stats-icon mb-2">
|
||||||
|
<ng-icon name="lucideUserCheck" class="text-success"></ng-icon>
|
||||||
|
</div>
|
||||||
|
<div class="stats-number">{{ stats.users.total }}</div>
|
||||||
|
<div class="stats-label">Utilisateurs</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="stats-card">
|
||||||
|
<div class="stats-icon mb-2">
|
||||||
|
<ng-icon name="lucideShield" class="text-warning"></ng-icon>
|
||||||
|
</div>
|
||||||
|
<div class="stats-number">{{ stats.configs.sensitive }}</div>
|
||||||
|
<div class="stats-label">Configs sensibles</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<!-- Informations principales -->
|
<!-- Informations principales -->
|
||||||
<div class="row g-4">
|
<div class="row g-4">
|
||||||
<!-- Configurations récentes -->
|
<!-- Configurations récentes -->
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { CommonModule } from '@angular/common';
|
|||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { NgIcon } from '@ng-icons/core';
|
import { NgIcon } from '@ng-icons/core';
|
||||||
import { NgbAlertModule, NgbPaginationModule, NgbNavModule, NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbAlertModule, NgbPaginationModule, NgbNavModule, NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { Subject, takeUntil } from 'rxjs';
|
import { catchError, map, Observable, of, Subject, takeUntil, tap } from 'rxjs';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Merchant,
|
Merchant,
|
||||||
@ -21,7 +21,9 @@ import { MerchantConfigService } from '../merchant-config.service';
|
|||||||
import { MerchantDataAdapter } from '../merchant-data-adapter.service';
|
import { MerchantDataAdapter } from '../merchant-data-adapter.service';
|
||||||
import { RoleManagementService } from '@core/services/hub-users-roles-management.service';
|
import { RoleManagementService } from '@core/services/hub-users-roles-management.service';
|
||||||
import { AuthService } from '@core/services/auth.service';
|
import { AuthService } from '@core/services/auth.service';
|
||||||
import { UserRole } from '@core/models/dcb-bo-hub-user.model';
|
import { UserRole, UserType } from '@core/models/dcb-bo-hub-user.model';
|
||||||
|
import { MinioService } from '@core/services/minio.service';
|
||||||
|
import { DomSanitizer } from '@angular/platform-browser';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-merchant-config-view',
|
selector: 'app-merchant-config-view',
|
||||||
@ -180,6 +182,19 @@ export class MerchantConfigView implements OnInit, OnDestroy {
|
|||||||
private modalService = inject(NgbModal);
|
private modalService = inject(NgbModal);
|
||||||
private destroy$ = new Subject<void>();
|
private destroy$ = new Subject<void>();
|
||||||
|
|
||||||
|
private minioService = inject(MinioService);
|
||||||
|
|
||||||
|
// Cache des URLs de logos
|
||||||
|
private logoUrlCache = new Map<string, string>();
|
||||||
|
// Ajouter un cache pour les logos non trouvés
|
||||||
|
private logoErrorCache = new Set<string>();
|
||||||
|
|
||||||
|
private deleteModalRef: any = null;
|
||||||
|
// Cache
|
||||||
|
private merchantCache: { data: Merchant, timestamp: number } | null = null;
|
||||||
|
private readonly CACHE_TTL = 2 * 60 * 1000; // 2 minutes
|
||||||
|
|
||||||
|
|
||||||
readonly ConfigType = ConfigType;
|
readonly ConfigType = ConfigType;
|
||||||
readonly Operator = Operator;
|
readonly Operator = Operator;
|
||||||
readonly MerchantUtils = MerchantUtils;
|
readonly MerchantUtils = MerchantUtils;
|
||||||
@ -203,12 +218,14 @@ export class MerchantConfigView implements OnInit, OnDestroy {
|
|||||||
// Gestion des permissions
|
// Gestion des permissions
|
||||||
currentUserRole: UserRole | null = null;
|
currentUserRole: UserRole | null = null;
|
||||||
currentMerchantPartnerId: string = '';
|
currentMerchantPartnerId: string = '';
|
||||||
|
// Déterminer le type d'utilisateur
|
||||||
|
isMerchantUser = false;
|
||||||
|
isHubUser = false;
|
||||||
|
|
||||||
// Édition des configurations
|
// Édition des configurations
|
||||||
editingConfigId: number | null = null;
|
editingConfigId: number | null = null;
|
||||||
editedConfig: UpdateMerchantConfigDto = {};
|
editedConfig: UpdateMerchantConfigDto = {};
|
||||||
configToDelete: MerchantConfig | null = null;
|
configToDelete: MerchantConfig | null = null;
|
||||||
private deleteModalRef: any = null;
|
|
||||||
|
|
||||||
// Affichage des valeurs sensibles
|
// Affichage des valeurs sensibles
|
||||||
showSensitiveValues: { [configId: number]: boolean } = {};
|
showSensitiveValues: { [configId: number]: boolean } = {};
|
||||||
@ -222,10 +239,6 @@ export class MerchantConfigView implements OnInit, OnDestroy {
|
|||||||
page = 1;
|
page = 1;
|
||||||
pageSize = 5;
|
pageSize = 5;
|
||||||
|
|
||||||
// Cache
|
|
||||||
private merchantCache: { data: Merchant, timestamp: number } | null = null;
|
|
||||||
private readonly CACHE_TTL = 2 * 60 * 1000; // 2 minutes
|
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
if (this.merchantId) {
|
if (this.merchantId) {
|
||||||
this.loadCurrentUserPermissions();
|
this.loadCurrentUserPermissions();
|
||||||
@ -281,6 +294,176 @@ export class MerchantConfigView implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== AFFICHAGE DU LOGO ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère l'URL du logo avec fallback automatique
|
||||||
|
*/
|
||||||
|
getMerchantLogoUrl(
|
||||||
|
merchantId: number | undefined,
|
||||||
|
logoFileName: string,
|
||||||
|
merchantName: string
|
||||||
|
): Observable<string> {
|
||||||
|
|
||||||
|
const newMerchantId = String(merchantId);
|
||||||
|
|
||||||
|
const cacheKey = `${merchantId}_${logoFileName}`;
|
||||||
|
|
||||||
|
// Vérifier si le logo est en cache d'erreur
|
||||||
|
if (this.logoErrorCache.has(cacheKey)) {
|
||||||
|
const defaultLogo = this.getDefaultLogoUrl(merchantName);
|
||||||
|
return of(defaultLogo);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier le cache normal
|
||||||
|
if (this.logoUrlCache.has(cacheKey)) {
|
||||||
|
return of(this.logoUrlCache.get(cacheKey)!);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Récupérer l'URL depuis l'API avec la nouvelle structure
|
||||||
|
return this.minioService.getMerchantLogoUrl(
|
||||||
|
newMerchantId,
|
||||||
|
logoFileName,
|
||||||
|
{ signed: true, expirySeconds: 3600 }
|
||||||
|
).pipe(
|
||||||
|
map(response => {
|
||||||
|
// Extraire l'URL de la réponse
|
||||||
|
const url = response.data.url ;
|
||||||
|
|
||||||
|
// Mettre en cache avec la clé composite
|
||||||
|
this.logoUrlCache.set(cacheKey, url);
|
||||||
|
|
||||||
|
return url;
|
||||||
|
}),
|
||||||
|
catchError(error => {
|
||||||
|
console.warn(`⚠️ Logo not found for merchant ${merchantId}: ${logoFileName}`, error);
|
||||||
|
|
||||||
|
// En cas d'erreur, ajouter au cache d'erreur
|
||||||
|
this.logoErrorCache.add(cacheKey);
|
||||||
|
|
||||||
|
// Générer un logo par défaut
|
||||||
|
const defaultLogo = this.getDefaultLogoUrl(merchantName);
|
||||||
|
|
||||||
|
// Mettre le logo par défaut dans le cache normal aussi
|
||||||
|
this.logoUrlCache.set(cacheKey, defaultLogo);
|
||||||
|
|
||||||
|
return of(defaultLogo);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Génère une URL de logo par défaut basée sur les initiales
|
||||||
|
*/
|
||||||
|
getDefaultLogoUrl(merchantName: string): string {
|
||||||
|
// Créer des initiales significatives
|
||||||
|
const initials = this.extractInitials(merchantName);
|
||||||
|
|
||||||
|
// Palette de couleurs agréables
|
||||||
|
const colors = [
|
||||||
|
'667eea', // Violet
|
||||||
|
'764ba2', // Violet foncé
|
||||||
|
'f56565', // Rouge
|
||||||
|
'4299e1', // Bleu
|
||||||
|
'48bb78', // Vert
|
||||||
|
'ed8936', // Orange
|
||||||
|
'FF6B6B', // Rouge clair
|
||||||
|
'4ECDC4', // Turquoise
|
||||||
|
'45B7D1', // Bleu clair
|
||||||
|
'96CEB4' // Vert menthe
|
||||||
|
];
|
||||||
|
|
||||||
|
const colorIndex = merchantName.length % colors.length;
|
||||||
|
const backgroundColor = colors[colorIndex];
|
||||||
|
|
||||||
|
// Taille fixe à 80px (l'API génère un carré de cette taille)
|
||||||
|
// L'image sera redimensionnée à 40px via CSS
|
||||||
|
return `https://ui-avatars.com/api/?name=${encodeURIComponent(initials)}&background=${backgroundColor}&color=FFFFFF&size=80`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extrait les initiales de manière intelligente
|
||||||
|
*/
|
||||||
|
private extractInitials(name: string): string {
|
||||||
|
if (!name || name.trim() === '') {
|
||||||
|
return '??';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nettoyer le nom
|
||||||
|
const cleanedName = name.trim().toUpperCase();
|
||||||
|
|
||||||
|
// Extraire les mots
|
||||||
|
const words = cleanedName.split(/\s+/);
|
||||||
|
|
||||||
|
// Si un seul mot, prendre les deux premières lettres
|
||||||
|
if (words.length === 1) {
|
||||||
|
return words[0].substring(0, 2) || '??';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prendre la première lettre des deux premiers mots
|
||||||
|
const initials = words
|
||||||
|
.slice(0, 2) // Prendre les 2 premiers mots
|
||||||
|
.map(word => word[0] || '')
|
||||||
|
.join('');
|
||||||
|
|
||||||
|
return initials || name.substring(0, 2).toUpperCase() || '??';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gère les erreurs de chargement des logos MinIO
|
||||||
|
*/
|
||||||
|
onLogoError(event: Event, merchantName: string): void {
|
||||||
|
const img = event.target as HTMLImageElement;
|
||||||
|
|
||||||
|
if (!img) return;
|
||||||
|
|
||||||
|
console.warn('Logo MinIO failed to load, using default for:', merchantName);
|
||||||
|
|
||||||
|
img.onerror = null;
|
||||||
|
img.src = this.getDefaultLogoUrl(merchantName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gère les erreurs de chargement des logos par défaut
|
||||||
|
*/
|
||||||
|
onDefaultLogoError(event: Event | string): void {
|
||||||
|
if (!(event instanceof Event)) {
|
||||||
|
console.error('Default logo error (non-event):', event);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const img = event.target as HTMLImageElement | null;
|
||||||
|
if (!img) return;
|
||||||
|
|
||||||
|
console.error('Default logo also failed to load, using fallback SVG');
|
||||||
|
|
||||||
|
// SVG local
|
||||||
|
img.onerror = null; // éviter boucle infinie
|
||||||
|
img.src = 'assets/images/default-merchant-logo.svg';
|
||||||
|
|
||||||
|
// Dernier recours
|
||||||
|
img.onerror = (e) => {
|
||||||
|
if (!(e instanceof Event)) return;
|
||||||
|
const fallbackImg = e.target as HTMLImageElement | null;
|
||||||
|
if (!fallbackImg) return;
|
||||||
|
|
||||||
|
fallbackImg.onerror = null;
|
||||||
|
fallbackImg.src = this.generateFallbackDataUrl();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Génère un fallback SVG en data URL
|
||||||
|
*/
|
||||||
|
private generateFallbackDataUrl(): string {
|
||||||
|
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40">
|
||||||
|
<rect width="40" height="40" fill="#667eea" rx="20"/>
|
||||||
|
<text x="20" y="22" text-anchor="middle" fill="white" font-family="Arial" font-size="14" font-weight="bold">?</text>
|
||||||
|
</svg>`;
|
||||||
|
|
||||||
|
return 'data:image/svg+xml;base64,' + btoa(svg);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Charge les permissions de l'utilisateur courant
|
* Charge les permissions de l'utilisateur courant
|
||||||
*/
|
*/
|
||||||
@ -290,6 +473,18 @@ export class MerchantConfigView implements OnInit, OnDestroy {
|
|||||||
.subscribe({
|
.subscribe({
|
||||||
next: (profile) => {
|
next: (profile) => {
|
||||||
this.currentUserRole = this.authService.getCurrentUserRole();
|
this.currentUserRole = this.authService.getCurrentUserRole();
|
||||||
|
// Déterminer si c'est un utilisateur marchand
|
||||||
|
if (this.currentUserRole === UserRole.DCB_PARTNER_ADMIN ||
|
||||||
|
this.currentUserRole === UserRole.DCB_PARTNER_MANAGER ||
|
||||||
|
this.currentUserRole === UserRole.DCB_PARTNER_SUPPORT ||
|
||||||
|
this.currentUserRole === UserRole.MERCHANT_CONFIG_MANAGER ||
|
||||||
|
this.currentUserRole === UserRole.MERCHANT_CONFIG_TECHNICAL ||
|
||||||
|
this.currentUserRole === UserRole.MERCHANT_CONFIG_VIEWER) {
|
||||||
|
this.isMerchantUser = true;
|
||||||
|
} else {
|
||||||
|
this.isHubUser = profile?.userType === UserType.HUB;
|
||||||
|
this.isMerchantUser = profile?.userType === UserType.MERCHANT_PARTNER;
|
||||||
|
}
|
||||||
this.cdRef.detectChanges();
|
this.cdRef.detectChanges();
|
||||||
},
|
},
|
||||||
error: (error) => {
|
error: (error) => {
|
||||||
|
|||||||
@ -177,15 +177,91 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="form-label">Logo URL</label>
|
<div class="form-group logo-upload-section">
|
||||||
<input
|
<label class="form-label">
|
||||||
type="text"
|
<ng-icon name="lucideImage" class="me-1"></ng-icon>
|
||||||
class="form-control"
|
Logo du marchand (optionnel)
|
||||||
placeholder="https://exemple.com/logo.png"
|
</label>
|
||||||
[(ngModel)]="newMerchant.logo"
|
|
||||||
name="logo"
|
<div class="logo-upload-container">
|
||||||
[disabled]="creatingMerchant"
|
<!-- Zone de prévisualisation -->
|
||||||
>
|
<div class="logo-preview-area">
|
||||||
|
@if (logoPreviewUrl) {
|
||||||
|
<!-- Image sélectionnée -->
|
||||||
|
<div class="logo-preview">
|
||||||
|
<img [src]="logoPreviewUrl" alt="Prévisualisation du logo">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn-remove-preview"
|
||||||
|
(click)="removeSelectedLogo()"
|
||||||
|
[disabled]="creatingMerchant || uploadingLogo"
|
||||||
|
title="Supprimer"
|
||||||
|
>
|
||||||
|
<ng-icon name="lucideX"></ng-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
} @else {
|
||||||
|
<!-- Placeholder -->
|
||||||
|
<div class="logo-placeholder">
|
||||||
|
<ng-icon name="lucideImage" size="48" class="text-muted mb-2"></ng-icon>
|
||||||
|
<p class="text-muted mb-0">Aucun logo sélectionné</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bouton de sélection -->
|
||||||
|
<div class="logo-upload-actions">
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
id="logoInput"
|
||||||
|
accept="image/jpeg,image/jpg,image/png,image/gif,image/webp,image/svg+xml"
|
||||||
|
(change)="onLogoSelected($event)"
|
||||||
|
[disabled]="creatingMerchant || uploadingLogo"
|
||||||
|
style="display: none;"
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
for="logoInput"
|
||||||
|
class="btn btn-outline-primary btn-select-logo"
|
||||||
|
[class.disabled]="creatingMerchant || uploadingLogo"
|
||||||
|
>
|
||||||
|
<ng-icon name="lucideUpload" class="me-1"></ng-icon>
|
||||||
|
{{ logoPreviewUrl ? 'Changer le logo' : 'Sélectionner un logo' }}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<small class="text-muted d-block mt-2">
|
||||||
|
<ng-icon name="lucideInfo" size="12" class="me-1"></ng-icon>
|
||||||
|
Formats acceptés : JPG, PNG, GIF, WebP, SVG | Taille max : 5MB
|
||||||
|
</small>
|
||||||
|
|
||||||
|
@if (selectedLogoFile) {
|
||||||
|
<div class="mt-2">
|
||||||
|
<small class="badge bg-info">
|
||||||
|
<ng-icon name="lucideFile" size="12" class="me-1"></ng-icon>
|
||||||
|
{{ selectedLogoFile.name }} ({{ formatFileSize(selectedLogoFile.size) }})
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Erreur upload logo -->
|
||||||
|
@if (logoUploadError) {
|
||||||
|
<div class="alert alert-danger mt-2">
|
||||||
|
<ng-icon name="lucideAlertCircle" class="me-1"></ng-icon>
|
||||||
|
{{ logoUploadError }}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Indicateur d'upload -->
|
||||||
|
@if (uploadingLogo) {
|
||||||
|
<div class="upload-progress">
|
||||||
|
<div class="spinner-border spinner-border-sm" role="status">
|
||||||
|
<span class="visually-hidden">Upload en cours...</span>
|
||||||
|
</div>
|
||||||
|
<span class="ms-2">Upload du logo en cours...</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
@ -517,15 +593,102 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="form-label">Logo URL</label>
|
<div class="form-group logo-edit-section">
|
||||||
<input
|
<label class="form-label">
|
||||||
type="text"
|
<ng-icon name="lucideImage" class="me-1"></ng-icon>
|
||||||
class="form-control"
|
Logo du marchand
|
||||||
[(ngModel)]="selectedMerchantForEdit.logo"
|
</label>
|
||||||
name="logo"
|
|
||||||
[disabled]="updatingMerchant"
|
<div class="logo-edit-container">
|
||||||
placeholder="https://exemple.com/logo.png"
|
<!-- Logo actuel ou nouveau -->
|
||||||
>
|
<div class="logo-display-area">
|
||||||
|
@if (editLogoPreviewUrl) {
|
||||||
|
<!-- Nouveau logo sélectionné -->
|
||||||
|
<div class="logo-preview">
|
||||||
|
<img [src]="editLogoPreviewUrl" alt="Nouveau logo">
|
||||||
|
<div class="logo-badge badge-new">Nouveau</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn-remove-preview"
|
||||||
|
(click)="cancelEditLogo()"
|
||||||
|
[disabled]="updatingMerchant || uploadingLogo"
|
||||||
|
title="Annuler"
|
||||||
|
>
|
||||||
|
<ng-icon name="lucideX"></ng-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
} @else if (currentLogoUrl) {
|
||||||
|
<!-- Logo actuel -->
|
||||||
|
<div class="logo-preview">
|
||||||
|
<img [src]="currentLogoUrl" alt="Logo actuel">
|
||||||
|
<div class="logo-badge badge-current">Actuel</div>
|
||||||
|
</div>
|
||||||
|
} @else {
|
||||||
|
<!-- Pas de logo -->
|
||||||
|
<div class="logo-placeholder">
|
||||||
|
<ng-icon name="lucideImage" size="48" class="text-muted mb-2"></ng-icon>
|
||||||
|
<p class="text-muted mb-0">Aucun logo</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Actions -->
|
||||||
|
<div class="logo-edit-actions">
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
id="editLogoInput"
|
||||||
|
accept="image/jpeg,image/jpg,image/png,image/gif,image/webp,image/svg+xml"
|
||||||
|
(change)="onEditLogoSelected($event)"
|
||||||
|
[disabled]="updatingMerchant || uploadingLogo"
|
||||||
|
style="display: none;"
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
for="editLogoInput"
|
||||||
|
class="btn btn-outline-primary btn-change-logo"
|
||||||
|
[class.disabled]="updatingMerchant || uploadingLogo"
|
||||||
|
>
|
||||||
|
<ng-icon name="lucideRefreshCw" class="me-1"></ng-icon>
|
||||||
|
{{ currentLogoUrl ? 'Changer le logo' : 'Ajouter un logo' }}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
@if (currentLogoUrl && !editLogoPreviewUrl) {
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-outline-danger btn-remove-logo"
|
||||||
|
(click)="selectedMerchantForEdit!.logo = ''; currentLogoUrl = null"
|
||||||
|
[disabled]="updatingMerchant"
|
||||||
|
>
|
||||||
|
<ng-icon name="lucideTrash2" class="me-1"></ng-icon>
|
||||||
|
Supprimer le logo
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
|
||||||
|
<small class="text-muted d-block mt-2">
|
||||||
|
<ng-icon name="lucideInfo" size="12" class="me-1"></ng-icon>
|
||||||
|
Formats : JPG, PNG, GIF, WebP, SVG | Max : 5MB
|
||||||
|
</small>
|
||||||
|
|
||||||
|
@if (editLogoFile) {
|
||||||
|
<div class="mt-2">
|
||||||
|
<small class="badge bg-info">
|
||||||
|
<ng-icon name="lucideFile" size="12" class="me-1"></ng-icon>
|
||||||
|
{{ editLogoFile.name }} ({{ formatFileSize(editLogoFile.size) }})
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Indicateur d'upload -->
|
||||||
|
@if (uploadingLogo) {
|
||||||
|
<div class="upload-progress">
|
||||||
|
<div class="spinner-border spinner-border-sm" role="status">
|
||||||
|
<span class="visually-hidden">Upload en cours...</span>
|
||||||
|
</div>
|
||||||
|
<span class="ms-2">Upload du nouveau logo en cours...</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
@ -567,104 +730,160 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- CONFIGURATIONS TECHNIQUES -->
|
<!-- LISTE DES CONFIGURATIONS (READONLY) -->
|
||||||
<div class="row g-3 mb-4">
|
<div class="row g-3">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="d-flex justify-content-between align-items-center border-bottom pb-2">
|
<div class="d-flex justify-content-between align-items-center border-bottom pb-2 mb-3">
|
||||||
<h6 class="mb-0 text-primary">
|
<h6 class="mb-0 text-primary">
|
||||||
<ng-icon name="lucideSettings" class="me-2"></ng-icon>
|
<ng-icon name="lucideSettings" class="me-2"></ng-icon>
|
||||||
Configurations Techniques
|
Configurations
|
||||||
</h6>
|
</h6>
|
||||||
<button
|
<span class="badge bg-secondary">{{ selectedMerchantForEdit.configs.length || 0 }} config(s)</span>
|
||||||
type="button"
|
|
||||||
class="btn btn-outline-primary btn-sm"
|
|
||||||
(click)="addConfigInEdit()"
|
|
||||||
[disabled]="updatingMerchant"
|
|
||||||
>
|
|
||||||
<ng-icon name="lucidePlus" class="me-1"></ng-icon>
|
|
||||||
Ajouter une configuration
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (!selectedMerchantForEdit.configs || selectedMerchantForEdit.configs.length === 0) {
|
@if (!selectedMerchantForEdit.configs || selectedMerchantForEdit.configs.length === 0) {
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="alert alert-warning">
|
<div class="alert alert-info">
|
||||||
<ng-icon name="lucideAlertCircle" class="me-2"></ng-icon>
|
<ng-icon name="lucideInfo" class="me-2"></ng-icon>
|
||||||
Au moins une configuration est requise
|
Aucune configuration disponible
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
<!-- Liste des configurations -->
|
<!-- Liste des configurations en mode lecture -->
|
||||||
@for (config of selectedMerchantForEdit.configs; track trackByConfigId($index, config); let i = $index) {
|
@for (config of selectedMerchantForEdit.configs; track config.id || $index; let i = $index) {
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="card border-0 shadow-sm">
|
<div class="card border-0 shadow-sm mb-3">
|
||||||
<div class="card-header bg-light py-2 d-flex justify-content-between align-items-center">
|
<div class="card-header bg-light py-2 d-flex justify-content-between align-items-center">
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<ng-icon [name]="getConfigTypeIconSafe(config.name)" class="me-2 text-primary"></ng-icon>
|
<ng-icon name="lucideSettings" class="me-2 text-primary"></ng-icon>
|
||||||
<span class="fw-semibold">Configuration {{ i + 1 }}</span>
|
<span class="fw-semibold">Configuration {{ i + 1 }}</span>
|
||||||
</div>
|
</div>
|
||||||
@if (selectedMerchantForEdit.configs.length > 1) {
|
@if (config.name.includes('SECRET') || config.name.includes('KEY') || config.value.includes('password')) {
|
||||||
<button
|
<span class="badge bg-warning text-dark">
|
||||||
type="button"
|
<ng-icon name="lucideShield" class="me-1"></ng-icon>
|
||||||
class="btn btn-sm btn-outline-danger"
|
Sensible
|
||||||
(click)="removeConfigInEdit(i)"
|
</span>
|
||||||
[disabled]="updatingMerchant"
|
|
||||||
>
|
|
||||||
<ng-icon name="lucideTrash2" class="me-1"></ng-icon>
|
|
||||||
Supprimer
|
|
||||||
</button>
|
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="row g-3">
|
<div class="row g-3">
|
||||||
|
<!-- Type de configuration -->
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="form-label">Type <span class="text-danger">*</span></label>
|
<div class="mb-2">
|
||||||
<select
|
<small class="text-muted d-block">Type</small>
|
||||||
class="form-select"
|
<div class="d-flex align-items-center">
|
||||||
[(ngModel)]="config.name"
|
<ng-icon name="lucideSettings" class="me-2 text-muted"></ng-icon>
|
||||||
[name]="'editConfigType_' + i"
|
<span class="fw-medium">
|
||||||
required
|
{{ config.name || 'Non spécifié' }}
|
||||||
[disabled]="updatingMerchant"
|
</span>
|
||||||
>
|
|
||||||
<option value="" disabled>Sélectionnez un type</option>
|
|
||||||
@for (type of configTypes; track type.name) {
|
|
||||||
<option [value]="type.name">{{ type.label }}</option>
|
|
||||||
}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<label class="form-label">Opérateur <span class="text-danger">*</span></label>
|
|
||||||
<select
|
|
||||||
class="form-select"
|
|
||||||
[(ngModel)]="config.operatorId"
|
|
||||||
[name]="'editOperatorId_' + i"
|
|
||||||
required
|
|
||||||
[disabled]="updatingMerchant"
|
|
||||||
>
|
|
||||||
<option value="" disabled>Sélectionnez un opérateur</option>
|
|
||||||
@for (operator of operators; track operator.id) {
|
|
||||||
<option [value]="operator.id">{{ operator.name }}</option>
|
|
||||||
}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="col-12">
|
|
||||||
<label class="form-label">Valeur <span class="text-danger">*</span></label>
|
|
||||||
<textarea
|
|
||||||
class="form-control font-monospace"
|
|
||||||
[(ngModel)]="config.value"
|
|
||||||
[name]="'editValue_' + i"
|
|
||||||
required
|
|
||||||
[disabled]="updatingMerchant"
|
|
||||||
rows="3"
|
|
||||||
placeholder="Valeur de configuration"
|
|
||||||
></textarea>
|
|
||||||
@if (isSensitiveConfig(config)) {
|
|
||||||
<div class="form-text text-warning">
|
|
||||||
<ng-icon name="lucideShield" class="me-1"></ng-icon>
|
|
||||||
Cette configuration contient des informations sensibles
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Opérateur -->
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="mb-2">
|
||||||
|
<small class="text-muted d-block">Opérateur ID</small>
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<ng-icon name="lucideBuilding" class="me-2 text-muted"></ng-icon>
|
||||||
|
<span class="fw-medium">
|
||||||
|
{{ config.operatorId || 'Non spécifié' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Valeur -->
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="mb-2">
|
||||||
|
<small class="text-muted d-block">Valeur</small>
|
||||||
|
<div class="p-3 bg-light rounded border">
|
||||||
|
<pre class="mb-0" style="white-space: pre-wrap; word-break: break-all;">
|
||||||
|
{{ config.value || 'Aucune valeur' }}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- CONTACTS TECHNIQUES (READONLY) -->
|
||||||
|
<div class="row g-3 mt-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="d-flex justify-content-between align-items-center border-bottom pb-2 mb-3">
|
||||||
|
<h6 class="mb-0 text-primary">
|
||||||
|
<ng-icon name="lucideUsers" class="me-2"></ng-icon>
|
||||||
|
Contacts Techniques
|
||||||
|
</h6>
|
||||||
|
<span class="badge bg-secondary">{{ selectedMerchantForEdit.technicalContacts.length || 0 }} contact(s)</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (!selectedMerchantForEdit.technicalContacts || selectedMerchantForEdit.technicalContacts.length === 0) {
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
<ng-icon name="lucideAlertCircle" class="me-2"></ng-icon>
|
||||||
|
Aucun contact technique défini
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<!-- Liste des contacts techniques en mode lecture -->
|
||||||
|
@for (contact of selectedMerchantForEdit.technicalContacts; track contact.id || $index; let i = $index) {
|
||||||
|
<div class="col-12 col-md-6 col-lg-4">
|
||||||
|
<div class="card border-0 shadow-sm h-100">
|
||||||
|
<div class="card-header bg-light py-2">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<ng-icon name="lucideUser" class="me-2 text-primary"></ng-icon>
|
||||||
|
<span class="fw-semibold">Contact {{ i + 1 }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<!-- Nom complet -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<small class="text-muted d-block">Nom complet</small>
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<ng-icon name="lucideUser" class="me-2 text-muted"></ng-icon>
|
||||||
|
<span class="fw-medium">
|
||||||
|
{{ contact.firstName || '' }} {{ contact.lastName || '' }}
|
||||||
|
@if (!contact.firstName && !contact.lastName) {
|
||||||
|
<span class="text-muted fst-italic">Non spécifié</span>
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Téléphone -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<small class="text-muted d-block">Téléphone</small>
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<ng-icon name="lucidePhone" class="me-2 text-muted"></ng-icon>
|
||||||
|
@if (contact.phone) {
|
||||||
|
<a href="tel:{{ contact.phone }}" class="text-decoration-none">
|
||||||
|
{{ contact.phone }}
|
||||||
|
</a>
|
||||||
|
} @else {
|
||||||
|
<span class="text-muted fst-italic">Non spécifié</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Email -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<small class="text-muted d-block">Email</small>
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<ng-icon name="lucideMail" class="me-2 text-muted"></ng-icon>
|
||||||
|
@if (contact.email) {
|
||||||
|
<a href="mailto:{{ contact.email }}" class="text-decoration-none">
|
||||||
|
{{ contact.email }}
|
||||||
|
</a>
|
||||||
|
} @else {
|
||||||
|
<span class="text-muted fst-italic">Non spécifié</span>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -673,114 +892,6 @@
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- CONTACTS TECHNIQUES -->
|
|
||||||
<div class="row g-3">
|
|
||||||
<div class="col-12">
|
|
||||||
<div class="d-flex justify-content-between align-items-center border-bottom pb-2">
|
|
||||||
<h6 class="mb-0 text-primary">
|
|
||||||
<ng-icon name="lucideUsers" class="me-2"></ng-icon>
|
|
||||||
Contacts Techniques
|
|
||||||
</h6>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn btn-outline-primary btn-sm"
|
|
||||||
(click)="addTechnicalContactInEdit()"
|
|
||||||
[disabled]="updatingMerchant"
|
|
||||||
>
|
|
||||||
<ng-icon name="lucideUserPlus" class="me-1"></ng-icon>
|
|
||||||
Ajouter un contact
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@if (!selectedMerchantForEdit.technicalContacts || selectedMerchantForEdit.technicalContacts.length === 0) {
|
|
||||||
<div class="col-12">
|
|
||||||
<div class="alert alert-warning">
|
|
||||||
<ng-icon name="lucideAlertCircle" class="me-2"></ng-icon>
|
|
||||||
Au moins un contact technique est requis
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
<!-- Liste des contacts techniques -->
|
|
||||||
@for (contact of selectedMerchantForEdit.technicalContacts; track trackByContactId($index, contact); let i = $index) {
|
|
||||||
<div class="col-12">
|
|
||||||
<div class="card border-0 shadow-sm">
|
|
||||||
<div class="card-header bg-light py-2 d-flex justify-content-between align-items-center">
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<ng-icon name="lucideUser" class="me-2 text-primary"></ng-icon>
|
|
||||||
<span class="fw-semibold">Contact {{ i + 1 }}</span>
|
|
||||||
</div>
|
|
||||||
@if (selectedMerchantForEdit.technicalContacts.length > 1) {
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn btn-sm btn-outline-danger"
|
|
||||||
(click)="removeTechnicalContactInEdit(i)"
|
|
||||||
[disabled]="updatingMerchant"
|
|
||||||
>
|
|
||||||
<ng-icon name="lucideTrash2" class="me-1"></ng-icon>
|
|
||||||
Supprimer
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="row g-3">
|
|
||||||
<div class="col-md-6">
|
|
||||||
<label class="form-label">Prénom <span class="text-danger">*</span></label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="form-control"
|
|
||||||
[(ngModel)]="contact.firstName"
|
|
||||||
[name]="'editFirstName_' + i"
|
|
||||||
required
|
|
||||||
[disabled]="updatingMerchant"
|
|
||||||
placeholder="Prénom"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<label class="form-label">Nom <span class="text-danger">*</span></label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="form-control"
|
|
||||||
[(ngModel)]="contact.lastName"
|
|
||||||
[name]="'editLastName_' + i"
|
|
||||||
required
|
|
||||||
[disabled]="updatingMerchant"
|
|
||||||
placeholder="Nom"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<label class="form-label">Téléphone <span class="text-danger">*</span></label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="form-control"
|
|
||||||
[(ngModel)]="contact.phone"
|
|
||||||
[name]="'editPhone_' + i"
|
|
||||||
required
|
|
||||||
[disabled]="updatingMerchant"
|
|
||||||
placeholder="+XX X XX XX XX XX"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<label class="form-label">Email <span class="text-danger">*</span></label>
|
|
||||||
<input
|
|
||||||
type="email"
|
|
||||||
class="form-control"
|
|
||||||
[(ngModel)]="contact.email"
|
|
||||||
[name]="'editEmail_' + i"
|
|
||||||
required
|
|
||||||
[disabled]="updatingMerchant"
|
|
||||||
placeholder="email@exemple.com"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal-footer mt-4 border-top pt-3">
|
<div class="modal-footer mt-4 border-top pt-3">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@ -28,7 +28,6 @@ import { MerchantDataAdapter } from './merchant-data-adapter.service';
|
|||||||
import { SearchUsersParams } from '@core/models/dcb-bo-hub-user.model';
|
import { SearchUsersParams } from '@core/models/dcb-bo-hub-user.model';
|
||||||
|
|
||||||
import { MerchantUsersService } from '../hub-users-management/merchant-users.service';
|
import { MerchantUsersService } from '../hub-users-management/merchant-users.service';
|
||||||
import { User, UserType } from '@core/models/dcb-bo-hub-user.model';
|
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class MerchantConfigService {
|
export class MerchantConfigService {
|
||||||
@ -62,205 +61,47 @@ export class MerchantConfigService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getMerchants(page: number = 1, limit: number = 10, params?: SearchMerchantsParams): Observable<PaginatedResponse<Merchant>> {
|
/**
|
||||||
// Vérifier si le cache est valide
|
* Récupère tous les merchants (optionnel: avec recherche)
|
||||||
const paramsChanged = !this.areParamsEqual(params, this.cacheParams);
|
*/
|
||||||
|
getAllMerchants(
|
||||||
|
page: number = 1,
|
||||||
|
limit: number = 10,
|
||||||
|
params?: SearchMerchantsParams
|
||||||
|
): Observable<PaginatedResponse<Merchant>> {
|
||||||
|
const skip = (page - 1) * limit;
|
||||||
|
|
||||||
if (paramsChanged || this.merchantsCache.length === 0) {
|
|
||||||
// Nouvelle requête nécessaire
|
|
||||||
return this.fetchAllMerchants(params).pipe(
|
|
||||||
map(allMerchants => {
|
|
||||||
// Mettre en cache
|
|
||||||
this.merchantsCache = allMerchants;
|
|
||||||
this.cacheParams = params;
|
|
||||||
|
|
||||||
// Appliquer pagination
|
|
||||||
return this.applyPagination(allMerchants, page, limit);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// Vérifier si le cache contient assez d'éléments pour la page demandée
|
|
||||||
const neededItems = page * limit;
|
|
||||||
|
|
||||||
if (neededItems <= this.merchantsCache.length) {
|
|
||||||
// Cache suffisant
|
|
||||||
return of(this.applyPagination(this.merchantsCache, page, limit));
|
|
||||||
} else {
|
|
||||||
// Besoin de plus d'éléments
|
|
||||||
const additionalNeeded = neededItems - this.merchantsCache.length;
|
|
||||||
const newTake = this.calculateOptimalTake(additionalNeeded, page, limit);
|
|
||||||
|
|
||||||
console.log(`🔄 Cache insufficient (${this.merchantsCache.length}), fetching ${newTake} more items`);
|
|
||||||
|
|
||||||
return this.fetchAdditionalMerchants(newTake, params).pipe(
|
|
||||||
map(additionalMerchants => {
|
|
||||||
// Fusionner avec le cache (éviter les doublons)
|
|
||||||
const merged = this.mergeMerchants(this.merchantsCache, additionalMerchants);
|
|
||||||
this.merchantsCache = merged;
|
|
||||||
|
|
||||||
return this.applyPagination(merged, page, limit);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculer le take optimal en fonction du total
|
|
||||||
private calculateOptimalTake(totalCount: number, page: number, limit: number): number {
|
|
||||||
// Calculer combien d'éléments nous avons besoin pour la pagination
|
|
||||||
const neededItems = page * limit;
|
|
||||||
|
|
||||||
// Ajouter un buffer pour éviter les appels fréquents
|
|
||||||
const buffer = Math.max(limit * 2, 100);
|
|
||||||
|
|
||||||
// Calculer le take nécessaire
|
|
||||||
let optimalTake = neededItems + buffer;
|
|
||||||
|
|
||||||
// Si le total est connu, adapter le take
|
|
||||||
if (totalCount > 0) {
|
|
||||||
// Prendre soit ce dont on a besoin, soit le total (le plus petit)
|
|
||||||
optimalTake = Math.min(optimalTake, totalCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Arrondir aux paliers optimaux
|
|
||||||
return this.roundToOptimalValue(optimalTake);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Arrondir à des valeurs optimales (100, 500, 1000, etc.)
|
|
||||||
private roundToOptimalValue(value: number): number {
|
|
||||||
if (value <= 100) return 100;
|
|
||||||
if (value <= 500) return 500;
|
|
||||||
if (value <= 1000) return 1000;
|
|
||||||
if (value <= 2000) return 2000;
|
|
||||||
if (value <= 5000) return 5000;
|
|
||||||
if (value <= 10000) return 10000;
|
|
||||||
|
|
||||||
// Pour les très grands nombres, arrondir au multiple de 10000 supérieur
|
|
||||||
return Math.ceil(value / 10000) * 10000;
|
|
||||||
}
|
|
||||||
|
|
||||||
private fetchAllMerchants(params?: SearchMerchantsParams): Observable<Merchant[]> {
|
|
||||||
// Commencer avec un take raisonnable
|
|
||||||
const initialTake = 500;
|
|
||||||
|
|
||||||
return this.fetchMerchantsWithParams(initialTake, params).pipe(
|
|
||||||
switchMap(initialBatch => {
|
|
||||||
// Si nous avons récupéré moins que demandé, c'est probablement tout
|
|
||||||
if (initialBatch.length < initialTake) {
|
|
||||||
console.log(`✅ Retrieved all ${initialBatch.length} merchants`);
|
|
||||||
return of(initialBatch);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sinon, peut-être qu'il y a plus, essayer un take plus grand
|
|
||||||
console.log(`⚠️ Initial batch size (${initialBatch.length}) equals take, might be more`);
|
|
||||||
|
|
||||||
const largerTake = 2000;
|
|
||||||
return this.fetchMerchantsWithParams(largerTake, params).pipe(
|
|
||||||
map(largerBatch => {
|
|
||||||
console.log(`✅ Retrieved ${largerBatch.length} merchants with larger take`);
|
|
||||||
return largerBatch;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private fetchMerchantsWithParams(take: number, params?: SearchMerchantsParams): Observable<Merchant[]> {
|
|
||||||
let httpParams = new HttpParams().set('take', take.toString());
|
|
||||||
|
|
||||||
if (params?.query) {
|
|
||||||
httpParams = httpParams.set('query', params.query.trim());
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`📥 Fetching ${take} merchants`);
|
|
||||||
|
|
||||||
return this.http.get<ApiMerchant[]>(this.baseApiUrl, {
|
|
||||||
params: httpParams
|
|
||||||
}).pipe(
|
|
||||||
timeout(this.REQUEST_TIMEOUT),
|
|
||||||
map(apiMerchants =>
|
|
||||||
apiMerchants.map(merchant =>
|
|
||||||
this.dataAdapter.convertApiMerchantToFrontend(merchant)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private fetchAdditionalMerchants(take: number, params?: SearchMerchantsParams): Observable<Merchant[]> {
|
|
||||||
// Prendre à partir de la fin du cache
|
|
||||||
const skip = this.merchantsCache.length;
|
|
||||||
let httpParams = new HttpParams()
|
let httpParams = new HttpParams()
|
||||||
.set('take', take.toString())
|
.set('skip', skip.toString())
|
||||||
.set('skip', skip.toString()); // Si votre API supporte skip
|
.set('take', limit.toString());
|
||||||
|
|
||||||
if (params?.query) {
|
if (params?.query) {
|
||||||
httpParams = httpParams.set('query', params.query.trim());
|
httpParams = httpParams.set('query', params.query.trim());
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`📥 Fetching additional ${take} merchants (skip: ${skip})`);
|
return this.http.get<{ items: ApiMerchant[], total: number }>(this.baseApiUrl, { params: httpParams })
|
||||||
|
.pipe(
|
||||||
|
timeout(this.REQUEST_TIMEOUT),
|
||||||
|
map(response => {
|
||||||
|
// Sécuriser le mapping, même si items est undefined
|
||||||
|
const itemsArray: ApiMerchant[] = Array.isArray(response?.items) ? response.items : [];
|
||||||
|
const merchants: Merchant[] = itemsArray.map(m => this.dataAdapter.convertApiMerchantToFrontend(m));
|
||||||
|
|
||||||
return this.http.get<ApiMerchant[]>(this.baseApiUrl, {
|
const total: number = typeof response?.total === 'number' ? response.total : merchants.length;
|
||||||
params: httpParams
|
|
||||||
}).pipe(
|
|
||||||
timeout(this.REQUEST_TIMEOUT),
|
|
||||||
map(apiMerchants =>
|
|
||||||
apiMerchants.map(merchant =>
|
|
||||||
this.dataAdapter.convertApiMerchantToFrontend(merchant)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private mergeMerchants(existing: Merchant[], newOnes: Merchant[]): Merchant[] {
|
return {
|
||||||
const existingIds = new Set(existing.map(m => m.id));
|
items: merchants,
|
||||||
const uniqueNewOnes = newOnes.filter(m => !existingIds.has(m.id));
|
total,
|
||||||
return [...existing, ...uniqueNewOnes];
|
page,
|
||||||
}
|
limit,
|
||||||
|
totalPages: Math.ceil(total / limit)
|
||||||
private applyPagination(merchants: Merchant[], page: number, limit: number): PaginatedResponse<Merchant> {
|
};
|
||||||
const total = merchants.length;
|
}),
|
||||||
const totalPages = Math.ceil(total / limit);
|
catchError(error => this.handleError('getAllMerchants', error))
|
||||||
|
);
|
||||||
const startIndex = (page - 1) * limit;
|
|
||||||
const endIndex = Math.min(startIndex + limit, total);
|
|
||||||
const paginatedItems = merchants.slice(startIndex, endIndex);
|
|
||||||
|
|
||||||
return {
|
|
||||||
items: paginatedItems,
|
|
||||||
total: total,
|
|
||||||
page: page,
|
|
||||||
limit: limit,
|
|
||||||
totalPages: totalPages
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private areParamsEqual(a?: SearchMerchantsParams, b?: SearchMerchantsParams): boolean {
|
|
||||||
if (!a && !b) return true;
|
|
||||||
if (!a || !b) return false;
|
|
||||||
return a.query === b.query;
|
|
||||||
}
|
|
||||||
|
|
||||||
getAllMerchants(params?: SearchMerchantsParams): Observable<Merchant[]> {
|
|
||||||
let httpParams = new HttpParams();
|
|
||||||
|
|
||||||
if (params?.query) {
|
|
||||||
httpParams = httpParams.set('query', params.query.trim());
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.http.get<ApiMerchant[]>(this.baseApiUrl, { params: httpParams }).pipe(
|
|
||||||
timeout(this.REQUEST_TIMEOUT),
|
|
||||||
map(apiMerchants =>
|
|
||||||
apiMerchants.map(merchant =>
|
|
||||||
this.dataAdapter.convertApiMerchantToFrontend(merchant)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
catchError(error => this.handleError('getAllMerchants', error))
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getMerchantById(userId: number): Observable<Merchant> {
|
getMerchantById(userId: number): Observable<Merchant> {
|
||||||
//const numericId = this.convertIdToNumber(id);
|
|
||||||
|
|
||||||
console.log(`📥 Loading merchant ${userId}`);
|
console.log(`📥 Loading merchant ${userId}`);
|
||||||
|
|
||||||
return this.http.get<ApiMerchant>(`${this.baseApiUrl}/${userId}`).pipe(
|
return this.http.get<ApiMerchant>(`${this.baseApiUrl}/${userId}`).pipe(
|
||||||
@ -275,8 +116,6 @@ export class MerchantConfigService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateMerchant(id: number, updateMerchantDto: UpdateMerchantDto): Observable<Merchant> {
|
updateMerchant(id: number, updateMerchantDto: UpdateMerchantDto): Observable<Merchant> {
|
||||||
//const numericId = this.convertIdToNumber(id);
|
|
||||||
|
|
||||||
const apiDto = this.dataAdapter.convertUpdateMerchantToApi(updateMerchantDto);
|
const apiDto = this.dataAdapter.convertUpdateMerchantToApi(updateMerchantDto);
|
||||||
|
|
||||||
console.log(`📤 Updating merchant ${id}:`, apiDto);
|
console.log(`📤 Updating merchant ${id}:`, apiDto);
|
||||||
|
|||||||
@ -3,16 +3,18 @@ import { CommonModule } from '@angular/common';
|
|||||||
import { FormsModule, ReactiveFormsModule, FormBuilder, Validators, FormArray, FormGroup } from '@angular/forms';
|
import { FormsModule, ReactiveFormsModule, FormBuilder, Validators, FormArray, FormGroup } from '@angular/forms';
|
||||||
import { NgIcon } from '@ng-icons/core';
|
import { NgIcon } from '@ng-icons/core';
|
||||||
import { NgbNavModule, NgbModal, NgbModalModule } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbNavModule, NgbModal, NgbModalModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { catchError, finalize, map, of, Subject, takeUntil } from 'rxjs';
|
import { catchError, finalize, map, Observable, of, Subject, switchMap, takeUntil, tap } from 'rxjs';
|
||||||
|
import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
|
||||||
|
|
||||||
import { MerchantConfigService } from './merchant-config.service';
|
import { MerchantConfigService } from './merchant-config.service';
|
||||||
import { RoleManagementService } from '@core/services/hub-users-roles-management.service';
|
import { RoleManagementService } from '@core/services/hub-users-roles-management.service';
|
||||||
import { AuthService } from '@core/services/auth.service';
|
import { AuthService } from '@core/services/auth.service';
|
||||||
import { MerchantSyncService } from '../hub-users-management/merchant-sync-orchestrator.service';
|
|
||||||
import { PageTitle } from '@app/components/page-title/page-title';
|
import { PageTitle } from '@app/components/page-title/page-title';
|
||||||
import { MerchantConfigsList } from './merchant-config-list/merchant-config-list';
|
import { MerchantConfigsList } from './merchant-config-list/merchant-config-list';
|
||||||
import { MerchantConfigView } from './merchant-config-view/merchant-config-view';
|
import { MerchantConfigView } from './merchant-config-view/merchant-config-view';
|
||||||
|
|
||||||
|
import { MinioService } from '@core/services/minio.service';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CreateMerchantDto,
|
CreateMerchantDto,
|
||||||
MerchantUtils,
|
MerchantUtils,
|
||||||
@ -23,7 +25,7 @@ import {
|
|||||||
MerchantConfig,
|
MerchantConfig,
|
||||||
TechnicalContact
|
TechnicalContact
|
||||||
} from '@core/models/merchant-config.model';
|
} from '@core/models/merchant-config.model';
|
||||||
import { UserRole, UserType, PaginatedUserResponse, User } from '@core/models/dcb-bo-hub-user.model';
|
import { UserRole, UserType, User } from '@core/models/dcb-bo-hub-user.model';
|
||||||
|
|
||||||
import { MerchantDataAdapter } from './merchant-data-adapter.service';
|
import { MerchantDataAdapter } from './merchant-data-adapter.service';
|
||||||
|
|
||||||
@ -47,12 +49,20 @@ export class MerchantConfigManagement implements OnInit, OnDestroy {
|
|||||||
private modalService = inject(NgbModal);
|
private modalService = inject(NgbModal);
|
||||||
private authService = inject(AuthService);
|
private authService = inject(AuthService);
|
||||||
private merchantConfigService = inject(MerchantConfigService);
|
private merchantConfigService = inject(MerchantConfigService);
|
||||||
private merchantSyncService = inject(MerchantSyncService);
|
|
||||||
private dataAdapter = inject(MerchantDataAdapter);
|
private dataAdapter = inject(MerchantDataAdapter);
|
||||||
protected roleService = inject(RoleManagementService);
|
protected roleService = inject(RoleManagementService);
|
||||||
private cdRef = inject(ChangeDetectorRef);
|
private cdRef = inject(ChangeDetectorRef);
|
||||||
private destroy$ = new Subject<void>();
|
private destroy$ = new Subject<void>();
|
||||||
|
|
||||||
|
private minioService = inject(MinioService);
|
||||||
|
private sanitizer = inject(DomSanitizer);
|
||||||
|
|
||||||
|
// Cache des URLs de logos
|
||||||
|
private logoUrlCache = new Map<string, string>();
|
||||||
|
// Ajouter un cache pour les logos non trouvés
|
||||||
|
private logoErrorCache = new Set<string>();
|
||||||
|
|
||||||
|
|
||||||
// Configuration
|
// Configuration
|
||||||
readonly UserRole = UserRole;
|
readonly UserRole = UserRole;
|
||||||
readonly ConfigType = ConfigType;
|
readonly ConfigType = ConfigType;
|
||||||
@ -63,8 +73,22 @@ export class MerchantConfigManagement implements OnInit, OnDestroy {
|
|||||||
pageSubtitle: string = 'Administrez les marchands et leurs configurations techniques';
|
pageSubtitle: string = 'Administrez les marchands et leurs configurations techniques';
|
||||||
badge: any = { icon: 'lucideSettings', text: 'Merchant Management' };
|
badge: any = { icon: 'lucideSettings', text: 'Merchant Management' };
|
||||||
|
|
||||||
|
// ==================== GESTION DES LOGOS ====================
|
||||||
|
|
||||||
|
// Logo pour création
|
||||||
|
selectedLogoFile: File | null = null;
|
||||||
|
logoPreviewUrl: string | null = null;
|
||||||
|
uploadingLogo = false;
|
||||||
|
logoUploadError = '';
|
||||||
|
|
||||||
|
// Logo pour édition
|
||||||
|
editLogoFile: File | null = null;
|
||||||
|
editLogoPreviewUrl: string | null = null;
|
||||||
|
currentLogoUrl: string | null = null;
|
||||||
|
logoChanged = false;
|
||||||
|
|
||||||
// État de l'interface
|
// État de l'interface
|
||||||
activeTab: 'list' | 'merchant-profile' = 'list';
|
activeTab: 'list' | 'merchant-profile' = 'merchant-profile';
|
||||||
selectedMerchantId: number | null = null;
|
selectedMerchantId: number | null = null;
|
||||||
selectedConfigId: number | null = null;
|
selectedConfigId: number | null = null;
|
||||||
selectedUserId: string | null = null;
|
selectedUserId: string | null = null;
|
||||||
@ -232,8 +256,6 @@ export class MerchantConfigManagement implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.warn('No merchant found for current user');
|
console.warn('No merchant found for current user');
|
||||||
// Si aucun marchand trouvé, revenir à la liste
|
|
||||||
this.activeTab = 'list';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.loadingUserMerchant = false;
|
this.loadingUserMerchant = false;
|
||||||
@ -286,27 +308,6 @@ export class MerchantConfigManagement implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gestion des contacts dans l'édition
|
|
||||||
addTechnicalContactInEdit(): void {
|
|
||||||
if (!this.selectedMerchantForEdit?.technicalContacts) {
|
|
||||||
this.selectedMerchantForEdit!.technicalContacts = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
this.selectedMerchantForEdit?.technicalContacts.push({
|
|
||||||
firstName: '',
|
|
||||||
lastName: '',
|
|
||||||
phone: '',
|
|
||||||
email: ''
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
removeTechnicalContactInEdit(index: number): void {
|
|
||||||
if (this.selectedMerchantForEdit?.technicalContacts &&
|
|
||||||
this.selectedMerchantForEdit.technicalContacts.length > 1) {
|
|
||||||
this.selectedMerchantForEdit.technicalContacts.splice(index, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Méthodes pour la gestion des configurations
|
* Méthodes pour la gestion des configurations
|
||||||
*/
|
*/
|
||||||
@ -328,25 +329,6 @@ export class MerchantConfigManagement implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//Gestion des configs dans l'édition
|
|
||||||
addConfigInEdit(): void {
|
|
||||||
if (!this.selectedMerchantForEdit?.configs) {
|
|
||||||
this.selectedMerchantForEdit!.configs = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
this.selectedMerchantForEdit?.configs.push({
|
|
||||||
name: ConfigType.API_KEY,
|
|
||||||
value: '',
|
|
||||||
operatorId: Operator.ORANGE_OSN
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
removeConfigInEdit(index: number): void {
|
|
||||||
if (this.selectedMerchantForEdit?.configs && this.selectedMerchantForEdit.configs.length > 1) {
|
|
||||||
this.selectedMerchantForEdit.configs.splice(index, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== CONVERSION IDS ====================
|
// ==================== CONVERSION IDS ====================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -364,8 +346,8 @@ export class MerchantConfigManagement implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Conversion pour la mise à jour
|
// Conversion pour la mise à jour
|
||||||
private convertUpdateMerchantToBackend(dto: UpdateMerchantDto, existingMerchant?: Merchant): any {
|
private convertUpdateMerchantToBackend(dto: UpdateMerchantDto): any {
|
||||||
return this.dataAdapter.convertUpdateMerchantToApi(dto, existingMerchant);
|
return this.dataAdapter.convertUpdateMerchantToApi(dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== GESTION DES PERMISSIONS ====================
|
// ==================== GESTION DES PERMISSIONS ====================
|
||||||
@ -530,28 +512,24 @@ export class MerchantConfigManagement implements OnInit, OnDestroy {
|
|||||||
return this.canManageMerchants;
|
return this.canManageMerchants;
|
||||||
}
|
}
|
||||||
|
|
||||||
private loadMerchantProfile(merchantId: number): void {
|
|
||||||
if (this.loadingMerchants[merchantId]) return;
|
|
||||||
|
|
||||||
this.loadingMerchants[merchantId] = true;
|
|
||||||
|
|
||||||
this.merchantConfigService.getMerchantById(merchantId).subscribe({
|
|
||||||
next: (merchant) => {
|
|
||||||
// Conversion pour Angular
|
|
||||||
const frontendMerchant = this.convertMerchantToFrontend(merchant);
|
|
||||||
this.merchantProfiles[merchantId] = frontendMerchant;
|
|
||||||
this.loadingMerchants[merchantId] = false;
|
|
||||||
},
|
|
||||||
error: (error) => {
|
|
||||||
console.error(`Error loading merchant profile ${merchantId}:`, error);
|
|
||||||
this.loadingMerchants[merchantId] = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
backToList(): void {
|
backToList(): void {
|
||||||
console.log('🔙 Returning to list view');
|
console.log('🔙 Returning to list view');
|
||||||
this.showTab('list');
|
|
||||||
|
// Réinitialiser les IDs sélectionnés
|
||||||
|
this.selectedMerchantId = null;
|
||||||
|
this.selectedConfigId = null;
|
||||||
|
this.selectedUserId = null;
|
||||||
|
|
||||||
|
// Vider le cache du profil actuel
|
||||||
|
if (this.selectedMerchantId) {
|
||||||
|
delete this.merchantProfiles[this.selectedMerchantId];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Changer d'onglet
|
||||||
|
this.activeTab = 'list';
|
||||||
|
|
||||||
|
// Forcer la détection des changements
|
||||||
|
this.cdRef.detectChanges();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -594,6 +572,7 @@ export class MerchantConfigManagement implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.resetMerchantForm();
|
this.resetMerchantForm();
|
||||||
|
this.removeSelectedLogo();
|
||||||
this.createMerchantError = '';
|
this.createMerchantError = '';
|
||||||
this.openModal(this.createMerchantModal);
|
this.openModal(this.createMerchantModal);
|
||||||
}
|
}
|
||||||
@ -655,9 +634,101 @@ export class MerchantConfigManagement implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formate la taille du fichier
|
||||||
|
*/
|
||||||
|
formatFileSize(bytes: number): string {
|
||||||
|
if (bytes === 0) return '0 B';
|
||||||
|
const k = 1024;
|
||||||
|
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si un logo existe et est valide
|
||||||
|
*/
|
||||||
|
hasValidLogo(merchant: Merchant): boolean {
|
||||||
|
return !!(merchant.logo && merchant.logo.trim().length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Nettoie les ressources au changement de marchand
|
||||||
|
*/
|
||||||
|
private cleanupLogoResources(): void {
|
||||||
|
// Nettoyer les URLs de prévisualisation
|
||||||
|
if (this.logoPreviewUrl && this.logoPreviewUrl.startsWith('blob:')) {
|
||||||
|
URL.revokeObjectURL(this.logoPreviewUrl);
|
||||||
|
}
|
||||||
|
if (this.editLogoPreviewUrl && this.editLogoPreviewUrl.startsWith('blob:')) {
|
||||||
|
URL.revokeObjectURL(this.editLogoPreviewUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logoPreviewUrl = null;
|
||||||
|
this.editLogoPreviewUrl = null;
|
||||||
|
this.selectedLogoFile = null;
|
||||||
|
this.editLogoFile = null;
|
||||||
|
}
|
||||||
// ==================== OPÉRATIONS CRUD ====================
|
// ==================== OPÉRATIONS CRUD ====================
|
||||||
|
|
||||||
|
// ==================== GESTION DU LOGO LORS DE LA CRÉATION ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gère la sélection du logo lors de la création
|
||||||
|
*/
|
||||||
|
onLogoSelected(event: Event): void {
|
||||||
|
const input = event.target as HTMLInputElement;
|
||||||
|
if (!input.files || input.files.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const file = input.files[0];
|
||||||
|
|
||||||
|
// Validation du fichier
|
||||||
|
const validation = this.minioService.validateImageFile(file);
|
||||||
|
if (!validation.valid) {
|
||||||
|
this.logoUploadError = validation.error || 'Fichier invalide';
|
||||||
|
this.selectedLogoFile = null;
|
||||||
|
this.logoPreviewUrl = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.selectedLogoFile = file;
|
||||||
|
this.logoUploadError = '';
|
||||||
|
|
||||||
|
// Générer la prévisualisation
|
||||||
|
this.minioService.previewImage(file).then(
|
||||||
|
(dataUrl) => {
|
||||||
|
this.logoPreviewUrl = dataUrl;
|
||||||
|
this.cdRef.detectChanges();
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
console.error('Erreur prévisualisation:', error);
|
||||||
|
this.logoUploadError = 'Impossible de prévisualiser l\'image';
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supprime le logo sélectionné pour la création
|
||||||
|
*/
|
||||||
|
removeSelectedLogo(): void {
|
||||||
|
this.selectedLogoFile = null;
|
||||||
|
this.logoPreviewUrl = null;
|
||||||
|
this.logoUploadError = '';
|
||||||
|
|
||||||
|
// Reset l'input file
|
||||||
|
const fileInput = document.getElementById('logoInput') as HTMLInputElement;
|
||||||
|
if (fileInput) {
|
||||||
|
fileInput.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Appel API pour créer le marchand
|
||||||
|
*/
|
||||||
createMerchant(): void {
|
createMerchant(): void {
|
||||||
|
|
||||||
if (!this.canCreateMerchants) {
|
if (!this.canCreateMerchants) {
|
||||||
this.createMerchantError = 'Vous n\'avez pas la permission de créer des marchands';
|
this.createMerchantError = 'Vous n\'avez pas la permission de créer des marchands';
|
||||||
return;
|
return;
|
||||||
@ -672,7 +743,6 @@ export class MerchantConfigManagement implements OnInit, OnDestroy {
|
|||||||
this.creatingMerchant = true;
|
this.creatingMerchant = true;
|
||||||
this.createMerchantError = '';
|
this.createMerchantError = '';
|
||||||
|
|
||||||
// Conversion pour l'API
|
|
||||||
const createDto = this.convertMerchantToBackend(this.newMerchant);
|
const createDto = this.convertMerchantToBackend(this.newMerchant);
|
||||||
|
|
||||||
console.log('📤 Creating merchant:', createDto);
|
console.log('📤 Creating merchant:', createDto);
|
||||||
@ -681,13 +751,38 @@ export class MerchantConfigManagement implements OnInit, OnDestroy {
|
|||||||
.pipe(takeUntil(this.destroy$))
|
.pipe(takeUntil(this.destroy$))
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: (createdMerchant) => {
|
next: (createdMerchant) => {
|
||||||
// Conversion de la réponse pour Angular
|
|
||||||
const frontendMerchant = this.convertMerchantToFrontend(createdMerchant);
|
const frontendMerchant = this.convertMerchantToFrontend(createdMerchant);
|
||||||
|
|
||||||
|
// Si un logo est sélectionné, l'uploader d'abord
|
||||||
|
if (this.selectedLogoFile) {
|
||||||
|
this.uploadingLogo = true;
|
||||||
|
|
||||||
|
this.minioService.uploadMerchantLogo(Number(frontendMerchant.id), frontendMerchant.name, this.selectedLogoFile)
|
||||||
|
.pipe(takeUntil(this.destroy$))
|
||||||
|
.subscribe({
|
||||||
|
next: (uploadResponse) => {
|
||||||
|
console.log('✅ Logo uploaded:', uploadResponse);
|
||||||
|
this.uploadingLogo = false;
|
||||||
|
|
||||||
|
this.newMerchant.logo = uploadResponse.data.fileName;
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('❌ Error uploading logo:', error);
|
||||||
|
this.uploadingLogo = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
console.log('✅ Merchant created successfully:', frontendMerchant);
|
console.log('✅ Merchant created successfully:', frontendMerchant);
|
||||||
this.creatingMerchant = false;
|
this.creatingMerchant = false;
|
||||||
this.modalService.dismissAll();
|
this.modalService.dismissAll();
|
||||||
this.refreshMerchantsList();
|
this.refreshMerchantsList();
|
||||||
|
|
||||||
|
// Reset le formulaire et le logo
|
||||||
|
this.resetMerchantForm();
|
||||||
|
this.removeSelectedLogo();
|
||||||
|
|
||||||
this.cdRef.detectChanges();
|
this.cdRef.detectChanges();
|
||||||
},
|
},
|
||||||
error: (error) => {
|
error: (error) => {
|
||||||
@ -699,14 +794,69 @@ export class MerchantConfigManagement implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mise à jour COMPLÈTE du merchant
|
// ==================== GESTION DU LOGO LORS DE L'ÉDITION ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gère la sélection du nouveau logo lors de l'édition
|
||||||
|
*/
|
||||||
|
onEditLogoSelected(event: Event): void {
|
||||||
|
const input = event.target as HTMLInputElement;
|
||||||
|
if (!input.files || input.files.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const file = input.files[0];
|
||||||
|
|
||||||
|
// Validation du fichier
|
||||||
|
const validation = this.minioService.validateImageFile(file);
|
||||||
|
if (!validation.valid) {
|
||||||
|
this.updateMerchantError = validation.error || 'Fichier invalide';
|
||||||
|
this.editLogoFile = null;
|
||||||
|
this.editLogoPreviewUrl = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.editLogoFile = file;
|
||||||
|
this.logoChanged = true;
|
||||||
|
this.updateMerchantError = '';
|
||||||
|
|
||||||
|
// Générer la prévisualisation
|
||||||
|
this.minioService.previewImage(file).then(
|
||||||
|
(dataUrl) => {
|
||||||
|
this.editLogoPreviewUrl = dataUrl;
|
||||||
|
this.cdRef.detectChanges();
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
console.error('Erreur prévisualisation:', error);
|
||||||
|
this.updateMerchantError = 'Impossible de prévisualiser l\'image';
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Annule le changement de logo lors de l'édition
|
||||||
|
*/
|
||||||
|
cancelEditLogo(): void {
|
||||||
|
this.editLogoFile = null;
|
||||||
|
this.editLogoPreviewUrl = null;
|
||||||
|
this.logoChanged = false;
|
||||||
|
|
||||||
|
// Reset l'input file
|
||||||
|
const fileInput = document.getElementById('editLogoInput') as HTMLInputElement;
|
||||||
|
if (fileInput) {
|
||||||
|
fileInput.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Appel API pour mettre à jour le marchand
|
||||||
|
*/
|
||||||
updateMerchant(): void {
|
updateMerchant(): void {
|
||||||
if (!this.selectedMerchantForEdit) {
|
if (!this.selectedMerchantForEdit) {
|
||||||
this.updateMerchantError = 'Aucun marchand sélectionné pour modification';
|
this.updateMerchantError = 'Aucun marchand sélectionné pour modification';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validation des données complètes
|
|
||||||
const validation = this.validateMerchantUpdate(this.selectedMerchantForEdit);
|
const validation = this.validateMerchantUpdate(this.selectedMerchantForEdit);
|
||||||
if (!validation.isValid) {
|
if (!validation.isValid) {
|
||||||
this.updateMerchantError = validation.errors.join(', ');
|
this.updateMerchantError = validation.errors.join(', ');
|
||||||
@ -716,45 +866,289 @@ export class MerchantConfigManagement implements OnInit, OnDestroy {
|
|||||||
this.updatingMerchant = true;
|
this.updatingMerchant = true;
|
||||||
this.updateMerchantError = '';
|
this.updateMerchantError = '';
|
||||||
|
|
||||||
// Conversion pour l'API avec TOUTES les données
|
const merchantId = this.selectedMerchantForEdit!.id!;
|
||||||
const merchantId = this.selectedMerchantForEdit.id!;
|
const merchantName = this.selectedMerchantForEdit.name!;
|
||||||
const updateDto = this.convertUpdateMerchantToBackend(this.selectedMerchantForEdit, this.selectedMerchantForEdit);
|
|
||||||
|
|
||||||
console.log('📤 Updating merchant with full data:', updateDto);
|
let uploadObservable$;
|
||||||
|
|
||||||
this.merchantConfigService.updateMerchant(merchantId, updateDto)
|
// Si un nouveau logo est sélectionné
|
||||||
.pipe(takeUntil(this.destroy$))
|
if (this.editLogoFile && this.logoChanged) {
|
||||||
.subscribe({
|
this.uploadingLogo = true;
|
||||||
next: (updatedMerchant) => {
|
|
||||||
// Conversion pour Angular
|
|
||||||
const frontendMerchant = this.convertMerchantToFrontend(updatedMerchant);
|
|
||||||
|
|
||||||
this.updatingMerchant = false;
|
uploadObservable$ = this.minioService.uploadMerchantLogo(
|
||||||
this.modalService.dismissAll();
|
merchantId,
|
||||||
this.refreshMerchantsConfigsView();
|
merchantName,
|
||||||
this.refreshMerchantsList();
|
this.editLogoFile
|
||||||
|
).pipe(
|
||||||
|
switchMap(uploadResponse => {
|
||||||
|
console.log('✅ New logo uploaded:', uploadResponse);
|
||||||
|
this.uploadingLogo = false;
|
||||||
|
|
||||||
// Mettre à jour le cache
|
// Supprimer l'ancien logo si différent
|
||||||
if (this.selectedMerchantId) {
|
const oldLogo = this.selectedMerchantForEdit!.logo;
|
||||||
this.merchantProfiles[this.selectedMerchantId] = frontendMerchant;
|
if (oldLogo && oldLogo !== uploadResponse.data.fileName) {
|
||||||
|
|
||||||
|
this.logoUrlCache.delete(oldLogo);
|
||||||
|
|
||||||
|
this.minioService.deleteMerchantLogo(String(this.selectedMerchantForEdit?.id), oldLogo).subscribe({
|
||||||
|
next: () => console.log('🗑️ Old logo deleted'),
|
||||||
|
error: (err) => console.warn('⚠️ Could not delete old logo:', err)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mettre à jour le marchand de l'utilisateur si nécessaire
|
// Mettre à jour le nom du logo dans l'objet merchant
|
||||||
if (this.isMerchantUser && this.userMerchantId === merchantId) {
|
this.selectedMerchantForEdit!.logo = uploadResponse.data.fileName;
|
||||||
this.userMerchant = frontendMerchant;
|
|
||||||
|
console.log('Logo : ' + this.selectedMerchantForEdit!.logo)
|
||||||
|
|
||||||
|
// Retourner l'observable pour la mise à jour du marchand
|
||||||
|
const updateDto = this.convertUpdateMerchantToBackend(
|
||||||
|
this.selectedMerchantForEdit!
|
||||||
|
);
|
||||||
|
|
||||||
|
return this.merchantConfigService.updateMerchant(merchantId, updateDto);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Pas de nouveau logo, mettre à jour directement
|
||||||
|
const updateDto = this.convertUpdateMerchantToBackend(
|
||||||
|
this.selectedMerchantForEdit!
|
||||||
|
);
|
||||||
|
|
||||||
|
uploadObservable$ = this.merchantConfigService.updateMerchant(merchantId, updateDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadObservable$.pipe(
|
||||||
|
takeUntil(this.destroy$)
|
||||||
|
).subscribe({
|
||||||
|
next: (updatedMerchant) => {
|
||||||
|
const frontendMerchant = this.convertMerchantToFrontend(updatedMerchant);
|
||||||
|
|
||||||
|
this.updatingMerchant = false;
|
||||||
|
this.modalService.dismissAll();
|
||||||
|
this.refreshMerchantsConfigsView();
|
||||||
|
this.refreshMerchantsList();
|
||||||
|
|
||||||
|
// Mettre à jour le cache
|
||||||
|
if (merchantId) {
|
||||||
|
this.merchantProfiles[merchantId] = frontendMerchant;
|
||||||
|
|
||||||
|
const cacheKey = `${merchantId}_${frontendMerchant.name}`;
|
||||||
|
|
||||||
|
// Invalider le cache de l'URL du logo
|
||||||
|
if (frontendMerchant.logo) {
|
||||||
|
this.logoUrlCache.set(cacheKey, frontendMerchant.logo);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.successMessage = 'Marchand modifié avec succès';
|
|
||||||
|
|
||||||
this.cdRef.detectChanges();
|
|
||||||
},
|
|
||||||
error: (error) => {
|
|
||||||
console.error('❌ Error updating merchant:', error);
|
|
||||||
this.updatingMerchant = false;
|
|
||||||
this.updateMerchantError = this.getUpdateErrorMessage(error);
|
|
||||||
this.cdRef.detectChanges();
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
if (this.isMerchantUser && this.userMerchantId === merchantId) {
|
||||||
|
this.userMerchant = frontendMerchant;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.successMessage = 'Marchand modifié avec succès';
|
||||||
|
|
||||||
|
// Reset les états du logo
|
||||||
|
this.cancelEditLogo();
|
||||||
|
|
||||||
|
this.cdRef.detectChanges();
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('❌ Error in update process:', error);
|
||||||
|
this.updatingMerchant = false;
|
||||||
|
this.uploadingLogo = false;
|
||||||
|
this.updateMerchantError = this.getUpdateErrorMessage(error);
|
||||||
|
this.cdRef.detectChanges();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== AFFICHAGE DU LOGO ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère l'URL du logo avec fallback automatique
|
||||||
|
*/
|
||||||
|
getMerchantLogoUrl(
|
||||||
|
merchantId: string,
|
||||||
|
merchantName: string,
|
||||||
|
logoFileName: string,
|
||||||
|
): Observable<string> {
|
||||||
|
|
||||||
|
const cacheKey = `${merchantId}_${merchantName}`;
|
||||||
|
|
||||||
|
// Vérifier si le logo est en cache d'erreur
|
||||||
|
if (this.logoErrorCache.has(cacheKey)) {
|
||||||
|
const defaultLogo = this.getDefaultLogoUrl(merchantName);
|
||||||
|
return of(defaultLogo);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier le cache normal
|
||||||
|
if (this.logoUrlCache.has(cacheKey)) {
|
||||||
|
return of(this.logoUrlCache.get(cacheKey)!);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Récupérer l'URL depuis l'API avec la nouvelle structure
|
||||||
|
return this.minioService.getMerchantLogoUrl(
|
||||||
|
merchantId,
|
||||||
|
logoFileName,
|
||||||
|
{ signed: true, expirySeconds: 3600 }
|
||||||
|
).pipe(
|
||||||
|
map(response => {
|
||||||
|
// Extraire l'URL de la réponse
|
||||||
|
const url = response.data.url ;
|
||||||
|
|
||||||
|
// Mettre en cache avec la clé composite
|
||||||
|
this.logoUrlCache.set(cacheKey, url);
|
||||||
|
|
||||||
|
return url;
|
||||||
|
}),
|
||||||
|
catchError(error => {
|
||||||
|
console.warn(`⚠️ Logo not found for merchant ${merchantId}: ${logoFileName}`, error);
|
||||||
|
|
||||||
|
// En cas d'erreur, ajouter au cache d'erreur
|
||||||
|
this.logoErrorCache.add(cacheKey);
|
||||||
|
|
||||||
|
// Générer un logo par défaut
|
||||||
|
const defaultLogo = this.getDefaultLogoUrl(merchantName);
|
||||||
|
|
||||||
|
// Mettre le logo par défaut dans le cache normal aussi
|
||||||
|
this.logoUrlCache.set(cacheKey, defaultLogo);
|
||||||
|
|
||||||
|
return of(defaultLogo);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Génère une URL de logo par défaut basée sur les initiales
|
||||||
|
*/
|
||||||
|
getDefaultLogoUrl(merchantName: string): string {
|
||||||
|
// Créer des initiales significatives
|
||||||
|
const initials = this.extractInitials(merchantName);
|
||||||
|
|
||||||
|
// Palette de couleurs agréables
|
||||||
|
const colors = [
|
||||||
|
'667eea', // Violet
|
||||||
|
'764ba2', // Violet foncé
|
||||||
|
'f56565', // Rouge
|
||||||
|
'4299e1', // Bleu
|
||||||
|
'48bb78', // Vert
|
||||||
|
'ed8936', // Orange
|
||||||
|
'FF6B6B', // Rouge clair
|
||||||
|
'4ECDC4', // Turquoise
|
||||||
|
'45B7D1', // Bleu clair
|
||||||
|
'96CEB4' // Vert menthe
|
||||||
|
];
|
||||||
|
|
||||||
|
const colorIndex = merchantName.length % colors.length;
|
||||||
|
const backgroundColor = colors[colorIndex];
|
||||||
|
|
||||||
|
// Taille fixe à 80px (l'API génère un carré de cette taille)
|
||||||
|
// L'image sera redimensionnée à 40px via CSS
|
||||||
|
return `https://ui-avatars.com/api/?name=${encodeURIComponent(initials)}&background=${backgroundColor}&color=FFFFFF&size=80`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extrait les initiales de manière intelligente
|
||||||
|
*/
|
||||||
|
private extractInitials(name: string): string {
|
||||||
|
if (!name || name.trim() === '') {
|
||||||
|
return '??';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nettoyer le nom
|
||||||
|
const cleanedName = name.trim().toUpperCase();
|
||||||
|
|
||||||
|
// Extraire les mots
|
||||||
|
const words = cleanedName.split(/\s+/);
|
||||||
|
|
||||||
|
// Si un seul mot, prendre les deux premières lettres
|
||||||
|
if (words.length === 1) {
|
||||||
|
return words[0].substring(0, 2) || '??';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prendre la première lettre des deux premiers mots
|
||||||
|
const initials = words
|
||||||
|
.slice(0, 2) // Prendre les 2 premiers mots
|
||||||
|
.map(word => word[0] || '')
|
||||||
|
.join('');
|
||||||
|
|
||||||
|
return initials || name.substring(0, 2).toUpperCase() || '??';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gère les erreurs de chargement des logos MinIO
|
||||||
|
*/
|
||||||
|
onLogoError(event: Event, merchantName: string): void {
|
||||||
|
const img = event.target as HTMLImageElement;
|
||||||
|
|
||||||
|
if (!img) return;
|
||||||
|
|
||||||
|
console.warn('Logo MinIO failed to load, using default for:', merchantName);
|
||||||
|
|
||||||
|
img.onerror = null;
|
||||||
|
img.src = this.getDefaultLogoUrl(merchantName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gère les erreurs de chargement des logos par défaut
|
||||||
|
*/
|
||||||
|
onDefaultLogoError(event: Event | string): void {
|
||||||
|
if (!(event instanceof Event)) {
|
||||||
|
console.error('Default logo error (non-event):', event);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const img = event.target as HTMLImageElement | null;
|
||||||
|
if (!img) return;
|
||||||
|
|
||||||
|
console.error('Default logo also failed to load, using fallback SVG');
|
||||||
|
|
||||||
|
// SVG local
|
||||||
|
img.onerror = null; // éviter boucle infinie
|
||||||
|
img.src = 'assets/images/default-merchant-logo.svg';
|
||||||
|
|
||||||
|
// Dernier recours
|
||||||
|
img.onerror = (e) => {
|
||||||
|
if (!(e instanceof Event)) return;
|
||||||
|
const fallbackImg = e.target as HTMLImageElement | null;
|
||||||
|
if (!fallbackImg) return;
|
||||||
|
|
||||||
|
fallbackImg.onerror = null;
|
||||||
|
fallbackImg.src = this.generateFallbackDataUrl();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Génère un fallback SVG en data URL
|
||||||
|
*/
|
||||||
|
private generateFallbackDataUrl(): string {
|
||||||
|
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40">
|
||||||
|
<rect width="40" height="40" fill="#667eea" rx="20"/>
|
||||||
|
<text x="20" y="22" text-anchor="middle" fill="white" font-family="Arial" font-size="14" font-weight="bold">?</text>
|
||||||
|
</svg>`;
|
||||||
|
|
||||||
|
return 'data:image/svg+xml;base64,' + btoa(svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Charge le logo pour l'édition
|
||||||
|
*/
|
||||||
|
loadMerchantLogoForEdit(merchant: Merchant): void {
|
||||||
|
if (!merchant.logo) {
|
||||||
|
this.currentLogoUrl = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.getMerchantLogoUrl(String(merchant.id), merchant.name, merchant.logo).subscribe({
|
||||||
|
next: (url) => {
|
||||||
|
this.currentLogoUrl = url;
|
||||||
|
this.cdRef.detectChanges();
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error loading logo for edit:', error);
|
||||||
|
this.currentLogoUrl = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validation complète pour la mise à jour
|
// Validation complète pour la mise à jour
|
||||||
@ -774,57 +1168,12 @@ export class MerchantConfigManagement implements OnInit, OnDestroy {
|
|||||||
errors.push('Le téléphone est requis');
|
errors.push('Le téléphone est requis');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validation des configurations
|
|
||||||
if (!merchant.configs || merchant.configs.length === 0) {
|
|
||||||
errors.push('Au moins une configuration est requise');
|
|
||||||
} else {
|
|
||||||
merchant.configs.forEach((config, index) => {
|
|
||||||
if (!config.name?.trim()) {
|
|
||||||
errors.push(`Le type de configuration ${index + 1} est requis`);
|
|
||||||
}
|
|
||||||
if (!config.value?.trim()) {
|
|
||||||
errors.push(`La valeur de configuration ${index + 1} est requise`);
|
|
||||||
}
|
|
||||||
if (!config.operatorId) {
|
|
||||||
errors.push(`L'opérateur de configuration ${index + 1} est requis`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validation des contacts techniques
|
|
||||||
if (!merchant.technicalContacts || merchant.technicalContacts.length === 0) {
|
|
||||||
errors.push('Au moins un contact technique est requis');
|
|
||||||
} else {
|
|
||||||
merchant.technicalContacts.forEach((contact, index) => {
|
|
||||||
if (!contact.firstName?.trim()) {
|
|
||||||
errors.push(`Le prénom du contact ${index + 1} est requis`);
|
|
||||||
}
|
|
||||||
if (!contact.lastName?.trim()) {
|
|
||||||
errors.push(`Le nom du contact ${index + 1} est requis`);
|
|
||||||
}
|
|
||||||
if (!contact.phone?.trim()) {
|
|
||||||
errors.push(`Le téléphone du contact ${index + 1} est requis`);
|
|
||||||
}
|
|
||||||
if (!contact.email?.trim()) {
|
|
||||||
errors.push(`L'email du contact ${index + 1} est requis`);
|
|
||||||
} else if (!this.isValidEmail(contact.email)) {
|
|
||||||
errors.push(`L'email du contact ${index + 1} est invalide`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isValid: errors.length === 0,
|
isValid: errors.length === 0,
|
||||||
errors: errors
|
errors: errors
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validation d'email
|
|
||||||
private isValidEmail(email: string): boolean {
|
|
||||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
||||||
return emailRegex.test(email);
|
|
||||||
}
|
|
||||||
|
|
||||||
confirmDeleteMerchant(): void {
|
confirmDeleteMerchant(): void {
|
||||||
if (!this.selectedMerchantForDelete) {
|
if (!this.selectedMerchantForDelete) {
|
||||||
this.deleteMerchantError = 'Aucun marchand sélectionné pour suppression';
|
this.deleteMerchantError = 'Aucun marchand sélectionné pour suppression';
|
||||||
@ -891,17 +1240,28 @@ export class MerchantConfigManagement implements OnInit, OnDestroy {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private resetMerchantForm(): void {
|
|
||||||
this.newMerchant = this.getDefaultMerchantForm();
|
|
||||||
console.log('🔄 Merchant form reset');
|
|
||||||
}
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override de populateEditForm pour charger le logo
|
||||||
|
*/
|
||||||
private populateEditForm(merchant: Merchant): void {
|
private populateEditForm(merchant: Merchant): void {
|
||||||
this.selectedMerchantForEdit = {
|
this.selectedMerchantForEdit = {
|
||||||
...merchant,
|
...merchant,
|
||||||
configs: merchant.configs.map(config => ({ ...config })),
|
configs: merchant.configs.map(config => ({ ...config })),
|
||||||
technicalContacts: merchant.technicalContacts.map(contact => ({ ...contact }))
|
technicalContacts: merchant.technicalContacts.map(contact => ({ ...contact }))
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Charger le logo actuel
|
||||||
|
this.loadMerchantLogoForEdit(merchant);
|
||||||
|
this.editLogoFile = null;
|
||||||
|
this.editLogoPreviewUrl = null;
|
||||||
|
this.logoChanged = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private resetMerchantForm(): void {
|
||||||
|
this.newMerchant = this.getDefaultMerchantForm();
|
||||||
|
this.removeSelectedLogo();
|
||||||
|
console.log('🔄 Merchant form reset');
|
||||||
}
|
}
|
||||||
|
|
||||||
private refreshMerchantsList(): void {
|
private refreshMerchantsList(): void {
|
||||||
@ -1025,8 +1385,14 @@ export class MerchantConfigManagement implements OnInit, OnDestroy {
|
|||||||
return !this.isMerchantUser && this.activeTab === 'list';
|
return !this.isMerchantUser && this.activeTab === 'list';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== NETTOYAGE ====================
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
this.destroy$.next();
|
this.destroy$.next();
|
||||||
this.destroy$.complete();
|
this.destroy$.complete();
|
||||||
|
|
||||||
|
// Nettoyer les logos
|
||||||
|
this.cleanupLogoResources();
|
||||||
|
this.logoUrlCache.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -29,7 +29,7 @@ export class MerchantDataAdapter {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...apiMerchant,
|
...apiMerchant,
|
||||||
id: apiMerchant.id, //this.convertIdToString(apiMerchant.id),
|
id: apiMerchant.id,
|
||||||
configs: (apiMerchant.configs || []).map(config =>
|
configs: (apiMerchant.configs || []).map(config =>
|
||||||
this.convertApiConfigToFrontend(config)
|
this.convertApiConfigToFrontend(config)
|
||||||
),
|
),
|
||||||
@ -105,7 +105,7 @@ export class MerchantDataAdapter {
|
|||||||
/**
|
/**
|
||||||
* Convertit un DTO de mise à jour pour l'API
|
* Convertit un DTO de mise à jour pour l'API
|
||||||
*/
|
*/
|
||||||
convertUpdateMerchantToApi(dto: UpdateMerchantDto, existingMerchant?: Merchant): any {
|
convertUpdateMerchantToApi(dto: UpdateMerchantDto): any {
|
||||||
this.validateUpdateMerchantDto(dto);
|
this.validateUpdateMerchantDto(dto);
|
||||||
|
|
||||||
const updateData: any = {};
|
const updateData: any = {};
|
||||||
@ -117,33 +117,6 @@ export class MerchantDataAdapter {
|
|||||||
if (dto.adresse !== undefined) updateData.adresse = dto.adresse?.trim();
|
if (dto.adresse !== undefined) updateData.adresse = dto.adresse?.trim();
|
||||||
if (dto.phone !== undefined) updateData.phone = dto.phone?.trim();
|
if (dto.phone !== undefined) updateData.phone = dto.phone?.trim();
|
||||||
|
|
||||||
// Configurations - seulement si présentes dans le DTO
|
|
||||||
if (dto.configs !== undefined) {
|
|
||||||
updateData.configs = (dto.configs || []).map(config => {
|
|
||||||
const apiConfig: any = {
|
|
||||||
name: config.name,
|
|
||||||
value: config.value?.trim(),
|
|
||||||
operatorId: this.validateOperatorId(config.operatorId)
|
|
||||||
};
|
|
||||||
|
|
||||||
return apiConfig;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Contacts techniques - seulement si présents dans le DTO
|
|
||||||
if (dto.technicalContacts !== undefined) {
|
|
||||||
updateData.technicalContacts = (dto.technicalContacts || []).map(contact => {
|
|
||||||
const apiContact: any = {
|
|
||||||
firstName: contact.firstName?.trim(),
|
|
||||||
lastName: contact.lastName?.trim(),
|
|
||||||
phone: contact.phone?.trim(),
|
|
||||||
email: contact.email?.trim()
|
|
||||||
};
|
|
||||||
|
|
||||||
return apiContact;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return updateData;
|
return updateData;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -250,49 +223,6 @@ export class MerchantDataAdapter {
|
|||||||
errors.push('Le téléphone est requis');
|
errors.push('Le téléphone est requis');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validation des configurations si présentes
|
|
||||||
if (dto.configs !== undefined) {
|
|
||||||
if (dto.configs.length === 0) {
|
|
||||||
errors.push('Au moins une configuration est requise');
|
|
||||||
} else {
|
|
||||||
dto.configs.forEach((config, index) => {
|
|
||||||
if (!config.name?.trim()) {
|
|
||||||
errors.push(`Le type de configuration ${index + 1} est requis`);
|
|
||||||
}
|
|
||||||
if (!config.value?.trim()) {
|
|
||||||
errors.push(`La valeur de configuration ${index + 1} est requise`);
|
|
||||||
}
|
|
||||||
if (!config.operatorId) {
|
|
||||||
errors.push(`L'opérateur de configuration ${index + 1} est requis`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validation des contacts si présents
|
|
||||||
if (dto.technicalContacts !== undefined) {
|
|
||||||
if (dto.technicalContacts.length === 0) {
|
|
||||||
errors.push('Au moins un contact technique est requis');
|
|
||||||
} else {
|
|
||||||
dto.technicalContacts.forEach((contact, index) => {
|
|
||||||
if (!contact.firstName?.trim()) {
|
|
||||||
errors.push(`Le prénom du contact ${index + 1} est requis`);
|
|
||||||
}
|
|
||||||
if (!contact.lastName?.trim()) {
|
|
||||||
errors.push(`Le nom du contact ${index + 1} est requis`);
|
|
||||||
}
|
|
||||||
if (!contact.phone?.trim()) {
|
|
||||||
errors.push(`Le téléphone du contact ${index + 1} est requis`);
|
|
||||||
}
|
|
||||||
if (!contact.email?.trim()) {
|
|
||||||
errors.push(`L'email du contact ${index + 1} est requis`);
|
|
||||||
} else if (!this.isValidEmail(contact.email)) {
|
|
||||||
errors.push(`L'email du contact ${index + 1} est invalide`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (errors.length > 0) {
|
if (errors.length > 0) {
|
||||||
throw new Error(`Validation failed: ${errors.join(', ')}`);
|
throw new Error(`Validation failed: ${errors.join(', ')}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,16 +7,9 @@ import { MerchantUsersManagement } from '@modules/hub-users-management/merchant-
|
|||||||
|
|
||||||
// Composants principaux
|
// Composants principaux
|
||||||
import { DcbReportingDashboard } from '@modules/dcb-dashboard/dcb-reporting-dashboard';
|
import { DcbReportingDashboard } from '@modules/dcb-dashboard/dcb-reporting-dashboard';
|
||||||
import { Team } from '@modules/team/team';
|
|
||||||
import { Transactions } from '@modules/transactions/transactions';
|
import { Transactions } from '@modules/transactions/transactions';
|
||||||
import { OperatorsConfig } from '@modules/operators/config/config';
|
|
||||||
import { OperatorsStats } from '@modules/operators/stats/stats';
|
|
||||||
import { WebhooksHistory } from '@modules/webhooks/history/history';
|
|
||||||
import { WebhooksStatus } from '@modules/webhooks/status/status';
|
|
||||||
import { WebhooksRetry } from '@modules/webhooks/retry/retry';
|
|
||||||
import { Settings } from '@modules/settings/settings';
|
|
||||||
import { Integrations } from '@modules/integrations/integrations';
|
|
||||||
import { Support } from '@modules/support/support';
|
|
||||||
import { MyProfile } from '@modules/profile/profile';
|
import { MyProfile } from '@modules/profile/profile';
|
||||||
import { Documentation } from '@modules/documentation/documentation';
|
import { Documentation } from '@modules/documentation/documentation';
|
||||||
import { Help } from '@modules/help/help';
|
import { Help } from '@modules/help/help';
|
||||||
@ -39,18 +32,6 @@ const routes: Routes = [
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// ---------------------------
|
|
||||||
// Team
|
|
||||||
// ---------------------------
|
|
||||||
{
|
|
||||||
path: 'team',
|
|
||||||
component: Team,
|
|
||||||
canActivate: [authGuard, roleGuard],
|
|
||||||
data: {
|
|
||||||
title: 'Team',
|
|
||||||
module: 'team'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
// Transactions
|
// Transactions
|
||||||
@ -144,106 +125,10 @@ const routes: Routes = [
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// ---------------------------
|
|
||||||
// Operators (Admin seulement)
|
|
||||||
// ---------------------------
|
|
||||||
{
|
|
||||||
path: 'operators',
|
|
||||||
canActivate: [authGuard, roleGuard],
|
|
||||||
data: { module: 'operators' },
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: 'config',
|
|
||||||
component: OperatorsConfig,
|
|
||||||
data: {
|
|
||||||
title: 'Paramètres d\'Intégration',
|
|
||||||
module: 'operators/config'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'stats',
|
|
||||||
component: OperatorsStats,
|
|
||||||
data: {
|
|
||||||
title: 'Performance & Monitoring',
|
|
||||||
module: 'operators/stats'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
]
|
|
||||||
},
|
|
||||||
|
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
// Webhooks
|
// Profile (Tous les utilisateurs authentifiés)
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
{
|
|
||||||
path: 'webhooks',
|
|
||||||
canActivate: [authGuard, roleGuard],
|
|
||||||
data: { module: 'webhooks' },
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: 'history',
|
|
||||||
component: WebhooksHistory,
|
|
||||||
data: {
|
|
||||||
title: 'Historique',
|
|
||||||
module: 'webhooks/history'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'status',
|
|
||||||
component: WebhooksStatus,
|
|
||||||
data: {
|
|
||||||
title: 'Statut des Requêtes',
|
|
||||||
module: 'webhooks/status'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'retry',
|
|
||||||
component: WebhooksRetry,
|
|
||||||
data: {
|
|
||||||
title: 'Relancer Webhook',
|
|
||||||
module: 'webhooks/retry'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
]
|
|
||||||
},
|
|
||||||
|
|
||||||
// ---------------------------
|
|
||||||
// Settings
|
|
||||||
// ---------------------------
|
|
||||||
{
|
|
||||||
path: 'settings',
|
|
||||||
component: Settings,
|
|
||||||
canActivate: [authGuard, roleGuard],
|
|
||||||
data: {
|
|
||||||
title: 'Paramètres Système',
|
|
||||||
module: 'settings'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// ---------------------------
|
|
||||||
// Integrations (Admin seulement)
|
|
||||||
// ---------------------------
|
|
||||||
{
|
|
||||||
path: 'integrations',
|
|
||||||
component: Integrations,
|
|
||||||
canActivate: [authGuard, roleGuard],
|
|
||||||
data: {
|
|
||||||
title: 'Intégrations Externes',
|
|
||||||
module: 'integrations'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// ---------------------------
|
|
||||||
// Support & Profile (Tous les utilisateurs authentifiés)
|
|
||||||
// ---------------------------
|
|
||||||
{
|
|
||||||
path: 'support',
|
|
||||||
component: Support,
|
|
||||||
canActivate: [authGuard, roleGuard],
|
|
||||||
data: {
|
|
||||||
title: 'Support',
|
|
||||||
module: 'support'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: 'profile',
|
path: 'profile',
|
||||||
component: MyProfile,
|
component: MyProfile,
|
||||||
|
|||||||
@ -1 +0,0 @@
|
|||||||
<p>Notifications - Actions</p>
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
import { NotificationsActions } from './actions';
|
|
||||||
describe('NotificationsActions', () => {});
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
import { Component } from '@angular/core';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-notifications-actions',
|
|
||||||
templateUrl: './actions.html',
|
|
||||||
})
|
|
||||||
export class NotificationsActions {}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
<p>Notifications - Filters</p>
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
import { NotificationsFilters } from './filters';
|
|
||||||
describe('NotificationsFilters', () => {});
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
import { Component } from '@angular/core';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-notifications-filters',
|
|
||||||
templateUrl: './filters.html',
|
|
||||||
})
|
|
||||||
export class NotificationsFilters {}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
<p>Notifications - List</p>
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
import { NotificationsList } from './list';
|
|
||||||
describe('NotificationsList', () => {});
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
import { Component } from '@angular/core';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-notifications-list',
|
|
||||||
templateUrl: './list.html',
|
|
||||||
})
|
|
||||||
export class NotificationsList {}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
<p>Notifications</p>
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
import { Notifications } from './notifications';
|
|
||||||
describe('Notifications', () => {});
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
import { Component } from '@angular/core';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-notifications',
|
|
||||||
templateUrl: './notifications.html',
|
|
||||||
})
|
|
||||||
export class Notifications {}
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
import { Injectable } from '@angular/core';
|
|
||||||
|
|
||||||
@Injectable({
|
|
||||||
providedIn: 'root'
|
|
||||||
})
|
|
||||||
export class NotificationsActionsService {
|
|
||||||
constructor() {}
|
|
||||||
}
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
import { Injectable } from '@angular/core';
|
|
||||||
|
|
||||||
@Injectable({
|
|
||||||
providedIn: 'root'
|
|
||||||
})
|
|
||||||
export class NotificationsFiltersService {
|
|
||||||
constructor() {}
|
|
||||||
}
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
import { Injectable } from '@angular/core';
|
|
||||||
|
|
||||||
@Injectable({
|
|
||||||
providedIn: 'root'
|
|
||||||
})
|
|
||||||
export class NotificationsListService {
|
|
||||||
constructor() {}
|
|
||||||
}
|
|
||||||
@ -1,55 +0,0 @@
|
|||||||
import { Injectable, inject } from '@angular/core';
|
|
||||||
import { HttpClient } from '@angular/common/http';
|
|
||||||
import { environment } from '@environments/environment';
|
|
||||||
import { Observable } from 'rxjs';
|
|
||||||
|
|
||||||
export interface Notification {
|
|
||||||
id: string;
|
|
||||||
type: 'SMS' | 'EMAIL' | 'PUSH' | 'SYSTEM';
|
|
||||||
title: string;
|
|
||||||
message: string;
|
|
||||||
recipient: string;
|
|
||||||
status: 'SENT' | 'DELIVERED' | 'FAILED' | 'PENDING';
|
|
||||||
createdAt: Date;
|
|
||||||
sentAt?: Date;
|
|
||||||
errorMessage?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface NotificationFilter {
|
|
||||||
type?: string;
|
|
||||||
status?: string;
|
|
||||||
startDate?: Date;
|
|
||||||
endDate?: Date;
|
|
||||||
recipient?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
|
||||||
export class NotificationService {
|
|
||||||
private http = inject(HttpClient);
|
|
||||||
private apiUrl = `${environment.localServiceTestApiUrl}/notifications`;
|
|
||||||
|
|
||||||
getNotifications(filters?: NotificationFilter): Observable<Notification[]> {
|
|
||||||
return this.http.post<Notification[]>(
|
|
||||||
`${this.apiUrl}/list`,
|
|
||||||
filters
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
sendNotification(notification: Partial<Notification>): Observable<Notification> {
|
|
||||||
return this.http.post<Notification>(
|
|
||||||
`${this.apiUrl}/send`,
|
|
||||||
notification
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
getNotificationStats(): Observable<any> {
|
|
||||||
return this.http.get(`${this.apiUrl}/stats`);
|
|
||||||
}
|
|
||||||
|
|
||||||
retryNotification(notificationId: string): Observable<Notification> {
|
|
||||||
return this.http.post<Notification>(
|
|
||||||
`${this.apiUrl}/${notificationId}/retry`,
|
|
||||||
{}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
<p>Operators - Config</p>
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
import { OperatorsConfig } from './config';
|
|
||||||
describe('OperatorsConfig', () => {});
|
|
||||||
@ -1,36 +0,0 @@
|
|||||||
import { Component } from '@angular/core';
|
|
||||||
import { FormsModule } from '@angular/forms';
|
|
||||||
import { UiCard } from '@app/components/ui-card';
|
|
||||||
import { InputFields } from '@/app/modules/components/input-fields';
|
|
||||||
import { CheckboxesAndRadios } from '@/app/modules/components/checkboxes-and-radios';
|
|
||||||
import { InputTouchspin } from '@/app/modules/components/input-touchspin';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-operator-config',
|
|
||||||
//imports: [FormsModule, UiCard, InputFields, CheckboxesAndRadios, InputTouchspin],
|
|
||||||
templateUrl: './config.html',
|
|
||||||
})
|
|
||||||
export class OperatorsConfig {
|
|
||||||
operatorConfig = {
|
|
||||||
name: '',
|
|
||||||
apiEndpoint: '',
|
|
||||||
apiKey: '',
|
|
||||||
secretKey: '',
|
|
||||||
timeout: 30,
|
|
||||||
retryAttempts: 3,
|
|
||||||
webhookUrl: '',
|
|
||||||
isActive: true,
|
|
||||||
supportedCountries: [] as string[],
|
|
||||||
supportedServices: ['DCB', 'SMS', 'USSD']
|
|
||||||
};
|
|
||||||
|
|
||||||
countries = ['CIV', 'SEN', 'CMR', 'COD', 'TUN', 'BFA', 'MLI', 'GIN'];
|
|
||||||
|
|
||||||
saveConfig() {
|
|
||||||
console.log('Saving operator config:', this.operatorConfig);
|
|
||||||
}
|
|
||||||
|
|
||||||
testConnection() {
|
|
||||||
console.log('Testing connection to:', this.operatorConfig.apiEndpoint);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
<p>Operators</p>
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
import { Operators } from './operators';
|
|
||||||
describe('Operators', () => {});
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
import { Component } from '@angular/core';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-operators',
|
|
||||||
templateUrl: './operators.html',
|
|
||||||
})
|
|
||||||
export class Operators {}
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
import { Injectable } from '@angular/core';
|
|
||||||
|
|
||||||
@Injectable({
|
|
||||||
providedIn: 'root'
|
|
||||||
})
|
|
||||||
export class OperatorsConfigService {
|
|
||||||
constructor() {}
|
|
||||||
}
|
|
||||||
@ -1,47 +0,0 @@
|
|||||||
import { Injectable, inject } from '@angular/core';
|
|
||||||
import { HttpClient } from '@angular/common/http';
|
|
||||||
import { environment } from '@environments/environment';
|
|
||||||
import { Observable } from 'rxjs';
|
|
||||||
|
|
||||||
export interface Operator {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
country: string;
|
|
||||||
status: 'ACTIVE' | 'INACTIVE';
|
|
||||||
config: OperatorConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface OperatorConfig {
|
|
||||||
apiEndpoint: string;
|
|
||||||
apiKey: string;
|
|
||||||
secretKey: string;
|
|
||||||
timeout: number;
|
|
||||||
retryAttempts: number;
|
|
||||||
webhookUrl: string;
|
|
||||||
isActive: boolean;
|
|
||||||
supportedServices: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
|
||||||
export class OperatorService {
|
|
||||||
private http = inject(HttpClient);
|
|
||||||
private apiUrl = `${environment.localServiceTestApiUrl}/operators`;
|
|
||||||
|
|
||||||
getOperators(): Observable<Operator[]> {
|
|
||||||
return this.http.get<Operator[]>(`${this.apiUrl}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateOperatorConfig(operatorId: string, config: OperatorConfig): Observable<Operator> {
|
|
||||||
return this.http.put<Operator>(
|
|
||||||
`${this.apiUrl}/${operatorId}/config`,
|
|
||||||
config
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
testConnection(operatorId: string): Observable<{ success: boolean; latency: number }> {
|
|
||||||
return this.http.post<{ success: boolean; latency: number }>(
|
|
||||||
`${this.apiUrl}/${operatorId}/test-connection`,
|
|
||||||
{}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,44 +0,0 @@
|
|||||||
import { Injectable, inject } from '@angular/core';
|
|
||||||
import { HttpClient } from '@angular/common/http';
|
|
||||||
import { environment } from '@environments/environment';
|
|
||||||
import { Observable } from 'rxjs';
|
|
||||||
|
|
||||||
export interface OperatorStats {
|
|
||||||
operatorId: string;
|
|
||||||
totalTransactions: number;
|
|
||||||
successRate: number;
|
|
||||||
totalRevenue: number;
|
|
||||||
averageLatency: number;
|
|
||||||
errorCount: number;
|
|
||||||
uptime: number;
|
|
||||||
dailyStats: DailyStat[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DailyStat {
|
|
||||||
date: string;
|
|
||||||
transactions: number;
|
|
||||||
successRate: number;
|
|
||||||
revenue: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
|
||||||
export class OperatorStatsService {
|
|
||||||
private http = inject(HttpClient);
|
|
||||||
private apiUrl = `${environment.localServiceTestApiUrl}/operators`;
|
|
||||||
|
|
||||||
getOperatorStats(operatorId: string): Observable<OperatorStats> {
|
|
||||||
return this.http.get<OperatorStats>(
|
|
||||||
`${this.apiUrl}/${operatorId}/stats`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
getOperatorsComparison(): Observable<any[]> {
|
|
||||||
return this.http.get<any[]>(`${this.apiUrl}/comparison`);
|
|
||||||
}
|
|
||||||
|
|
||||||
getPerformanceMetrics(operatorId: string, period: string): Observable<any> {
|
|
||||||
return this.http.get(
|
|
||||||
`${this.apiUrl}/${operatorId}/metrics?period=${period}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
<p>Operators - Stats</p>
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
import { OperatorsStats } from './stats';
|
|
||||||
describe('OperatorsStats', () => {});
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
import { Component } from '@angular/core';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-operators-stats',
|
|
||||||
templateUrl: './stats.html',
|
|
||||||
})
|
|
||||||
export class OperatorsStats {}
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
import { Injectable } from '@angular/core';
|
|
||||||
|
|
||||||
@Injectable({
|
|
||||||
providedIn: 'root'
|
|
||||||
})
|
|
||||||
export class SettingsService {
|
|
||||||
constructor() {}
|
|
||||||
}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
<p>Settings</p>
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
import { Routes } from '@angular/router';
|
|
||||||
import { Settings } from './settings';
|
|
||||||
import { authGuard } from '../../core/guards/auth.guard';
|
|
||||||
import { roleGuard } from '../../core/guards/role.guard';
|
|
||||||
|
|
||||||
export const SETTINGS_ROUTES: Routes = [
|
|
||||||
{
|
|
||||||
path: 'settings',
|
|
||||||
canActivate: [authGuard, roleGuard],
|
|
||||||
component: Settings,
|
|
||||||
data: {
|
|
||||||
title: 'Configuration',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
];
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
import { Settings } from './settings';
|
|
||||||
describe('Settings', () => {});
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
import { Component } from '@angular/core';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-settings',
|
|
||||||
templateUrl: './settings.html',
|
|
||||||
})
|
|
||||||
export class Settings {}
|
|
||||||
@ -1,628 +0,0 @@
|
|||||||
import { Component, OnInit } from '@angular/core';
|
|
||||||
import { MerchantSyncService } from '../hub-users-management/merchant-sync-orchestrator.service';
|
|
||||||
import { UserRole } from '@core/models/dcb-bo-hub-user.model';
|
|
||||||
import { firstValueFrom } from 'rxjs';
|
|
||||||
import { MerchantUsersService } from '@modules/hub-users-management/merchant-users.service';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-support',
|
|
||||||
templateUrl: './support.html'
|
|
||||||
})
|
|
||||||
export class Support implements OnInit {
|
|
||||||
|
|
||||||
private testData = {
|
|
||||||
merchantConfigId: '',
|
|
||||||
keycloakUserId: '',
|
|
||||||
testUserId: '',
|
|
||||||
testMerchantConfigUserId: '',
|
|
||||||
associatedUserId: '' // ID d'un utilisateur associé au marchand
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private merchantCrudService: MerchantSyncService,
|
|
||||||
private merchantUsersService: MerchantUsersService
|
|
||||||
) {}
|
|
||||||
|
|
||||||
ngOnInit() {
|
|
||||||
console.log('🚀 Support Component - Tests CRUD Merchants');
|
|
||||||
console.log('='.repeat(60));
|
|
||||||
console.log('📌 NOUVELLE LOGIQUE: Merchant et User indépendants, association séparée');
|
|
||||||
console.log('='.repeat(60));
|
|
||||||
|
|
||||||
// Démarrer les tests automatiquement
|
|
||||||
this.runAllTests();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Exécuter tous les tests
|
|
||||||
*/
|
|
||||||
async runAllTests(): Promise<void> {
|
|
||||||
try {
|
|
||||||
console.log('\n🧪 DÉMARRAGE DES TESTS COMPLETS');
|
|
||||||
console.log('📌 NOUVELLE LOGIQUE: Association séparée');
|
|
||||||
console.log('='.repeat(50));
|
|
||||||
|
|
||||||
// Test 1: Création indépendante
|
|
||||||
await this.testCreateOperations();
|
|
||||||
|
|
||||||
// Test 2: Association
|
|
||||||
await this.testAssociationOperations();
|
|
||||||
|
|
||||||
// Test 3: Lecture
|
|
||||||
await this.testReadOperations();
|
|
||||||
|
|
||||||
// Test 4: Mise à jour
|
|
||||||
await this.testUpdateOperations();
|
|
||||||
|
|
||||||
// Test 5: Gestion utilisateurs
|
|
||||||
await this.testUserOperations();
|
|
||||||
|
|
||||||
// Test 6: Suppression
|
|
||||||
await this.testDeleteOperations();
|
|
||||||
|
|
||||||
console.log('\n✅ TOUS LES TESTS TERMINÉS AVEC SUCCÈS!');
|
|
||||||
console.log('='.repeat(50));
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ ERREUR CRITIQUE DANS LES TESTS:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TEST 1: Opérations de création indépendante
|
|
||||||
*/
|
|
||||||
private async testCreateOperations(): Promise<void> {
|
|
||||||
console.log('\n📝 TEST 1: Opérations CREATE INDÉPENDANTES');
|
|
||||||
console.log('-'.repeat(40));
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 1.1 Créer un merchant uniquement dans MerchantConfig
|
|
||||||
console.log('1.1 Création d\'un merchant dans MerchantConfig seulement...');
|
|
||||||
|
|
||||||
const merchantData = {
|
|
||||||
name: 'Test Merchant ' + Date.now(),
|
|
||||||
adresse: '123 Test Street',
|
|
||||||
phone: '+336' + Math.floor(10000000 + Math.random() * 90000000),
|
|
||||||
configs: [
|
|
||||||
{
|
|
||||||
name: 'API_KEY',
|
|
||||||
value: 'test-api-key-' + Date.now(),
|
|
||||||
operatorId: 1
|
|
||||||
}
|
|
||||||
],
|
|
||||||
technicalContacts: [
|
|
||||||
{
|
|
||||||
firstName: 'Test',
|
|
||||||
lastName: 'User',
|
|
||||||
phone: '+33612345678',
|
|
||||||
email: `test.${Date.now()}@example.com`
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log('📤 Données merchant pour MerchantConfig:', merchantData);
|
|
||||||
|
|
||||||
const merchantConfigResult = await firstValueFrom(
|
|
||||||
this.merchantCrudService.createMerchantInConfigOnly(merchantData)
|
|
||||||
);
|
|
||||||
|
|
||||||
this.testData.merchantConfigId = String(merchantConfigResult.id!);
|
|
||||||
|
|
||||||
console.log('✅ Merchant créé dans MerchantConfig uniquement!');
|
|
||||||
console.log(' Merchant Config ID:', this.testData.merchantConfigId);
|
|
||||||
console.log(' Merchant name:', merchantConfigResult.name);
|
|
||||||
|
|
||||||
// 1.2 Créer un utilisateur Hub dans Keycloak
|
|
||||||
console.log('\n1.2 Création d\'un utilisateur Hub dans Keycloak...');
|
|
||||||
|
|
||||||
const hubUserData = {
|
|
||||||
username: `testhubuser.${Date.now()}`,
|
|
||||||
email: `hubuser.${Date.now()}@example.com`,
|
|
||||||
password: 'HubPassword123!',
|
|
||||||
firstName: 'Test',
|
|
||||||
lastName: 'HubUser',
|
|
||||||
role: UserRole.DCB_SUPPORT
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log('👤 Données Hub User pour Keycloak:', { ...hubUserData, password: '***' });
|
|
||||||
|
|
||||||
const hubUserResult = await firstValueFrom(
|
|
||||||
this.merchantCrudService.createKeycloakUser(hubUserData)
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log('✅ Utilisateur Hub créé dans Keycloak:');
|
|
||||||
console.log(' Keycloak User ID:', hubUserResult.id);
|
|
||||||
console.log(' Email:', hubUserResult.email);
|
|
||||||
console.log(' Rôle:', hubUserResult.role);
|
|
||||||
console.log(' User Type:', hubUserResult.userType);
|
|
||||||
|
|
||||||
// 1.3 Créer un utilisateur Partenaire dans Keycloak
|
|
||||||
console.log('\n1.3 Création d\'un utilisateur Partenaire dans Keycloak...');
|
|
||||||
|
|
||||||
const partnerUserData = {
|
|
||||||
username: `testpartner.${Date.now()}`,
|
|
||||||
email: `partner.${Date.now()}@example.com`,
|
|
||||||
password: 'PartnerPassword123!',
|
|
||||||
firstName: 'Test',
|
|
||||||
lastName: 'Partner',
|
|
||||||
role: UserRole.DCB_PARTNER_ADMIN
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log('👤 Données Partner User pour Keycloak:', { ...partnerUserData, password: '***' });
|
|
||||||
|
|
||||||
const partnerUserResult = await firstValueFrom(
|
|
||||||
this.merchantCrudService.createKeycloakUser(partnerUserData)
|
|
||||||
);
|
|
||||||
|
|
||||||
this.testData.testUserId = partnerUserResult.id;
|
|
||||||
|
|
||||||
console.log('✅ Utilisateur Partenaire créé dans Keycloak:');
|
|
||||||
console.log(' Keycloak User ID:', this.testData.testUserId);
|
|
||||||
console.log(' Email:', partnerUserResult.email);
|
|
||||||
console.log(' Rôle:', partnerUserResult.role);
|
|
||||||
console.log(' User Type:', partnerUserResult.userType);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ ERREUR lors de la création:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TEST 2: Opérations d'association
|
|
||||||
*/
|
|
||||||
private async testAssociationOperations(): Promise<void> {
|
|
||||||
console.log('\n🔗 TEST 2: Opérations d\'ASSOCIATION');
|
|
||||||
console.log('-'.repeat(40));
|
|
||||||
|
|
||||||
if (!this.testData.merchantConfigId || !this.testData.testUserId) {
|
|
||||||
console.log('⚠️ Merchant ou utilisateur non créé, passage au test suivant');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 2.1 Associer l'utilisateur au marchand
|
|
||||||
console.log('2.1 Association de l\'utilisateur au marchand...');
|
|
||||||
|
|
||||||
const associationData = {
|
|
||||||
userId: this.testData.testUserId,
|
|
||||||
merchantConfigId: this.testData.merchantConfigId,
|
|
||||||
role: UserRole.MERCHANT_CONFIG_ADMIN
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log('🔗 Données d\'association:', associationData);
|
|
||||||
|
|
||||||
const associationResult = await firstValueFrom(
|
|
||||||
this.merchantCrudService.associateUserToMerchant(associationData)
|
|
||||||
);
|
|
||||||
|
|
||||||
this.testData.associatedUserId = associationResult.keycloakUser.id;
|
|
||||||
this.testData.testMerchantConfigUserId = String(associationResult.merchantConfigUser?.userId || '');
|
|
||||||
|
|
||||||
console.log('✅ Utilisateur associé au marchand:');
|
|
||||||
console.log(' Keycloak User ID:', associationResult.keycloakUser.id);
|
|
||||||
console.log(' Merchant Config User ID:', this.testData.testMerchantConfigUserId);
|
|
||||||
console.log(' Rôle dans MerchantConfig:', associationResult.merchantConfigUser?.role);
|
|
||||||
console.log(' Associé au merchant:', this.testData.merchantConfigId);
|
|
||||||
|
|
||||||
// 2.2 Récupérer les utilisateurs associés au marchand
|
|
||||||
console.log('\n2.2 Lecture des utilisateurs associés au marchand...');
|
|
||||||
|
|
||||||
const merchantUsers = await firstValueFrom(
|
|
||||||
this.merchantCrudService.getUsersByMerchant(this.testData.merchantConfigId)
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(`✅ ${merchantUsers.length} utilisateurs associés à ce merchant`);
|
|
||||||
|
|
||||||
merchantUsers.forEach((user: any, index: number) => {
|
|
||||||
console.log(` ${index + 1}. ${user.email || 'Inconnu'}`);
|
|
||||||
console.log(` ID: ${user.id}`);
|
|
||||||
console.log(` Rôle: ${user.role}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ ERREUR lors de l\'association:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TEST 3: Opérations de lecture
|
|
||||||
*/
|
|
||||||
private async testReadOperations(): Promise<void> {
|
|
||||||
console.log('\n🔍 TEST 3: Opérations READ');
|
|
||||||
console.log('-'.repeat(40));
|
|
||||||
|
|
||||||
if (!this.testData.merchantConfigId) {
|
|
||||||
console.log('⚠️ Aucun merchant créé, passage au test suivant');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 3.1 Lire le merchant depuis MerchantConfig
|
|
||||||
console.log('3.1 Lecture du merchant depuis MerchantConfig...');
|
|
||||||
|
|
||||||
const merchant = await firstValueFrom(
|
|
||||||
this.merchantCrudService.getMerchantFromConfigOnly(
|
|
||||||
this.testData.merchantConfigId
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log('✅ Merchant récupéré depuis MerchantConfig:');
|
|
||||||
console.log(' ID:', merchant.id);
|
|
||||||
console.log(' Nom:', merchant.name);
|
|
||||||
console.log(' Adresse:', merchant.adresse);
|
|
||||||
console.log(' Configurations:', merchant.configs?.length || 0);
|
|
||||||
|
|
||||||
// 3.2 Lire tous les merchants depuis MerchantConfig
|
|
||||||
console.log('\n3.2 Lecture de tous les merchants depuis MerchantConfig...');
|
|
||||||
|
|
||||||
const allMerchants = await firstValueFrom(
|
|
||||||
this.merchantCrudService.getAllMerchantsFromConfig()
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(`✅ ${allMerchants.length} merchants trouvés au total`);
|
|
||||||
|
|
||||||
if (allMerchants.length > 0) {
|
|
||||||
const lastMerchant = allMerchants[allMerchants.length - 1];
|
|
||||||
console.log(' Dernier merchant:', lastMerchant.name, '(ID:', lastMerchant.id, ')');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3.3 Rechercher des merchants
|
|
||||||
console.log('\n3.3 Recherche de merchants dans MerchantConfig...');
|
|
||||||
|
|
||||||
const searchResults = await firstValueFrom(
|
|
||||||
this.merchantCrudService.searchMerchantsInConfig('Test')
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(`✅ ${searchResults.length} merchants trouvés avec "Test"`);
|
|
||||||
|
|
||||||
if (searchResults.length > 0) {
|
|
||||||
console.log(' Premier résultat:', searchResults[0].name);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3.4 Rechercher des utilisateurs
|
|
||||||
console.log('\n3.4 Recherche d\'utilisateurs dans Keycloak...');
|
|
||||||
|
|
||||||
const userSearchResults = await firstValueFrom(
|
|
||||||
this.merchantCrudService.searchKeycloakUsers('test')
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(`✅ ${userSearchResults.length} utilisateurs trouvés avec "test"`);
|
|
||||||
|
|
||||||
if (userSearchResults.length > 0) {
|
|
||||||
console.log(' Premier résultat:', userSearchResults[0].email);
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ ERREUR lors de la lecture:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TEST 4: Opérations de mise à jour
|
|
||||||
*/
|
|
||||||
private async testUpdateOperations(): Promise<void> {
|
|
||||||
console.log('\n✏️ TEST 4: Opérations UPDATE');
|
|
||||||
console.log('-'.repeat(40));
|
|
||||||
|
|
||||||
if (!this.testData.merchantConfigId) {
|
|
||||||
console.log('⚠️ Aucun merchant créé, passage au test suivant');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 4.1 Mettre à jour le merchant dans MerchantConfig
|
|
||||||
console.log('4.1 Mise à jour du merchant dans MerchantConfig...');
|
|
||||||
|
|
||||||
const newName = `Updated Merchant ${Date.now()}`;
|
|
||||||
|
|
||||||
const updateResult = await firstValueFrom(
|
|
||||||
this.merchantCrudService.updateMerchantInConfigOnly(
|
|
||||||
this.testData.merchantConfigId,
|
|
||||||
{
|
|
||||||
name: newName,
|
|
||||||
description: 'Mis à jour par les tests',
|
|
||||||
adresse: '456 Updated Street'
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log('✅ Merchant mis à jour dans MerchantConfig:');
|
|
||||||
console.log(' Nouveau nom:', newName);
|
|
||||||
console.log(' Description:', updateResult.description);
|
|
||||||
|
|
||||||
// 4.3 Changer le rôle de l'utilisateur dans MerchantConfig
|
|
||||||
console.log('\n4.3 Changement de rôle utilisateur dans MerchantConfig...');
|
|
||||||
|
|
||||||
if (this.testData.testUserId && this.testData.merchantConfigId && this.testData.testMerchantConfigUserId) {
|
|
||||||
const roleUpdateResult = await firstValueFrom(
|
|
||||||
this.merchantCrudService.updateUserRoleInMerchantConfig(
|
|
||||||
this.testData.merchantConfigId,
|
|
||||||
this.testData.testUserId,
|
|
||||||
UserRole.MERCHANT_CONFIG_MANAGER
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log('✅ Rôle utilisateur mis à jour dans MerchantConfig:');
|
|
||||||
console.log(' Nouveau rôle:', UserRole.MERCHANT_CONFIG_MANAGER);
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ ERREUR lors de la mise à jour:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TEST 5: Opérations utilisateurs avancées
|
|
||||||
*/
|
|
||||||
private async testUserOperations(): Promise<void> {
|
|
||||||
console.log('\n👥 TEST 5: Opérations utilisateurs avancées');
|
|
||||||
console.log('-'.repeat(40));
|
|
||||||
|
|
||||||
if (!this.testData.testUserId) {
|
|
||||||
console.log('⚠️ Aucun utilisateur créé, passage au test suivant');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 5.1 Réinitialiser le mot de passe
|
|
||||||
console.log('5.1 Réinitialisation mot de passe...');
|
|
||||||
|
|
||||||
const resetPasswordDto = {
|
|
||||||
newPassword: 'NewPassword123!',
|
|
||||||
temporary: false
|
|
||||||
};
|
|
||||||
|
|
||||||
const resetResult = await firstValueFrom(
|
|
||||||
this.merchantUsersService.resetMerchantUserPassword(
|
|
||||||
this.testData.testUserId,
|
|
||||||
resetPasswordDto
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log('✅ Mot de passe réinitialisé:');
|
|
||||||
console.log(' Message:', resetResult.message);
|
|
||||||
|
|
||||||
// 5.2 Dissocier l'utilisateur du marchand
|
|
||||||
console.log('\n5.2 Dissociation de l\'utilisateur du marchand...');
|
|
||||||
|
|
||||||
if (this.testData.testUserId && this.testData.merchantConfigId) {
|
|
||||||
const dissociateResult = await firstValueFrom(
|
|
||||||
this.merchantCrudService.dissociateUserFromMerchant(
|
|
||||||
this.testData.testUserId,
|
|
||||||
this.testData.merchantConfigId
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log('✅ Utilisateur dissocié du marchand:');
|
|
||||||
console.log(' Succès:', dissociateResult.success);
|
|
||||||
console.log(' Message:', dissociateResult.message);
|
|
||||||
|
|
||||||
// Vérifier que l'utilisateur n'est plus associé
|
|
||||||
const userMerchants = await firstValueFrom(
|
|
||||||
this.merchantCrudService.getUsersByMerchant(this.testData.merchantConfigId)
|
|
||||||
);
|
|
||||||
|
|
||||||
const userStillLinked = userMerchants.some(
|
|
||||||
user => user.id === this.testData.testUserId
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(` Marchands associés après dissociation: ${userMerchants.length}`);
|
|
||||||
|
|
||||||
if (userStillLinked) {
|
|
||||||
console.error('❌ L’utilisateur est encore associé au marchand ! ❌');
|
|
||||||
} else {
|
|
||||||
console.log('✅ L’utilisateur a été correctement dissocié du marchand.');
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5.3 Réassocier l'utilisateur (pour les tests suivants)
|
|
||||||
console.log('\n5.3 Réassociation de l\'utilisateur (pour les tests)...');
|
|
||||||
|
|
||||||
if (this.testData.testUserId && this.testData.merchantConfigId) {
|
|
||||||
const reassociationData = {
|
|
||||||
userId: this.testData.testUserId,
|
|
||||||
merchantConfigId: this.testData.merchantConfigId,
|
|
||||||
role: UserRole.MERCHANT_CONFIG_ADMIN
|
|
||||||
};
|
|
||||||
|
|
||||||
await firstValueFrom(
|
|
||||||
this.merchantCrudService.associateUserToMerchant(reassociationData)
|
|
||||||
);
|
|
||||||
console.log('✅ Utilisateur réassocié pour les tests suivants');
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ ERREUR opérations utilisateurs:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TEST 6: Opérations de suppression
|
|
||||||
*/
|
|
||||||
private async testDeleteOperations(): Promise<void> {
|
|
||||||
console.log('\n🗑️ TEST 6: Opérations DELETE');
|
|
||||||
console.log('-'.repeat(40));
|
|
||||||
|
|
||||||
if (!this.testData.merchantConfigId) {
|
|
||||||
console.log('⚠️ Aucun merchant créé, passage au test suivant');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 6.1 Dissocier avant suppression
|
|
||||||
console.log('6.1 Dissociation de l\'utilisateur avant suppression...');
|
|
||||||
|
|
||||||
if (this.testData.testUserId && this.testData.merchantConfigId) {
|
|
||||||
await firstValueFrom(
|
|
||||||
this.merchantCrudService.dissociateUserFromMerchant(
|
|
||||||
this.testData.testUserId,
|
|
||||||
this.testData.merchantConfigId
|
|
||||||
)
|
|
||||||
);
|
|
||||||
console.log('✅ Utilisateur dissocié');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 6.2 Supprimer l'utilisateur de Keycloak
|
|
||||||
console.log('\n6.2 Suppression de l\'utilisateur de Keycloak...');
|
|
||||||
|
|
||||||
if (this.testData.testUserId) {
|
|
||||||
const deleteUserResult = await firstValueFrom(
|
|
||||||
this.merchantCrudService.deleteKeycloakUser(
|
|
||||||
this.testData.testUserId
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log('✅ Utilisateur supprimé de Keycloak:');
|
|
||||||
console.log(' Succès:', deleteUserResult.success);
|
|
||||||
console.log(' Message:', deleteUserResult.message);
|
|
||||||
|
|
||||||
this.testData.testUserId = '';
|
|
||||||
this.testData.testMerchantConfigUserId = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
// 6.3 Supprimer le merchant de MerchantConfig
|
|
||||||
console.log('\n6.3 Suppression du merchant de MerchantConfig...');
|
|
||||||
|
|
||||||
const deleteMerchantResult = await firstValueFrom(
|
|
||||||
this.merchantCrudService.deleteMerchantFromConfigOnly(
|
|
||||||
this.testData.merchantConfigId
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log('✅ Merchant supprimé de MerchantConfig:');
|
|
||||||
console.log(' Succès:', deleteMerchantResult.success);
|
|
||||||
console.log(' Message:', deleteMerchantResult.message);
|
|
||||||
|
|
||||||
// 6.4 Vérifier la suppression
|
|
||||||
console.log('\n6.4 Vérification de la suppression...');
|
|
||||||
|
|
||||||
try {
|
|
||||||
await firstValueFrom(
|
|
||||||
this.merchantCrudService.getMerchantFromConfigOnly(
|
|
||||||
this.testData.merchantConfigId
|
|
||||||
)
|
|
||||||
);
|
|
||||||
console.log('❌ Le merchant existe toujours dans MerchantConfig - PROBLÈME!');
|
|
||||||
} catch (error) {
|
|
||||||
console.log('✅ Le merchant a bien été supprimé de MerchantConfig');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Réinitialiser toutes les données
|
|
||||||
this.testData = {
|
|
||||||
merchantConfigId: '',
|
|
||||||
keycloakUserId: '',
|
|
||||||
testUserId: '',
|
|
||||||
testMerchantConfigUserId: '',
|
|
||||||
associatedUserId: ''
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log('🧹 Données de test réinitialisées');
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ ERREUR lors de la suppression:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== MÉTHODES POUR TESTS INDIVIDUELS ====================
|
|
||||||
|
|
||||||
async testCreateOnly(): Promise<void> {
|
|
||||||
console.log('🧪 Test CREATE uniquement');
|
|
||||||
console.log('📌 Création indépendante');
|
|
||||||
await this.testCreateOperations();
|
|
||||||
}
|
|
||||||
|
|
||||||
async testAssociationOnly(): Promise<void> {
|
|
||||||
console.log('🧪 Test ASSOCIATION uniquement');
|
|
||||||
|
|
||||||
// Créer d'abord un merchant et un utilisateur si nécessaire
|
|
||||||
if (!this.testData.merchantConfigId) {
|
|
||||||
await this.createTestMerchant();
|
|
||||||
}
|
|
||||||
if (!this.testData.testUserId) {
|
|
||||||
await this.createTestUser();
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.testAssociationOperations();
|
|
||||||
}
|
|
||||||
|
|
||||||
async testReadOnly(): Promise<void> {
|
|
||||||
console.log('🧪 Test READ uniquement');
|
|
||||||
await this.testReadOperations();
|
|
||||||
}
|
|
||||||
|
|
||||||
async testUpdateOnly(): Promise<void> {
|
|
||||||
console.log('🧪 Test UPDATE uniquement');
|
|
||||||
await this.testUpdateOperations();
|
|
||||||
}
|
|
||||||
|
|
||||||
async testDeleteOnly(): Promise<void> {
|
|
||||||
console.log('🧪 Test DELETE uniquement');
|
|
||||||
await this.testDeleteOperations();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== MÉTHODES UTILITAIRES ====================
|
|
||||||
|
|
||||||
private async createTestMerchant(): Promise<void> {
|
|
||||||
const merchantData = {
|
|
||||||
name: 'Test Merchant ' + Date.now(),
|
|
||||||
adresse: '123 Test Street',
|
|
||||||
phone: '+336' + Math.floor(10000000 + Math.random() * 90000000),
|
|
||||||
configs: [],
|
|
||||||
technicalContacts: []
|
|
||||||
};
|
|
||||||
|
|
||||||
const merchant = await firstValueFrom(
|
|
||||||
this.merchantCrudService.createMerchantInConfigOnly(merchantData)
|
|
||||||
);
|
|
||||||
|
|
||||||
this.testData.merchantConfigId = String(merchant.id!);
|
|
||||||
console.log('✅ Merchant de test créé:', this.testData.merchantConfigId);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async createTestUser(): Promise<void> {
|
|
||||||
const userData = {
|
|
||||||
username: `testuser.${Date.now()}`,
|
|
||||||
email: `user.${Date.now()}@example.com`,
|
|
||||||
password: 'TestPassword123!',
|
|
||||||
firstName: 'Test',
|
|
||||||
lastName: 'User',
|
|
||||||
role: UserRole.DCB_PARTNER_ADMIN
|
|
||||||
};
|
|
||||||
|
|
||||||
const user = await firstValueFrom(
|
|
||||||
this.merchantCrudService.createKeycloakUser(userData)
|
|
||||||
);
|
|
||||||
|
|
||||||
this.testData.testUserId = user.id;
|
|
||||||
console.log('✅ Utilisateur de test créé:', this.testData.testUserId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Afficher l'état actuel des tests
|
|
||||||
*/
|
|
||||||
showTestStatus(): void {
|
|
||||||
console.log('\n📊 ÉTAT ACTUEL DES TESTS');
|
|
||||||
console.log('📌 NOUVELLE LOGIQUE: Association séparée');
|
|
||||||
console.log('-'.repeat(30));
|
|
||||||
console.log('Merchant Config ID:', this.testData.merchantConfigId || 'Non créé');
|
|
||||||
console.log('Keycloak User ID:', this.testData.testUserId || 'Non créé');
|
|
||||||
console.log('Associated User ID:', this.testData.associatedUserId || 'Non associé');
|
|
||||||
console.log('Merchant Config User ID:', this.testData.testMerchantConfigUserId || 'Non créé');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Réinitialiser les données de test
|
|
||||||
*/
|
|
||||||
resetTestData(): void {
|
|
||||||
this.testData = {
|
|
||||||
merchantConfigId: '',
|
|
||||||
keycloakUserId: '',
|
|
||||||
testUserId: '',
|
|
||||||
testMerchantConfigUserId: '',
|
|
||||||
associatedUserId: ''
|
|
||||||
};
|
|
||||||
console.log('🧹 Données de test réinitialisées');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
<p>Team</p>
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
import { Team } from './team';
|
|
||||||
describe('Team', () => {});
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
import { Component } from '@angular/core';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-team',
|
|
||||||
templateUrl: './team.html',
|
|
||||||
})
|
|
||||||
export class Team {}
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
export type ContactType = {
|
|
||||||
name: string
|
|
||||||
avatar: string
|
|
||||||
country: {
|
|
||||||
name: string
|
|
||||||
flag: string
|
|
||||||
}
|
|
||||||
jobTitle: string
|
|
||||||
about: string
|
|
||||||
verified?: boolean
|
|
||||||
rating: number
|
|
||||||
campaigns: number
|
|
||||||
contacts: number
|
|
||||||
engagement: string
|
|
||||||
}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
<p>Webhooks - History</p>
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
import { WebhooksHistory } from './history';
|
|
||||||
describe('WebhooksHistory', () => {});
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
import { Component } from '@angular/core';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-webhooks-history',
|
|
||||||
templateUrl: './history.html',
|
|
||||||
})
|
|
||||||
export class WebhooksHistory {}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
<p>Webhooks - Retry</p>
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
import { WebhooksRetry } from './retry';
|
|
||||||
describe('WebhooksRetry', () => {});
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
import { Component } from '@angular/core';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-webhooks-retry',
|
|
||||||
templateUrl: './retry.html',
|
|
||||||
})
|
|
||||||
export class WebhooksRetry {}
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
import { Injectable } from '@angular/core';
|
|
||||||
|
|
||||||
@Injectable({
|
|
||||||
providedIn: 'root'
|
|
||||||
})
|
|
||||||
export class WebhooksHistoryService {
|
|
||||||
constructor() {}
|
|
||||||
}
|
|
||||||
@ -1,32 +0,0 @@
|
|||||||
import { Injectable, inject } from '@angular/core';
|
|
||||||
import { HttpClient } from '@angular/common/http';
|
|
||||||
import { environment } from '@environments/environment';
|
|
||||||
import { Observable } from 'rxjs';
|
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
|
||||||
export class WebhookRetryService {
|
|
||||||
private http = inject(HttpClient);
|
|
||||||
private apiUrl = `${environment.localServiceTestApiUrl}/webhooks`;
|
|
||||||
|
|
||||||
retryWebhook(webhookId: string): Observable<{ success: boolean }> {
|
|
||||||
return this.http.post<{ success: boolean }>(
|
|
||||||
`${this.apiUrl}/${webhookId}/retry`,
|
|
||||||
{}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
bulkRetryWebhooks(webhookIds: string[]): Observable<{ success: number; failed: number }> {
|
|
||||||
return this.http.post<{ success: number; failed: number }>(
|
|
||||||
`${this.apiUrl}/bulk-retry`,
|
|
||||||
{ webhookIds }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
getRetryConfig(): Observable<any> {
|
|
||||||
return this.http.get(`${this.apiUrl}/retry-config`);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateRetryConfig(config: any): Observable<any> {
|
|
||||||
return this.http.put(`${this.apiUrl}/retry-config`, config);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
import { Injectable } from '@angular/core';
|
|
||||||
|
|
||||||
@Injectable({
|
|
||||||
providedIn: 'root'
|
|
||||||
})
|
|
||||||
export class WebhooksStatusService {
|
|
||||||
constructor() {}
|
|
||||||
}
|
|
||||||
@ -1,45 +0,0 @@
|
|||||||
import { Injectable, inject } from '@angular/core';
|
|
||||||
import { HttpClient } from '@angular/common/http';
|
|
||||||
import { environment } from '@environments/environment';
|
|
||||||
import { Observable } from 'rxjs';
|
|
||||||
|
|
||||||
export interface WebhookEvent {
|
|
||||||
id: string;
|
|
||||||
url: string;
|
|
||||||
eventType: string;
|
|
||||||
payload: any;
|
|
||||||
status: 'SUCCESS' | 'FAILED' | 'PENDING';
|
|
||||||
retryCount: number;
|
|
||||||
createdAt: Date;
|
|
||||||
lastAttempt?: Date;
|
|
||||||
errorMessage?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface WebhookFilter {
|
|
||||||
status?: string;
|
|
||||||
eventType?: string;
|
|
||||||
startDate?: Date;
|
|
||||||
endDate?: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
|
||||||
export class WebhookService {
|
|
||||||
private http = inject(HttpClient);
|
|
||||||
private apiUrl = `${environment.localServiceTestApiUrl}/webhooks`;
|
|
||||||
|
|
||||||
getWebhookHistory(filters?: WebhookFilter): Observable<WebhookEvent[]> {
|
|
||||||
return this.http.post<WebhookEvent[]>(
|
|
||||||
`${this.apiUrl}/history`,
|
|
||||||
filters
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
getWebhookStatus(): Observable<{
|
|
||||||
total: number;
|
|
||||||
success: number;
|
|
||||||
failed: number;
|
|
||||||
pending: number;
|
|
||||||
}> {
|
|
||||||
return this.http.get<any>(`${this.apiUrl}/status`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
<p>Webhooks - Status</p>
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
import { WebhooksStatus } from './status';
|
|
||||||
describe('WebhooksStatus', () => {});
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
import { Component } from '@angular/core';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-webhooks-status',
|
|
||||||
templateUrl: './status.html',
|
|
||||||
})
|
|
||||||
export class WebhooksStatus {}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
<p>Webhooks</p>
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
import { Webhooks } from './webhooks';
|
|
||||||
describe('Webhooks', () => {});
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
import { Component } from '@angular/core';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-webhooks',
|
|
||||||
templateUrl: './webhooks.html',
|
|
||||||
})
|
|
||||||
export class Webhooks {}
|
|
||||||
1
src/assets/images/01.svg
Normal file
1
src/assets/images/01.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 154 36" xmlns:v="https://vecta.io/nano"><path d="M20.9 30.8h25.6V5.2H20.9v25.6zm20.8-15.3h-5.4v-5.4h5.4v5.4zm-15.8-5.4h5.4v10.4h10.4v5.4H25.9V10.1zm37.4-4.9c-2.5 0-5 .8-7.1 2.2s-3.8 3.4-4.7 5.8c-1 2.3-1.2 4.9-.7 7.4s1.7 4.8 3.5 6.6 4.1 3 6.6 3.5 5.1.2 7.4-.7a13.93 13.93 0 0 0 5.8-4.7c1.4-2.1 2.2-4.6 2.2-7.1 0-3.4-1.4-6.7-3.8-9.1-2.5-2.6-5.8-3.9-9.2-3.9zm0 20.7c-1.6 0-3.1-.5-4.4-1.3-1.3-.9-2.3-2.1-2.9-3.5s-.8-3-.4-4.6c.3-1.5 1.1-2.9 2.2-4s2.5-1.9 4-2.2 3.1-.1 4.6.4c1.4.6 2.7 1.6 3.5 2.9.9 1.3 1.3 2.8 1.3 4.4 0 2.1-.8 4.1-2.3 5.6s-3.5 2.3-5.6 2.3zM122 5.2c-2.5 0-5 .8-7.1 2.2s-3.8 3.4-4.7 5.8c-1 2.3-1.2 4.9-.7 7.4s1.7 4.8 3.5 6.6 4.1 3 6.6 3.5 5.1.2 7.4-.7a13.93 13.93 0 0 0 5.8-4.7c1.4-2.1 2.2-4.6 2.2-7.1 0-3.4-1.4-6.7-3.8-9.1-2.5-2.6-5.8-3.9-9.2-3.9zm0 20.7c-1.6 0-3.1-.5-4.4-1.3-1.3-.9-2.3-2.1-2.9-3.5s-.8-3-.4-4.6c.3-1.5 1.1-2.9 2.2-4s2.5-1.9 4-2.2 3.1-.1 4.6.4c1.4.6 2.7 1.6 3.5 2.9.9 1.3 1.3 2.8 1.3 4.4 0 2.1-.8 4.1-2.3 5.6s-3.5 2.3-5.6 2.3zM92.7 5.2c-2.5 0-5 .8-7.1 2.2s-3.8 3.4-4.7 5.8c-1 2.3-1.2 4.9-.7 7.4s1.7 4.8 3.5 6.6 4.1 3 6.6 3.5 5.1.2 7.4-.7a13.93 13.93 0 0 0 5.8-4.7c1.4-2.1 2.2-4.6 2.2-7.1 0-3.4-1.4-6.7-3.8-9.1-2.6-2.6-5.8-3.9-9.2-3.9zm0 20.7c-1.5 0-2.9-.4-4.1-1.2s-2.2-1.8-2.9-3.1-1-2.7-.8-4.2c.1-1.4.6-2.8 1.5-4s2-2.1 3.4-2.7c1.3-.6 2.8-.7 4.2-.5s2.8.8 3.9 1.8c1.1.9 1.9 2.2 2.4 3.5h-7.5v4.9h7.5c-.5 1.6-1.5 2.9-2.9 3.9-1.5 1.1-3.1 1.6-4.7 1.6z" fill="#b4b7c9"/></svg>
|
||||||
|
After Width: | Height: | Size: 1.4 KiB |
@ -4,5 +4,5 @@ export const environment = {
|
|||||||
iamApiUrl: "https://api-user-service.dcb.pixpay.sn/api/v1",
|
iamApiUrl: "https://api-user-service.dcb.pixpay.sn/api/v1",
|
||||||
configApiUrl: 'https://api-merchant-config-service.dcb.pixpay.sn/api/v1',
|
configApiUrl: 'https://api-merchant-config-service.dcb.pixpay.sn/api/v1',
|
||||||
apiCoreUrl: 'https://api-core-service.dcb.pixpay.sn/api/v1',
|
apiCoreUrl: 'https://api-core-service.dcb.pixpay.sn/api/v1',
|
||||||
reportingApiUrl: 'https://api-reporting-service.dcb.pixpay.sn/api/v1/',
|
reportingApiUrl: 'https://api-reporting-service.dcb.pixpay.sn/api/v1/'
|
||||||
};
|
};
|
||||||
|
|||||||
@ -2,7 +2,7 @@ export const environment = {
|
|||||||
production: true,
|
production: true,
|
||||||
localServiceTestApiUrl: "https://backoffice.dcb.pixpay.sn/api/v1",
|
localServiceTestApiUrl: "https://backoffice.dcb.pixpay.sn/api/v1",
|
||||||
iamApiUrl: "https://api-user-service.dcb.pixpay.sn/api/v1",
|
iamApiUrl: "https://api-user-service.dcb.pixpay.sn/api/v1",
|
||||||
configApiUrl: 'https://api-merchant-config-service.dcb.pixpay.sn/api/v1',
|
configApiUrl: "https://api-merchant-config-service.dcb.pixpay.sn/api/v1",
|
||||||
apiCoreUrl: 'https://api-core-service.dcb.pixpay.sn/api/v1',
|
apiCoreUrl: "https://api-core-service.dcb.pixpay.sn/api/v1",
|
||||||
reportingApiUrl: 'https://api-reporting-service.dcb.pixpay.sn/api/v1/',
|
reportingApiUrl: "https://api-reporting-service.dcb.pixpay.sn/api/v1/"
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
export const environment = {
|
export const environment = {
|
||||||
production: false,
|
production: false,
|
||||||
localServiceTestApiUrl: "http://localhost:4200/api/v1",
|
localServiceTestApiUrl: "http://localhost:4200/api/v1",
|
||||||
iamApiUrl: "http://localhost:3000/api/v1",
|
iamApiUrl: "http://localhost:3001/api/v1",
|
||||||
configApiUrl: 'https://api-merchant-config-service.dcb.pixpay.sn/api/v1',
|
configApiUrl: "http://localhost:3000/api/v1",
|
||||||
apiCoreUrl: 'https://api-core-service.dcb.pixpay.sn/api/v1',
|
apiCoreUrl: "https://api-core-service.dcb.pixpay.sn/api/v1",
|
||||||
reportingApiUrl: 'https://api-reporting-service.dcb.pixpay.sn/api/v1/',
|
reportingApiUrl: "https://api-reporting-service.dcb.pixpay.sn/api/v1/"
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user