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",
|
||||
"lodash-es": "^4.17.21",
|
||||
"mermaid": "^11.12.2",
|
||||
"minio": "^8.0.6",
|
||||
"ng-otp-input": "^2.0.9",
|
||||
"ng2-charts": "^8.0.0",
|
||||
"ngx-countup": "^13.2.0",
|
||||
@ -74,6 +75,8 @@
|
||||
"@types/jquery": "^3.5.33",
|
||||
"@types/leaflet": "^1.9.21",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/node": "^25.0.3",
|
||||
"baseline-browser-mapping": "^2.9.11",
|
||||
"jasmine-core": "~5.12.0",
|
||||
"karma": "~6.4.4",
|
||||
"karma-chrome-launcher": "~3.2.0",
|
||||
@ -4697,9 +4700,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "24.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.1.tgz",
|
||||
"integrity": "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==",
|
||||
"version": "25.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz",
|
||||
"integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@ -4746,6 +4749,13 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.1.tgz",
|
||||
@ -4953,6 +4963,27 @@
|
||||
"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": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
@ -4971,9 +5002,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/baseline-browser-mapping": {
|
||||
"version": "2.8.19",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.19.tgz",
|
||||
"integrity": "sha512-zoKGUdu6vb2jd3YOq0nnhEDQVbPcHhco3UImJrv5dSkvxTc2pl2WjOPsjZXDwPDSl5eghIMuY3R6J9NDKF3KcQ==",
|
||||
"version": "2.9.11",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.11.tgz",
|
||||
"integrity": "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"baseline-browser-mapping": "dist/cli.js"
|
||||
@ -5012,6 +5043,29 @@
|
||||
"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": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz",
|
||||
@ -5096,6 +5150,12 @@
|
||||
"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": {
|
||||
"version": "4.27.0",
|
||||
"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_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": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||
@ -5261,11 +5330,28 @@
|
||||
"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": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
@ -5279,7 +5365,6 @@
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
|
||||
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"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": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz",
|
||||
@ -6590,7 +6701,6 @@
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.1",
|
||||
@ -6821,7 +6931,6 @@
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@ -6831,7 +6940,6 @@
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@ -6841,7 +6949,6 @@
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0"
|
||||
@ -7057,6 +7164,24 @@
|
||||
],
|
||||
"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": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||
@ -7087,6 +7212,15 @@
|
||||
"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": {
|
||||
"version": "2.1.0",
|
||||
"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": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
|
||||
@ -7230,7 +7379,6 @@
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
@ -7245,6 +7393,15 @@
|
||||
"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": {
|
||||
"version": "1.0.0-beta.2",
|
||||
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
|
||||
@ -7279,7 +7436,6 @@
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
@ -7304,7 +7460,6 @@
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dunder-proto": "^1.0.1",
|
||||
@ -7360,7 +7515,6 @@
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@ -7392,11 +7546,22 @@
|
||||
"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": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@ -7409,7 +7574,6 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-symbols": "^1.0.3"
|
||||
@ -7425,7 +7589,6 @@
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.2"
|
||||
@ -7700,6 +7863,22 @@
|
||||
"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": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
||||
@ -7713,6 +7892,18 @@
|
||||
"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": {
|
||||
"version": "2.16.1",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
||||
@ -7799,7 +8009,6 @@
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
|
||||
"integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bound": "^1.0.2",
|
||||
@ -7814,6 +8023,21 @@
|
||||
"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": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz",
|
||||
@ -8917,7 +9141,6 @@
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@ -9075,6 +9298,67 @@
|
||||
"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": {
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
|
||||
@ -10182,6 +10466,15 @@
|
||||
"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": {
|
||||
"version": "8.5.6",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
||||
@ -10320,6 +10613,24 @@
|
||||
"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": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/quill/-/quill-2.0.3.tgz",
|
||||
@ -10639,7 +10950,6 @@
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz",
|
||||
"integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bound": "^1.0.2",
|
||||
@ -10680,6 +10990,12 @@
|
||||
"@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": {
|
||||
"version": "7.7.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
|
||||
@ -10732,6 +11048,23 @@
|
||||
"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": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
|
||||
@ -11179,6 +11512,15 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "12.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ssri/-/ssri-12.0.0.tgz",
|
||||
@ -11215,6 +11557,21 @@
|
||||
"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": {
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.1.5.tgz",
|
||||
@ -11230,6 +11587,15 @@
|
||||
"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": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||
@ -11357,6 +11723,18 @@
|
||||
"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": {
|
||||
"version": "4.3.6",
|
||||
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz",
|
||||
@ -11490,6 +11868,29 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz",
|
||||
@ -11743,6 +12144,19 @@
|
||||
"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": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
@ -11985,6 +12399,18 @@
|
||||
"license": "MIT",
|
||||
"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": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
@ -12001,6 +12427,27 @@
|
||||
"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": {
|
||||
"version": "6.2.0",
|
||||
"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": {
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
||||
|
||||
@ -57,6 +57,7 @@
|
||||
"leaflet": "^1.9.4",
|
||||
"lodash-es": "^4.17.21",
|
||||
"mermaid": "^11.12.2",
|
||||
"minio": "^8.0.6",
|
||||
"ng-otp-input": "^2.0.9",
|
||||
"ng2-charts": "^8.0.0",
|
||||
"ngx-countup": "^13.2.0",
|
||||
@ -77,6 +78,8 @@
|
||||
"@types/jquery": "^3.5.33",
|
||||
"@types/leaflet": "^1.9.21",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/node": "^25.0.3",
|
||||
"baseline-browser-mapping": "^2.9.11",
|
||||
"jasmine-core": "~5.12.0",
|
||||
"karma": "~6.4.4",
|
||||
"karma-chrome-launcher": "~3.2.0",
|
||||
|
||||
616
src/app/app.scss
616
src/app/app.scss
@ -646,3 +646,619 @@
|
||||
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 { HttpClient, HttpErrorResponse, HttpHeaders } from '@angular/common/http';
|
||||
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 {
|
||||
@ -325,13 +325,12 @@ export class AuthService {
|
||||
return this.http.get<any>(
|
||||
`${environment.iamApiUrl}/auth/profile`
|
||||
).pipe(
|
||||
tap(apiResponse => {
|
||||
// Déterminer le type d'utilisateur
|
||||
map(apiResponse => {
|
||||
const userType = this.determineUserType(apiResponse);
|
||||
// Mapper vers le modèle User
|
||||
const userProfile = this.mapToUserModel(apiResponse, userType);
|
||||
|
||||
this.userProfile$.next(userProfile);
|
||||
return userProfile;
|
||||
}),
|
||||
catchError(error => {
|
||||
console.error('❌ Erreur chargement profil:', error);
|
||||
|
||||
@ -96,8 +96,7 @@ export class MenuService {
|
||||
{ label: 'Configurations', isTitle: true },
|
||||
{ label: 'Merchant Config', icon: 'lucideStore', url: '/merchant-config' },
|
||||
|
||||
{ label: 'Support & Profil', isTitle: true },
|
||||
{ label: 'Support', icon: 'lucideLifeBuoy', url: '/support' },
|
||||
{ label: 'Profil', isTitle: true },
|
||||
{ label: 'Mon Profil', icon: 'lucideUser', url: '/profile' },
|
||||
|
||||
{ label: 'Informations', isTitle: true },
|
||||
@ -111,8 +110,7 @@ export class MenuService {
|
||||
return [
|
||||
{ label: 'Welcome back!', isHeader: true },
|
||||
{ label: 'Profile', icon: 'tablerUserCircle', url: '/profile' },
|
||||
{ label: 'Account Settings', icon: 'tablerSettings2', url: '/settings' },
|
||||
{ label: 'Support Center', icon: 'tablerHeadset', url: '/support' },
|
||||
{ label: 'Aide', icon: 'tablerHeadset', url: '/help' },
|
||||
{ isDivider: true },
|
||||
{
|
||||
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 -->
|
||||
@if (!isLoading) {
|
||||
<div class="d-flex align-items-center">
|
||||
<img
|
||||
[src]="getUserAvatar()"
|
||||
class="rounded-circle me-2"
|
||||
width="36"
|
||||
height="36"
|
||||
alt="user-image"
|
||||
(error)="onAvatarError($event)"
|
||||
/>
|
||||
@if (user){
|
||||
@if (merchant){
|
||||
@if (merchant.logo && merchant.logo.trim() !== '') {
|
||||
<img
|
||||
[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)"
|
||||
/>
|
||||
}
|
||||
}
|
||||
<div>
|
||||
<h5 class="my-0 fw-semibold">
|
||||
{{ getDisplayName() || 'Utilisateur' }}
|
||||
|
||||
@ -3,23 +3,45 @@ import { NgbCollapseModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { userDropdownItems } from '@layouts/components/data';
|
||||
import { AuthService } from '@/app/core/services/auth.service';
|
||||
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({
|
||||
selector: 'app-user-profile',
|
||||
standalone: true,
|
||||
imports: [NgbCollapseModule],
|
||||
imports: [NgbCollapseModule,CommonModule],
|
||||
templateUrl: './user-profile.component.html',
|
||||
})
|
||||
export class UserProfileComponent implements OnInit, OnDestroy {
|
||||
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>();
|
||||
|
||||
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;
|
||||
hasError = false;
|
||||
hasSuccess = '';
|
||||
currentProfileLoaded = false;
|
||||
|
||||
ngOnInit(): void {
|
||||
@ -45,9 +67,9 @@ export class UserProfileComponent implements OnInit, OnDestroy {
|
||||
// Le profil sera chargé via la subscription
|
||||
} else {
|
||||
console.log('🔐 User not authenticated');
|
||||
this.user = null;
|
||||
this.user = undefined;
|
||||
this.isLoading = false;
|
||||
this.cdr.detectChanges();
|
||||
this.cdRef.detectChanges();
|
||||
}
|
||||
}
|
||||
|
||||
@ -76,22 +98,31 @@ export class UserProfileComponent implements OnInit, OnDestroy {
|
||||
if (profile) {
|
||||
console.log('📥 User profile updated:', profile.username);
|
||||
this.user = profile;
|
||||
|
||||
this.currentUserRole = this.extractUserRole(profile);
|
||||
this.isHubUser = this.checkIfHubUser();
|
||||
|
||||
if (!this.isHubUser) {
|
||||
this.merchanPartnerId = profile?.merchantPartnerId;
|
||||
this.loadMerchantProfile()
|
||||
}
|
||||
|
||||
this.currentProfileLoaded = true;
|
||||
} else {
|
||||
console.log('📭 User profile cleared');
|
||||
this.user = null;
|
||||
this.user = undefined;
|
||||
this.currentProfileLoaded = false;
|
||||
}
|
||||
|
||||
this.isLoading = false;
|
||||
this.hasError = false;
|
||||
this.cdr.detectChanges();
|
||||
this.cdRef.detectChanges();
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('❌ Error in profile subscription:', error);
|
||||
this.hasError = true;
|
||||
this.isLoading = false;
|
||||
this.cdr.detectChanges();
|
||||
this.cdRef.detectChanges();
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -115,10 +146,10 @@ export class UserProfileComponent implements OnInit, OnDestroy {
|
||||
} else {
|
||||
// Si l'utilisateur s'est déconnecté
|
||||
console.log('👋 User logged out');
|
||||
this.user = null;
|
||||
this.user = undefined;
|
||||
this.currentProfileLoaded = false;
|
||||
this.isLoading = false;
|
||||
this.cdr.detectChanges();
|
||||
this.cdRef.detectChanges();
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -130,21 +161,20 @@ export class UserProfileComponent implements OnInit, OnDestroy {
|
||||
loadUserProfile(): void {
|
||||
this.isLoading = true;
|
||||
this.hasError = false;
|
||||
this.cdr.detectChanges();
|
||||
this.cdRef.detectChanges();
|
||||
|
||||
this.authService.loadUserProfile()
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe({
|
||||
next: (profile) => {
|
||||
// Note: le profil sera automatiquement mis à jour via la subscription getUserProfile()
|
||||
this.isLoading = false;
|
||||
this.cdr.detectChanges();
|
||||
this.cdRef.detectChanges();
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('❌ Failed to load user profile:', error);
|
||||
this.hasError = true;
|
||||
this.isLoading = false;
|
||||
this.cdr.detectChanges();
|
||||
this.cdRef.detectChanges();
|
||||
|
||||
// Essayer de rafraîchir le token si erreur 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
|
||||
*/
|
||||
@ -234,19 +437,91 @@ export class UserProfileComponent implements OnInit, OnDestroy {
|
||||
return roleClassMap[this.user.role] || 'badge bg-secondary';
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtient l'URL de l'avatar de l'utilisateur
|
||||
*/
|
||||
getUserAvatar(): string {
|
||||
return `assets/images/users/user-2.jpg`;
|
||||
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 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 {
|
||||
const img = event.target as HTMLImageElement;
|
||||
img.src = 'assets/images/users/user-2.jpg';
|
||||
img.onerror = null;
|
||||
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.';
|
||||
}
|
||||
}
|
||||
@ -4,12 +4,41 @@
|
||||
ngbDropdownToggle
|
||||
class="topbar-link dropdown-toggle drop-arrow-none px-2"
|
||||
>
|
||||
<img
|
||||
src="assets/images/users/user-2.jpg"
|
||||
width="32"
|
||||
class="rounded-circle d-flex"
|
||||
alt="user-image"
|
||||
/>
|
||||
@if (user){
|
||||
@if (merchant){
|
||||
@if (merchant.logo && merchant.logo.trim() !== '') {
|
||||
<img
|
||||
[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>
|
||||
<div ngbDropdownMenu class="dropdown-menu dropdown-menu-end">
|
||||
@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 { MenuService } from '@core/services/menu.service'
|
||||
import {
|
||||
@ -9,7 +9,12 @@ import {
|
||||
import { RouterLink } from '@angular/router'
|
||||
import { NgIcon } from '@ng-icons/core'
|
||||
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({
|
||||
selector: 'app-user-profile-topbar',
|
||||
@ -19,17 +24,46 @@ import { Subscription } from 'rxjs'
|
||||
NgbDropdownToggle,
|
||||
RouterLink,
|
||||
NgIcon,
|
||||
CommonModule,
|
||||
],
|
||||
templateUrl: './user-profile.html',
|
||||
})
|
||||
export class UserProfile implements OnInit, OnDestroy {
|
||||
private authService = inject(AuthService)
|
||||
private menuService = inject(MenuService)
|
||||
private subscription?: Subscription
|
||||
private authService = inject(AuthService);
|
||||
private merchantConfigService = inject(MerchantConfigService);
|
||||
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[] = []
|
||||
merchanPartnerId: string | undefined
|
||||
|
||||
ngOnInit() {
|
||||
this.loadUserProfile()
|
||||
this.loadDropdownItems()
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
// ==================== 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() {
|
||||
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(
|
||||
private accessService: DashboardAccessService,
|
||||
private cdr: ChangeDetectorRef
|
||||
private cdRef: ChangeDetectorRef
|
||||
) {
|
||||
Chart.register(...registerables);
|
||||
}
|
||||
@ -326,12 +326,14 @@ export class DcbReportingDashboard implements OnInit, OnDestroy, AfterViewInit {
|
||||
console.log('✅ Dashboard: waitForReady() a émis - Initialisation...');
|
||||
this.dashboardInitialized = true;
|
||||
this.initializeDashboard();
|
||||
this.cdRef.detectChanges();
|
||||
},
|
||||
error: (err) => {
|
||||
console.error('❌ Dashboard: Erreur dans waitForReady():', err);
|
||||
// Gérer l'erreur - peut-être rediriger vers une page d'erreur
|
||||
this.addAlert('danger', 'Erreur d\'initialisation',
|
||||
'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({
|
||||
next: (merchants) => {
|
||||
this.allowedMerchants = merchants;
|
||||
this.cdr.detectChanges();
|
||||
this.cdRef.detectChanges();
|
||||
},
|
||||
error: (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');
|
||||
this.loading.globalData = false;
|
||||
this.calculateStats();
|
||||
this.cdr.detectChanges();
|
||||
this.cdRef.detectChanges();
|
||||
setTimeout(() => this.updateAllCharts(), 100);
|
||||
},
|
||||
error: (err) => {
|
||||
console.error('Erreur lors du chargement des données globales:', err);
|
||||
this.loading.globalData = false;
|
||||
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`);
|
||||
this.loading.merchantData = false;
|
||||
this.calculateStats();
|
||||
this.cdr.detectChanges();
|
||||
this.cdRef.detectChanges();
|
||||
setTimeout(() => this.updateAllCharts(), 100);
|
||||
},
|
||||
error: (err) => {
|
||||
@ -493,7 +495,7 @@ export class DcbReportingDashboard implements OnInit, OnDestroy, AfterViewInit {
|
||||
this.loading.merchantData = false;
|
||||
this.addAlert('danger', 'Erreur de chargement',
|
||||
`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;
|
||||
|
||||
this.loading.chart = false;
|
||||
this.cdr.detectChanges();
|
||||
this.cdRef.detectChanges();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la création du graphique principal:', error);
|
||||
this.loading.chart = false;
|
||||
this.cdr.detectChanges();
|
||||
this.cdRef.detectChanges();
|
||||
}
|
||||
}
|
||||
|
||||
@ -1109,13 +1111,13 @@ export class DcbReportingDashboard implements OnInit, OnDestroy, AfterViewInit {
|
||||
this.updateOverallHealth();
|
||||
this.generateHealthAlerts();
|
||||
this.loading.healthCheck = false;
|
||||
this.cdr.detectChanges();
|
||||
this.cdRef.detectChanges();
|
||||
}),
|
||||
catchError(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.loading.healthCheck = false;
|
||||
this.cdr.detectChanges();
|
||||
this.cdRef.detectChanges();
|
||||
return of(null);
|
||||
})
|
||||
).subscribe()
|
||||
@ -1274,7 +1276,7 @@ export class DcbReportingDashboard implements OnInit, OnDestroy, AfterViewInit {
|
||||
this.metricDropdown.close();
|
||||
}
|
||||
|
||||
this.cdr.detectChanges();
|
||||
this.cdRef.detectChanges();
|
||||
|
||||
setTimeout(() => {
|
||||
this.updateMainChart();
|
||||
@ -1284,7 +1286,7 @@ export class DcbReportingDashboard implements OnInit, OnDestroy, AfterViewInit {
|
||||
changePeriod(period: ReportPeriod): void {
|
||||
this.dataSelection.period = period;
|
||||
|
||||
this.cdr.detectChanges();
|
||||
this.cdRef.detectChanges();
|
||||
|
||||
setTimeout(() => {
|
||||
this.updateMainChart();
|
||||
@ -1294,7 +1296,7 @@ export class DcbReportingDashboard implements OnInit, OnDestroy, AfterViewInit {
|
||||
changeChartType(type: ChartType): void {
|
||||
this.dataSelection.chartType = type;
|
||||
|
||||
this.cdr.detectChanges();
|
||||
this.cdRef.detectChanges();
|
||||
|
||||
setTimeout(() => {
|
||||
this.updateMainChart();
|
||||
@ -1302,7 +1304,7 @@ export class DcbReportingDashboard implements OnInit, OnDestroy, AfterViewInit {
|
||||
}
|
||||
|
||||
refreshChartData(): void {
|
||||
this.cdr.detectChanges();
|
||||
this.cdRef.detectChanges();
|
||||
setTimeout(() => {
|
||||
this.updateAllCharts();
|
||||
}, 50);
|
||||
|
||||
@ -68,13 +68,26 @@ export interface ReportParams {
|
||||
endDate?: string;
|
||||
merchantPartnerId?: number;
|
||||
}
|
||||
|
||||
export interface HealthCheckStatus {
|
||||
service: string;
|
||||
url: string;
|
||||
status: 'UP' | 'DOWN';
|
||||
statusCode: number;
|
||||
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
|
||||
|
||||
@ -150,7 +150,7 @@ export class DashboardAccessService {
|
||||
if (access.isHubUser) {
|
||||
return this.merchantService.getAllMerchants().pipe(
|
||||
map(merchants => {
|
||||
const available: AllowedMerchant[] = merchants.map(m => ({
|
||||
const available: AllowedMerchant[] = merchants.items.map(m => ({
|
||||
id: m.id,
|
||||
name: m.name
|
||||
}));
|
||||
|
||||
@ -8,6 +8,7 @@ import {
|
||||
SubscriptionReport,
|
||||
SyncResponse,
|
||||
HealthCheckStatus,
|
||||
HealthCheckResponse,
|
||||
ChartDataNormalized
|
||||
} from '../models/dcb-reporting.models';
|
||||
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(
|
||||
@ -316,6 +317,8 @@ export class ReportService {
|
||||
timeout(this.DEFAULT_TIMEOUT),
|
||||
map((resp: HttpResponse<any>) => {
|
||||
const finalResponseTime = Date.now() - startTime;
|
||||
const body: any = resp.body;
|
||||
|
||||
return {
|
||||
service,
|
||||
url,
|
||||
@ -323,6 +326,7 @@ export class ReportService {
|
||||
statusCode: resp.status,
|
||||
checkedAt: new Date().toISOString(),
|
||||
responseTime: `${finalResponseTime}ms`,
|
||||
uptime: body?.uptime,
|
||||
note: 'Used GET fallback'
|
||||
};
|
||||
}),
|
||||
@ -397,16 +401,17 @@ export class ReportService {
|
||||
/**
|
||||
* Health check global de toutes les APIs
|
||||
* Scanne chaque URL d'API directement
|
||||
*/
|
||||
*/
|
||||
private buildHealthUrl(baseUrl: string): string {
|
||||
return `${baseUrl.replace(/\/$/, '')}/health`;
|
||||
}
|
||||
|
||||
globalHealthCheck(): Observable<HealthCheckStatus[]> {
|
||||
const healthChecks: Observable<HealthCheckStatus>[] = [];
|
||||
|
||||
// Vérifiez chaque service avec sa racine
|
||||
Object.entries(this.apiEndpoints).forEach(([service, url]) => {
|
||||
healthChecks.push(this.checkApiAvailability(service, url));
|
||||
});
|
||||
|
||||
return forkJoin(healthChecks);
|
||||
return forkJoin(
|
||||
Object.entries(this.apiEndpoints).map(([service, url]) =>
|
||||
this.checkApiAvailability(service, this.buildHealthUrl(url))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -438,10 +443,7 @@ export class ReportService {
|
||||
/**
|
||||
* Health check détaillé avec métriques
|
||||
*/
|
||||
detailedHealthCheck(): Observable<{
|
||||
summary: { total: number; up: number; down: number; timestamp: string };
|
||||
details: HealthCheckStatus[];
|
||||
}> {
|
||||
detailedHealthCheck(): Observable<HealthCheckResponse> {
|
||||
return this.globalHealthCheck().pipe(
|
||||
map(results => {
|
||||
const adjustedResults = results.map(result => ({
|
||||
@ -462,7 +464,24 @@ export class ReportService {
|
||||
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 { RoleManagementService } from '@core/services/hub-users-roles-management.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 { HubUsersList } from './hub-users-list/hub-users-list';
|
||||
import { HubUserProfile } from './hub-users-profile/hub-users-profile';
|
||||
@ -39,7 +38,6 @@ export class HubUsersManagement implements OnInit, OnDestroy {
|
||||
private modalService = inject(NgbModal);
|
||||
private authService = inject(AuthService);
|
||||
private hubUsersService = inject(HubUsersService);
|
||||
private merchantSyncService = inject(MerchantSyncService);
|
||||
protected roleService = inject(RoleManagementService);
|
||||
private cdRef = inject(ChangeDetectorRef);
|
||||
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
|
||||
*/
|
||||
|
||||
@ -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 { NgIcon } from '@ng-icons/core';
|
||||
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 { RoleManagementService } from '@core/services/hub-users-roles-management.service';
|
||||
@ -346,23 +346,49 @@ export class MerchantUsersManagement implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
private loadAllMerchants(): void {
|
||||
if (this.isMerchantUser) {
|
||||
console.log('⚠️ User is not a Hub user, merchant list not displayed');
|
||||
return;
|
||||
}
|
||||
|
||||
this.loadingMerchantPartners = true;
|
||||
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$))
|
||||
.subscribe({
|
||||
next: (merchants) => {
|
||||
this.merchantPartners = merchants;
|
||||
this.loadingMerchantPartners = false;
|
||||
console.log('✅ All merchants loaded for Hub Admin:', merchants.length);
|
||||
next: response => {
|
||||
const items = Array.isArray(response.items) ? response.items : [];
|
||||
this.merchantPartners.push(...items);
|
||||
|
||||
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) => {
|
||||
console.error('❌ Error loading all merchants:', error);
|
||||
error: err => {
|
||||
console.error('❌ Error loading merchants:', err);
|
||||
this.merchantPartnersError = 'Erreur lors du chargement des merchants';
|
||||
this.loadingMerchantPartners = false;
|
||||
}
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
loadNextPage();
|
||||
}
|
||||
|
||||
getCurrentUserMerchantName(): string {
|
||||
@ -571,15 +597,19 @@ export class MerchantUsersManagement implements OnInit, OnDestroy {
|
||||
enabled: this.newUser.enabled,
|
||||
emailVerified: this.newUser.emailVerified,
|
||||
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)
|
||||
.pipe(
|
||||
switchMap((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) {
|
||||
const merchantPartnerId = Number(this.newUser.merchantPartnerId);
|
||||
|
||||
@ -589,38 +619,94 @@ export class MerchantUsersManagement implements OnInit, OnDestroy {
|
||||
merchantPartnerId: merchantPartnerId
|
||||
};
|
||||
|
||||
console.log('📤 Adding user to merchant config:', addUserDto);
|
||||
return this.merchantConfigService.addUserToMerchant(addUserDto).pipe(
|
||||
map((merchantConfigUser) => {
|
||||
return {
|
||||
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$)
|
||||
)
|
||||
.subscribe({
|
||||
next: (result) => {
|
||||
console.log('✅ Complete user creation successful:', result);
|
||||
this.creatingUser = false;
|
||||
this.modalService.dismissAll();
|
||||
this.refreshUsersList();
|
||||
this.cdRef.detectChanges();
|
||||
if (result.success) {
|
||||
console.log('✅ Complete user creation successful:', result);
|
||||
this.creatingUser = false;
|
||||
this.modalService.dismissAll();
|
||||
this.refreshUsersList();
|
||||
this.cdRef.detectChanges();
|
||||
}
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('❌ Error in user creation process:', error);
|
||||
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é
|
||||
private findMerchantPartnerIdForCurrentUser(): void {
|
||||
const currentUserId = this.authService.getCurrentUserId();
|
||||
@ -791,16 +877,28 @@ export class MerchantUsersManagement implements OnInit, OnDestroy {
|
||||
if (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) {
|
||||
return 'Un utilisateur avec ce nom d\'utilisateur ou email existe déjà.';
|
||||
}
|
||||
|
||||
if (error.status === 403) {
|
||||
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 {
|
||||
|
||||
@ -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>
|
||||
<td>
|
||||
<div class="d-flex align-items-center">
|
||||
@if (merchant.logo) {
|
||||
<img
|
||||
[src]="merchant.logo"
|
||||
alt="Logo {{ merchant.name }}"
|
||||
class="avatar-sm rounded-circle me-2"
|
||||
onerror="this.style.display='none'"
|
||||
>
|
||||
}
|
||||
<div class="avatar-sm bg-primary bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center me-2">
|
||||
<ng-icon name="lucideStore" class="text-primary fs-12"></ng-icon>
|
||||
<!-- ==================== LOGO AVEC ICÔNE ==================== -->
|
||||
<div class="d-flex align-items-center me-3">
|
||||
<!-- Icône fixe à gauche -->
|
||||
<div class="avatar-sm bg-primary bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center me-2">
|
||||
<ng-icon name="lucideStore" class="text-primary fs-12"></ng-icon>
|
||||
</div>
|
||||
|
||||
<!-- Logo du marchand -->
|
||||
<div>
|
||||
@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>
|
||||
<strong class="d-block">{{ merchant.name }}</strong>
|
||||
<small class="text-muted">{{ merchant.adresse }}</small>
|
||||
|
||||
<!-- Informations du marchand -->
|
||||
<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>
|
||||
</td>
|
||||
@ -314,7 +334,7 @@
|
||||
</table>
|
||||
|
||||
<!-- Pagination -->
|
||||
@if (totalPages > 1) {
|
||||
@if (totalPages >= 1) {
|
||||
<div class="d-flex justify-content-between align-items-center mt-3">
|
||||
<div class="text-muted">
|
||||
Affichage de {{ getStartIndex() }}
|
||||
|
||||
@ -4,7 +4,7 @@ import { FormsModule } from '@angular/forms';
|
||||
import { NgIcon } from '@ng-icons/core';
|
||||
import { NgbPaginationModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { Observable, Subject, of } from 'rxjs';
|
||||
import { catchError, takeUntil } from 'rxjs/operators';
|
||||
import { catchError, map, takeUntil, tap } from 'rxjs/operators';
|
||||
|
||||
import {
|
||||
Merchant,
|
||||
@ -20,6 +20,8 @@ import { MerchantConfigService } from '../merchant-config.service';
|
||||
import { RoleManagementService } from '@core/services/hub-users-roles-management.service';
|
||||
import { AuthService } from '@core/services/auth.service';
|
||||
import { UiCard } from '@app/components/ui-card';
|
||||
import { DomSanitizer } from '@angular/platform-browser';
|
||||
import { MinioService } from '@core/services/minio.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-merchant-config-list',
|
||||
@ -40,6 +42,13 @@ export class MerchantConfigsList implements OnInit, OnDestroy {
|
||||
private cdRef = inject(ChangeDetectorRef);
|
||||
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
|
||||
readonly ConfigType = ConfigType;
|
||||
readonly Operator = Operator;
|
||||
@ -103,11 +112,6 @@ export class MerchantConfigsList implements OnInit, OnDestroy {
|
||||
this.loadCurrentUserPermissions();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
private loadCurrentUserPermissions() {
|
||||
this.authService.getUserProfile()
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
@ -150,7 +154,7 @@ export class MerchantConfigsList implements OnInit, OnDestroy {
|
||||
];
|
||||
}
|
||||
|
||||
loadMerchants() {
|
||||
loadMerchants(): void {
|
||||
if (!this.isHubUser) {
|
||||
console.log('⚠️ User is not a Hub user, merchant list not displayed');
|
||||
return;
|
||||
@ -159,15 +163,14 @@ export class MerchantConfigsList implements OnInit, OnDestroy {
|
||||
this.loading = true;
|
||||
this.error = '';
|
||||
|
||||
this.merchantConfigService.getMerchants(
|
||||
this.currentPage,
|
||||
this.itemsPerPage,
|
||||
this.buildSearchParams()
|
||||
)
|
||||
const params = this.buildSearchParams();
|
||||
const skip = (this.currentPage - 1) * this.itemsPerPage;
|
||||
|
||||
this.merchantConfigService.getAllMerchants(this.currentPage, this.itemsPerPage, params)
|
||||
.pipe(
|
||||
takeUntil(this.destroy$),
|
||||
catchError(error => {
|
||||
console.error('Error loading merchants:', error);
|
||||
console.error('❌ Error loading merchants:', error);
|
||||
this.error = 'Erreur lors du chargement des marchands';
|
||||
return of({
|
||||
items: [],
|
||||
@ -178,36 +181,196 @@ export class MerchantConfigsList implements OnInit, OnDestroy {
|
||||
} as PaginatedResponse<Merchant>);
|
||||
})
|
||||
)
|
||||
.subscribe({
|
||||
next: (response) => {
|
||||
console.log('📊 Pagination response:', {
|
||||
page: response.page,
|
||||
total: response.total,
|
||||
totalPages: response.totalPages,
|
||||
itemsCount: response.items?.length,
|
||||
limit: response.limit
|
||||
});
|
||||
.subscribe(response => {
|
||||
this.allMerchants = response.items || [];
|
||||
this.displayedMerchants = response.items || [];
|
||||
this.totalItems = response.total || 0;
|
||||
this.totalPages = response.totalPages || Math.ceil((response.total || 0) / this.itemsPerPage);
|
||||
|
||||
this.allMerchants = response.items || [];
|
||||
this.displayedMerchants = response.items || [];
|
||||
this.totalItems = response.total || 0;
|
||||
this.totalPages = response.totalPages || 0;
|
||||
this.loading = false;
|
||||
this.cdRef.detectChanges();
|
||||
|
||||
this.loading = false;
|
||||
this.cdRef.detectChanges();
|
||||
},
|
||||
error: () => {
|
||||
this.error = 'Erreur lors du chargement des marchands';
|
||||
this.loading = false;
|
||||
this.allMerchants = [];
|
||||
this.displayedMerchants = [];
|
||||
this.totalItems = 0;
|
||||
this.totalPages = 0;
|
||||
this.cdRef.detectChanges();
|
||||
}
|
||||
console.log('📊 Pagination response:', {
|
||||
page: response.page,
|
||||
total: response.total,
|
||||
totalPages: this.totalPages,
|
||||
itemsCount: response.items?.length,
|
||||
limit: response.limit
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== 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 {
|
||||
const params: SearchMerchantsParams = {};
|
||||
|
||||
@ -370,4 +533,15 @@ export class MerchantConfigsList implements OnInit, OnDestroy {
|
||||
shouldDisplayMerchantList(): boolean {
|
||||
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>
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb mb-0">
|
||||
<li class="breadcrumb-item">
|
||||
<a href="javascript:void(0)" (click)="goBack()" class="text-decoration-none cursor-pointer">
|
||||
Marchands
|
||||
</a>
|
||||
</li>
|
||||
@if(isHubUser){
|
||||
<li class="breadcrumb-item">
|
||||
<a href="javascript:void(0)" (click)="goBack()" class="text-decoration-none cursor-pointer">
|
||||
Marchands
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
<li class="breadcrumb-item active" aria-current="page">
|
||||
{{ merchant?.name || 'Chargement...' }}
|
||||
</li>
|
||||
@ -29,15 +31,16 @@
|
||||
<ng-icon name="lucideRefreshCw" class="me-1" [class.spin]="loading"></ng-icon>
|
||||
Actualiser
|
||||
</button>
|
||||
|
||||
<!-- Bouton retour -->
|
||||
<button
|
||||
class="btn btn-outline-secondary"
|
||||
(click)="goBack()"
|
||||
>
|
||||
<ng-icon name="lucideArrowLeft" class="me-1"></ng-icon>
|
||||
Retour
|
||||
</button>
|
||||
@if(isHubUser){
|
||||
<!-- Bouton retour -->
|
||||
<button
|
||||
class="btn btn-outline-secondary"
|
||||
(click)="goBack()"
|
||||
>
|
||||
<ng-icon name="lucideArrowLeft" class="me-1"></ng-icon>
|
||||
Retour
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -106,61 +109,128 @@
|
||||
<ng-template ngbNavContent>
|
||||
<!-- Vue d'ensemble -->
|
||||
<div class="p-3">
|
||||
<!-- En-tête du profil -->
|
||||
<div class="profile-section">
|
||||
<div class="profile-header">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-md-8">
|
||||
<h2 class="mb-2">{{ merchant.name }}</h2>
|
||||
<p class="mb-0 opacity-75">{{ merchant.description || 'Aucune description' }}</p>
|
||||
</div>
|
||||
<div class="col-md-4 text-md-end">
|
||||
@if (canEditMerchant()) {
|
||||
<button
|
||||
class="btn btn-light"
|
||||
(click)="editMerchant(merchant)"
|
||||
>
|
||||
<ng-icon name="lucideEdit" class="me-1"></ng-icon>
|
||||
Modifier le profil
|
||||
</button>
|
||||
}
|
||||
<!-- En-tête du profil avec logo -->
|
||||
<div class="profile-section">
|
||||
<div class="profile-header">
|
||||
<div class="row align-items-center">
|
||||
<!-- Logo et informations -->
|
||||
<div class="col-md-8">
|
||||
<div class="d-flex align-items-start">
|
||||
|
||||
<!-- ==================== LOGO DU MARCHAND ==================== -->
|
||||
<div class="merchant-logo-container me-4">
|
||||
<div class="logo-display">
|
||||
@if (merchant.logo && merchant.logo.trim() !== '') {
|
||||
<img
|
||||
[src]="getMerchantLogoUrl(merchant.id, merchant.logo, merchant.name) | async"
|
||||
[alt]="merchant.name + ' logo'"
|
||||
class="merchant-logo"
|
||||
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>
|
||||
|
||||
<!-- 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-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-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>
|
||||
<!-- Actions -->
|
||||
<div class="col-md-4 text-md-end">
|
||||
@if (canEditMerchant()) {
|
||||
<button
|
||||
class="btn btn-light"
|
||||
(click)="editMerchant(merchant)"
|
||||
>
|
||||
<ng-icon name="lucideEdit" class="me-1"></ng-icon>
|
||||
Modifier le profil
|
||||
</button>
|
||||
}
|
||||
</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 -->
|
||||
<div class="row g-4">
|
||||
<!-- Configurations récentes -->
|
||||
|
||||
@ -3,7 +3,7 @@ import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { NgIcon } from '@ng-icons/core';
|
||||
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 {
|
||||
Merchant,
|
||||
@ -21,7 +21,9 @@ import { MerchantConfigService } from '../merchant-config.service';
|
||||
import { MerchantDataAdapter } from '../merchant-data-adapter.service';
|
||||
import { RoleManagementService } from '@core/services/hub-users-roles-management.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({
|
||||
selector: 'app-merchant-config-view',
|
||||
@ -180,6 +182,19 @@ export class MerchantConfigView implements OnInit, OnDestroy {
|
||||
private modalService = inject(NgbModal);
|
||||
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 Operator = Operator;
|
||||
readonly MerchantUtils = MerchantUtils;
|
||||
@ -203,12 +218,14 @@ export class MerchantConfigView implements OnInit, OnDestroy {
|
||||
// Gestion des permissions
|
||||
currentUserRole: UserRole | null = null;
|
||||
currentMerchantPartnerId: string = '';
|
||||
// Déterminer le type d'utilisateur
|
||||
isMerchantUser = false;
|
||||
isHubUser = false;
|
||||
|
||||
// Édition des configurations
|
||||
editingConfigId: number | null = null;
|
||||
editedConfig: UpdateMerchantConfigDto = {};
|
||||
configToDelete: MerchantConfig | null = null;
|
||||
private deleteModalRef: any = null;
|
||||
|
||||
// Affichage des valeurs sensibles
|
||||
showSensitiveValues: { [configId: number]: boolean } = {};
|
||||
@ -222,10 +239,6 @@ export class MerchantConfigView implements OnInit, OnDestroy {
|
||||
page = 1;
|
||||
pageSize = 5;
|
||||
|
||||
// Cache
|
||||
private merchantCache: { data: Merchant, timestamp: number } | null = null;
|
||||
private readonly CACHE_TTL = 2 * 60 * 1000; // 2 minutes
|
||||
|
||||
ngOnInit() {
|
||||
if (this.merchantId) {
|
||||
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
|
||||
*/
|
||||
@ -290,6 +473,18 @@ export class MerchantConfigView implements OnInit, OnDestroy {
|
||||
.subscribe({
|
||||
next: (profile) => {
|
||||
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();
|
||||
},
|
||||
error: (error) => {
|
||||
|
||||
@ -177,15 +177,91 @@
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Logo URL</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
placeholder="https://exemple.com/logo.png"
|
||||
[(ngModel)]="newMerchant.logo"
|
||||
name="logo"
|
||||
[disabled]="creatingMerchant"
|
||||
>
|
||||
<div class="form-group logo-upload-section">
|
||||
<label class="form-label">
|
||||
<ng-icon name="lucideImage" class="me-1"></ng-icon>
|
||||
Logo du marchand (optionnel)
|
||||
</label>
|
||||
|
||||
<div class="logo-upload-container">
|
||||
<!-- 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 class="col-12">
|
||||
@ -517,15 +593,102 @@
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Logo URL</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
[(ngModel)]="selectedMerchantForEdit.logo"
|
||||
name="logo"
|
||||
[disabled]="updatingMerchant"
|
||||
placeholder="https://exemple.com/logo.png"
|
||||
>
|
||||
<div class="form-group logo-edit-section">
|
||||
<label class="form-label">
|
||||
<ng-icon name="lucideImage" class="me-1"></ng-icon>
|
||||
Logo du marchand
|
||||
</label>
|
||||
|
||||
<div class="logo-edit-container">
|
||||
<!-- 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 class="col-12">
|
||||
@ -567,104 +730,160 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CONFIGURATIONS TECHNIQUES -->
|
||||
<div class="row g-3 mb-4">
|
||||
<!-- LISTE DES CONFIGURATIONS (READONLY) -->
|
||||
<div class="row g-3">
|
||||
<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">
|
||||
<ng-icon name="lucideSettings" class="me-2"></ng-icon>
|
||||
Configurations Techniques
|
||||
Configurations
|
||||
</h6>
|
||||
<button
|
||||
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>
|
||||
<span class="badge bg-secondary">{{ selectedMerchantForEdit.configs.length || 0 }} config(s)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (!selectedMerchantForEdit.configs || selectedMerchantForEdit.configs.length === 0) {
|
||||
<div class="col-12">
|
||||
<div class="alert alert-warning">
|
||||
<ng-icon name="lucideAlertCircle" class="me-2"></ng-icon>
|
||||
Au moins une configuration est requise
|
||||
<div class="alert alert-info">
|
||||
<ng-icon name="lucideInfo" class="me-2"></ng-icon>
|
||||
Aucune configuration disponible
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Liste des configurations -->
|
||||
@for (config of selectedMerchantForEdit.configs; track trackByConfigId($index, config); let i = $index) {
|
||||
<!-- Liste des configurations en mode lecture -->
|
||||
@for (config of selectedMerchantForEdit.configs; track config.id || $index; let i = $index) {
|
||||
<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="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>
|
||||
</div>
|
||||
@if (selectedMerchantForEdit.configs.length > 1) {
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-outline-danger"
|
||||
(click)="removeConfigInEdit(i)"
|
||||
[disabled]="updatingMerchant"
|
||||
>
|
||||
<ng-icon name="lucideTrash2" class="me-1"></ng-icon>
|
||||
Supprimer
|
||||
</button>
|
||||
@if (config.name.includes('SECRET') || config.name.includes('KEY') || config.value.includes('password')) {
|
||||
<span class="badge bg-warning text-dark">
|
||||
<ng-icon name="lucideShield" class="me-1"></ng-icon>
|
||||
Sensible
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<!-- Type de configuration -->
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Type <span class="text-danger">*</span></label>
|
||||
<select
|
||||
class="form-select"
|
||||
[(ngModel)]="config.name"
|
||||
[name]="'editConfigType_' + i"
|
||||
required
|
||||
[disabled]="updatingMerchant"
|
||||
>
|
||||
<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 class="mb-2">
|
||||
<small class="text-muted d-block">Type</small>
|
||||
<div class="d-flex align-items-center">
|
||||
<ng-icon name="lucideSettings" class="me-2 text-muted"></ng-icon>
|
||||
<span class="fw-medium">
|
||||
{{ config.name || 'Non spécifié' }}
|
||||
</span>
|
||||
</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>
|
||||
@ -673,114 +892,6 @@
|
||||
</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">
|
||||
<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 { MerchantUsersService } from '../hub-users-management/merchant-users.service';
|
||||
import { User, UserType } from '@core/models/dcb-bo-hub-user.model';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
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
|
||||
const paramsChanged = !this.areParamsEqual(params, this.cacheParams);
|
||||
/**
|
||||
* Récupère tous les merchants (optionnel: avec recherche)
|
||||
*/
|
||||
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()
|
||||
.set('take', take.toString())
|
||||
.set('skip', skip.toString()); // Si votre API supporte skip
|
||||
.set('skip', skip.toString())
|
||||
.set('take', limit.toString());
|
||||
|
||||
if (params?.query) {
|
||||
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, {
|
||||
params: httpParams
|
||||
}).pipe(
|
||||
timeout(this.REQUEST_TIMEOUT),
|
||||
map(apiMerchants =>
|
||||
apiMerchants.map(merchant =>
|
||||
this.dataAdapter.convertApiMerchantToFrontend(merchant)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
const total: number = typeof response?.total === 'number' ? response.total : merchants.length;
|
||||
|
||||
private mergeMerchants(existing: Merchant[], newOnes: Merchant[]): Merchant[] {
|
||||
const existingIds = new Set(existing.map(m => m.id));
|
||||
const uniqueNewOnes = newOnes.filter(m => !existingIds.has(m.id));
|
||||
return [...existing, ...uniqueNewOnes];
|
||||
}
|
||||
|
||||
private applyPagination(merchants: Merchant[], page: number, limit: number): PaginatedResponse<Merchant> {
|
||||
const total = merchants.length;
|
||||
const totalPages = Math.ceil(total / limit);
|
||||
|
||||
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))
|
||||
);
|
||||
return {
|
||||
items: merchants,
|
||||
total,
|
||||
page,
|
||||
limit,
|
||||
totalPages: Math.ceil(total / limit)
|
||||
};
|
||||
}),
|
||||
catchError(error => this.handleError('getAllMerchants', error))
|
||||
);
|
||||
}
|
||||
|
||||
getMerchantById(userId: number): Observable<Merchant> {
|
||||
//const numericId = this.convertIdToNumber(id);
|
||||
|
||||
console.log(`📥 Loading merchant ${userId}`);
|
||||
|
||||
return this.http.get<ApiMerchant>(`${this.baseApiUrl}/${userId}`).pipe(
|
||||
@ -275,8 +116,6 @@ export class MerchantConfigService {
|
||||
}
|
||||
|
||||
updateMerchant(id: number, updateMerchantDto: UpdateMerchantDto): Observable<Merchant> {
|
||||
//const numericId = this.convertIdToNumber(id);
|
||||
|
||||
const apiDto = this.dataAdapter.convertUpdateMerchantToApi(updateMerchantDto);
|
||||
|
||||
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 { NgIcon } from '@ng-icons/core';
|
||||
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 { RoleManagementService } from '@core/services/hub-users-roles-management.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 { MerchantConfigsList } from './merchant-config-list/merchant-config-list';
|
||||
import { MerchantConfigView } from './merchant-config-view/merchant-config-view';
|
||||
|
||||
import { MinioService } from '@core/services/minio.service';
|
||||
|
||||
import {
|
||||
CreateMerchantDto,
|
||||
MerchantUtils,
|
||||
@ -23,7 +25,7 @@ import {
|
||||
MerchantConfig,
|
||||
TechnicalContact
|
||||
} 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';
|
||||
|
||||
@ -47,12 +49,20 @@ export class MerchantConfigManagement implements OnInit, OnDestroy {
|
||||
private modalService = inject(NgbModal);
|
||||
private authService = inject(AuthService);
|
||||
private merchantConfigService = inject(MerchantConfigService);
|
||||
private merchantSyncService = inject(MerchantSyncService);
|
||||
private dataAdapter = inject(MerchantDataAdapter);
|
||||
protected roleService = inject(RoleManagementService);
|
||||
private cdRef = inject(ChangeDetectorRef);
|
||||
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
|
||||
readonly UserRole = UserRole;
|
||||
readonly ConfigType = ConfigType;
|
||||
@ -63,8 +73,22 @@ export class MerchantConfigManagement implements OnInit, OnDestroy {
|
||||
pageSubtitle: string = 'Administrez les marchands et leurs configurations techniques';
|
||||
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
|
||||
activeTab: 'list' | 'merchant-profile' = 'list';
|
||||
activeTab: 'list' | 'merchant-profile' = 'merchant-profile';
|
||||
selectedMerchantId: number | null = null;
|
||||
selectedConfigId: number | null = null;
|
||||
selectedUserId: string | null = null;
|
||||
@ -232,8 +256,6 @@ export class MerchantConfigManagement implements OnInit, OnDestroy {
|
||||
}
|
||||
} else {
|
||||
console.warn('No merchant found for current user');
|
||||
// Si aucun marchand trouvé, revenir à la liste
|
||||
this.activeTab = 'list';
|
||||
}
|
||||
|
||||
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
|
||||
*/
|
||||
@ -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 ====================
|
||||
|
||||
/**
|
||||
@ -364,8 +346,8 @@ export class MerchantConfigManagement implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
// Conversion pour la mise à jour
|
||||
private convertUpdateMerchantToBackend(dto: UpdateMerchantDto, existingMerchant?: Merchant): any {
|
||||
return this.dataAdapter.convertUpdateMerchantToApi(dto, existingMerchant);
|
||||
private convertUpdateMerchantToBackend(dto: UpdateMerchantDto): any {
|
||||
return this.dataAdapter.convertUpdateMerchantToApi(dto);
|
||||
}
|
||||
|
||||
// ==================== GESTION DES PERMISSIONS ====================
|
||||
@ -530,28 +512,24 @@ export class MerchantConfigManagement implements OnInit, OnDestroy {
|
||||
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 {
|
||||
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.removeSelectedLogo();
|
||||
this.createMerchantError = '';
|
||||
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 ====================
|
||||
|
||||
// ==================== 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 {
|
||||
|
||||
if (!this.canCreateMerchants) {
|
||||
this.createMerchantError = 'Vous n\'avez pas la permission de créer des marchands';
|
||||
return;
|
||||
@ -672,7 +743,6 @@ export class MerchantConfigManagement implements OnInit, OnDestroy {
|
||||
this.creatingMerchant = true;
|
||||
this.createMerchantError = '';
|
||||
|
||||
// Conversion pour l'API
|
||||
const createDto = this.convertMerchantToBackend(this.newMerchant);
|
||||
|
||||
console.log('📤 Creating merchant:', createDto);
|
||||
@ -681,13 +751,38 @@ export class MerchantConfigManagement implements OnInit, OnDestroy {
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe({
|
||||
next: (createdMerchant) => {
|
||||
// Conversion de la réponse pour Angular
|
||||
|
||||
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);
|
||||
this.creatingMerchant = false;
|
||||
this.modalService.dismissAll();
|
||||
this.refreshMerchantsList();
|
||||
|
||||
// Reset le formulaire et le logo
|
||||
this.resetMerchantForm();
|
||||
this.removeSelectedLogo();
|
||||
|
||||
this.cdRef.detectChanges();
|
||||
},
|
||||
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 {
|
||||
if (!this.selectedMerchantForEdit) {
|
||||
this.updateMerchantError = 'Aucun marchand sélectionné pour modification';
|
||||
return;
|
||||
}
|
||||
|
||||
// Validation des données complètes
|
||||
const validation = this.validateMerchantUpdate(this.selectedMerchantForEdit);
|
||||
if (!validation.isValid) {
|
||||
this.updateMerchantError = validation.errors.join(', ');
|
||||
@ -716,45 +866,289 @@ export class MerchantConfigManagement implements OnInit, OnDestroy {
|
||||
this.updatingMerchant = true;
|
||||
this.updateMerchantError = '';
|
||||
|
||||
// Conversion pour l'API avec TOUTES les données
|
||||
const merchantId = this.selectedMerchantForEdit.id!;
|
||||
const updateDto = this.convertUpdateMerchantToBackend(this.selectedMerchantForEdit, this.selectedMerchantForEdit);
|
||||
const merchantId = this.selectedMerchantForEdit!.id!;
|
||||
const merchantName = this.selectedMerchantForEdit.name!;
|
||||
|
||||
console.log('📤 Updating merchant with full data:', updateDto);
|
||||
let uploadObservable$;
|
||||
|
||||
this.merchantConfigService.updateMerchant(merchantId, updateDto)
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe({
|
||||
next: (updatedMerchant) => {
|
||||
// Conversion pour Angular
|
||||
const frontendMerchant = this.convertMerchantToFrontend(updatedMerchant);
|
||||
// Si un nouveau logo est sélectionné
|
||||
if (this.editLogoFile && this.logoChanged) {
|
||||
this.uploadingLogo = true;
|
||||
|
||||
this.updatingMerchant = false;
|
||||
this.modalService.dismissAll();
|
||||
this.refreshMerchantsConfigsView();
|
||||
this.refreshMerchantsList();
|
||||
uploadObservable$ = this.minioService.uploadMerchantLogo(
|
||||
merchantId,
|
||||
merchantName,
|
||||
this.editLogoFile
|
||||
).pipe(
|
||||
switchMap(uploadResponse => {
|
||||
console.log('✅ New logo uploaded:', uploadResponse);
|
||||
this.uploadingLogo = false;
|
||||
|
||||
// Mettre à jour le cache
|
||||
if (this.selectedMerchantId) {
|
||||
this.merchantProfiles[this.selectedMerchantId] = frontendMerchant;
|
||||
// Supprimer l'ancien logo si différent
|
||||
const oldLogo = this.selectedMerchantForEdit!.logo;
|
||||
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
|
||||
if (this.isMerchantUser && this.userMerchantId === merchantId) {
|
||||
this.userMerchant = frontendMerchant;
|
||||
// Mettre à jour le nom du logo dans l'objet merchant
|
||||
this.selectedMerchantForEdit!.logo = uploadResponse.data.fileName;
|
||||
|
||||
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
|
||||
@ -774,57 +1168,12 @@ export class MerchantConfigManagement implements OnInit, OnDestroy {
|
||||
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 {
|
||||
isValid: errors.length === 0,
|
||||
errors: errors
|
||||
};
|
||||
}
|
||||
|
||||
// Validation d'email
|
||||
private isValidEmail(email: string): boolean {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return emailRegex.test(email);
|
||||
}
|
||||
|
||||
confirmDeleteMerchant(): void {
|
||||
if (!this.selectedMerchantForDelete) {
|
||||
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 {
|
||||
this.selectedMerchantForEdit = {
|
||||
...merchant,
|
||||
configs: merchant.configs.map(config => ({ ...config })),
|
||||
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 {
|
||||
@ -1025,8 +1385,14 @@ export class MerchantConfigManagement implements OnInit, OnDestroy {
|
||||
return !this.isMerchantUser && this.activeTab === 'list';
|
||||
}
|
||||
|
||||
// ==================== NETTOYAGE ====================
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
|
||||
// Nettoyer les logos
|
||||
this.cleanupLogoResources();
|
||||
this.logoUrlCache.clear();
|
||||
}
|
||||
}
|
||||
@ -29,7 +29,7 @@ export class MerchantDataAdapter {
|
||||
|
||||
return {
|
||||
...apiMerchant,
|
||||
id: apiMerchant.id, //this.convertIdToString(apiMerchant.id),
|
||||
id: apiMerchant.id,
|
||||
configs: (apiMerchant.configs || []).map(config =>
|
||||
this.convertApiConfigToFrontend(config)
|
||||
),
|
||||
@ -105,7 +105,7 @@ export class MerchantDataAdapter {
|
||||
/**
|
||||
* Convertit un DTO de mise à jour pour l'API
|
||||
*/
|
||||
convertUpdateMerchantToApi(dto: UpdateMerchantDto, existingMerchant?: Merchant): any {
|
||||
convertUpdateMerchantToApi(dto: UpdateMerchantDto): any {
|
||||
this.validateUpdateMerchantDto(dto);
|
||||
|
||||
const updateData: any = {};
|
||||
@ -117,33 +117,6 @@ export class MerchantDataAdapter {
|
||||
if (dto.adresse !== undefined) updateData.adresse = dto.adresse?.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;
|
||||
}
|
||||
|
||||
@ -250,49 +223,6 @@ export class MerchantDataAdapter {
|
||||
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) {
|
||||
throw new Error(`Validation failed: ${errors.join(', ')}`);
|
||||
}
|
||||
|
||||
@ -7,16 +7,9 @@ import { MerchantUsersManagement } from '@modules/hub-users-management/merchant-
|
||||
|
||||
// Composants principaux
|
||||
import { DcbReportingDashboard } from '@modules/dcb-dashboard/dcb-reporting-dashboard';
|
||||
import { Team } from '@modules/team/team';
|
||||
|
||||
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 { Documentation } from '@modules/documentation/documentation';
|
||||
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
|
||||
@ -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',
|
||||
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",
|
||||
configApiUrl: 'https://api-merchant-config-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,
|
||||
localServiceTestApiUrl: "https://backoffice.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',
|
||||
apiCoreUrl: 'https://api-core-service.dcb.pixpay.sn/api/v1',
|
||||
reportingApiUrl: 'https://api-reporting-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",
|
||||
reportingApiUrl: "https://api-reporting-service.dcb.pixpay.sn/api/v1/"
|
||||
};
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
export const environment = {
|
||||
production: false,
|
||||
localServiceTestApiUrl: "http://localhost:4200/api/v1",
|
||||
iamApiUrl: "http://localhost:3000/api/v1",
|
||||
configApiUrl: 'https://api-merchant-config-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/',
|
||||
iamApiUrl: "http://localhost:3001/api/v1",
|
||||
configApiUrl: "http://localhost:3000/api/v1",
|
||||
apiCoreUrl: "https://api-core-service.dcb.pixpay.sn/api/v1",
|
||||
reportingApiUrl: "https://api-reporting-service.dcb.pixpay.sn/api/v1/"
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user