diff --git a/package-lock.json b/package-lock.json index 3389379..f3acdb3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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,7 @@ "@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", @@ -4698,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": { @@ -4747,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", @@ -4954,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", @@ -5013,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", @@ -5097,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", @@ -5130,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", @@ -5262,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", @@ -5280,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", @@ -6452,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", @@ -6591,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", @@ -6822,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" @@ -6832,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" @@ -6842,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" @@ -7058,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", @@ -7088,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", @@ -7140,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", @@ -7231,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" @@ -7246,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", @@ -7280,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", @@ -7305,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", @@ -7361,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" @@ -7393,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" @@ -7410,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" @@ -7426,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" @@ -7701,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", @@ -7714,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", @@ -7753,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", @@ -7800,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", @@ -7815,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", @@ -8918,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" @@ -9076,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", @@ -10183,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", @@ -10321,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", @@ -10640,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", @@ -10681,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", @@ -10733,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", @@ -11180,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", @@ -11216,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", @@ -11231,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", @@ -11358,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", @@ -11491,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", @@ -11744,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", @@ -11986,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", @@ -12002,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", @@ -12207,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", diff --git a/package.json b/package.json index da72977..0a317c3 100644 --- a/package.json +++ b/package.json @@ -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,7 @@ "@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", diff --git a/src/app/core/services/minio.service.ts b/src/app/core/services/minio.service.ts new file mode 100644 index 0000000..e4dd573 --- /dev/null +++ b/src/app/core/services/minio.service.ts @@ -0,0 +1,109 @@ +import { Injectable } from '@angular/core'; +import { HttpClient, HttpHeaders } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { environment } from '@environments/environment'; + +export interface UploadLogoResponse { + success: boolean; + fileName: string; + url: string; + size: number; +} + +@Injectable({ + providedIn: 'root' +}) +export class MinioService { + private apiUrl = `${environment.configApiUrl}/minio`; // URL de votre backend + + constructor(private http: HttpClient) {} + + /** + * Upload un logo de marchand vers MinIO + */ + uploadMerchantLogo(file: File, merchantId?: number): Observable { + const formData = new FormData(); + formData.append('file', file); + formData.append('bucketName', 'bo-assets'); + + // Générer un nom unique pour le logo + const timestamp = Date.now(); + const extension = file.name.split('.').pop(); + const fileName = merchantId + ? `merchant_${merchantId}_${timestamp}.${extension}` + : `logo_${timestamp}.${extension}`; + + formData.append('objectName', fileName); + + return this.http.post(`${this.apiUrl}/upload-logo`, formData); + } + + /** + * Récupère l'URL présignée pour afficher un logo + * URL valide pour 7 jours + */ + getMerchantLogoUrl(logoFileName: string): Observable { + const expiry = 7 * 24 * 60 * 60; // 7 jours en secondes + return this.http.get<{ url: string }>( + `${this.apiUrl}/presigned-url`, + { + params: { + bucketName: 'bo-assets', + objectName: logoFileName, + expiry: expiry.toString() + } + } + ).pipe(map(response => response.url)); + } + + /** + * Supprime un logo de marchand + */ + deleteMerchantLogo(logoFileName: string): Observable { + return this.http.delete(`${this.apiUrl}/delete`, { + params: { + bucketName: 'bo-assets', + objectName: logoFileName + } + }); + } + + /** + * Valide qu'un fichier est une image valide + */ + validateImageFile(file: File): { valid: boolean; error?: string } { + // Vérifier le type MIME + const allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml']; + if (!allowedTypes.includes(file.type)) { + return { + valid: false, + error: 'Format non supporté. Utilisez JPG, PNG, GIF, WebP ou SVG.' + }; + } + + // Vérifier la taille (2MB max pour un logo) + const maxSize = 2 * 1024 * 1024; // 2MB + if (file.size > maxSize) { + return { + valid: false, + error: 'Le fichier est trop volumineux (max 2MB pour un logo)' + }; + } + + // Vérifier les dimensions si possible (optionnel) + return { valid: true }; + } + + /** + * Prévisualise une image avant upload + */ + previewImage(file: File): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = (e: any) => resolve(e.target.result); + reader.onerror = () => reject(new Error('Erreur lors de la lecture du fichier')); + reader.readAsDataURL(file); + }); + } +} \ No newline at end of file diff --git a/src/app/modules/merchant-config/merchant-config-list/merchant-config-list.html b/src/app/modules/merchant-config/merchant-config-list/merchant-config-list.html index 06ba01f..a8307df 100644 --- a/src/app/modules/merchant-config/merchant-config-list/merchant-config-list.html +++ b/src/app/modules/merchant-config/merchant-config-list/merchant-config-list.html @@ -180,17 +180,20 @@
- @if (merchant.logo) { - Logo {{ merchant.name }} - }
+ +
+ + + + +
{{ merchant.name }} {{ merchant.adresse }} diff --git a/src/app/modules/merchant-config/merchant-config-list/merchant-config-list.ts b/src/app/modules/merchant-config/merchant-config-list/merchant-config-list.ts index 54308c0..cb744a8 100644 --- a/src/app/modules/merchant-config/merchant-config-list/merchant-config-list.ts +++ b/src/app/modules/merchant-config/merchant-config-list/merchant-config-list.ts @@ -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, 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', @@ -39,6 +41,14 @@ export class MerchantConfigsList implements OnInit, OnDestroy { protected roleService = inject(RoleManagementService); private cdRef = inject(ChangeDetectorRef); private destroy$ = new Subject(); + + private minioService = inject(MinioService); + private sanitizer = inject(DomSanitizer); + + // Cache des URLs de logos + private logoUrlCache = new Map(); + // Ajouter un cache pour les logos non trouvés + private logoErrorCache = new Set(); // Configuration readonly ConfigType = ConfigType; @@ -208,6 +218,85 @@ export class MerchantConfigsList implements OnInit, OnDestroy { }); } + // ==================== AFFICHAGE DU LOGO ==================== + + /** + * Récupère l'URL du logo avec fallback automatique + */ + getMerchantLogoUrl(logoFileName: string, merchantName?: string): Observable { + // Vérifier si le logo est en cache d'erreur + if (this.logoErrorCache.has(logoFileName)) { + const defaultLogo = this.getDefaultLogoUrl(merchantName || logoFileName); + return of(defaultLogo); + } + + // Vérifier le cache normal + if (this.logoUrlCache.has(logoFileName)) { + return of(this.logoUrlCache.get(logoFileName)!); + } + + // Récupérer l'URL depuis MinIO + return this.minioService.getMerchantLogoUrl(logoFileName).pipe( + tap(url => { + // Mettre en cache + this.logoUrlCache.set(logoFileName, url); + }), + catchError(error => { + // En cas d'erreur, ajouter au cache d'erreur et retourner le logo par défaut + this.logoErrorCache.add(logoFileName); + const defaultLogo = this.getDefaultLogoUrl(merchantName || logoFileName); + 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 = [ + 'FF6B6B', '4ECDC4', '45B7D1', '96CEB4', 'FFEAA7', + 'DDA0DD', '98D8C8', 'F7DC6F', 'BB8FCE', '85C1E9' + ]; + + const colorIndex = merchantName.length % colors.length; + const backgroundColor = colors[colorIndex]; + const textColor = 'FFFFFF'; // Blanc pour contraste + + return `https://ui-avatars.com/api/?name=${encodeURIComponent(initials)}&background=${backgroundColor}&color=${textColor}&size=200&bold=true&font-size=0.5`; + } + + /** + * Extrait les initiales de manière intelligente + */ + private extractInitials(name: string): string { + // Nettoyer le nom + const cleanedName = name.trim().toUpperCase(); + + // Extraire les mots (ignorer les articles, prépositions courtes) + 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 significatifs + const initials = words + .filter(word => word.length > 2) // Ignorer les mots courts + .slice(0, 2) // Prendre maximum 2 mots + .map(word => word[0]) + .join(''); + + return initials || name.substring(0, 2).toUpperCase(); + } + + private buildSearchParams(): SearchMerchantsParams { const params: SearchMerchantsParams = {}; diff --git a/src/app/modules/merchant-config/merchant-config-view/merchant-config-view.html b/src/app/modules/merchant-config/merchant-config-view/merchant-config-view.html index c71355d..63b156c 100644 --- a/src/app/modules/merchant-config/merchant-config-view/merchant-config-view.html +++ b/src/app/modules/merchant-config/merchant-config-view/merchant-config-view.html @@ -110,10 +110,21 @@
-
+

{{ merchant.name }}

{{ merchant.description || 'Aucune description' }}

+ +
+ + + +
+
@if (canEditMerchant()) {
+ + +
- - +
+ + +
+ +
+
+ Prévisualisation du logo + +
+ +
+ +

Aucun logo sélectionné

+
+
+ + +
+ + + + + Formats: JPG, PNG, GIF, WebP, SVG | Taille max: 2MB + +
+
+ + +
+ {{ logoUploadError }} +
+ + +
+
+ Upload en cours... +
+ Upload du logo en cours... +
+
@@ -517,15 +570,79 @@
- - +
+ + +
+ +
+ +
+ Nouveau logo +
Nouveau
+ +
+ + +
+ Logo actuel +
Actuel
+
+ + +
+ +

Aucun logo

+
+
+ + +
+ + + + + + + Formats: JPG, PNG, GIF, WebP, SVG | Taille max: 2MB + +
+
+ + +
+
+ Upload en cours... +
+ Upload du nouveau logo en cours... +
+
diff --git a/src/app/modules/merchant-config/merchant-config.ts b/src/app/modules/merchant-config/merchant-config.ts index b5b1e19..34dd2b0 100644 --- a/src/app/modules/merchant-config/merchant-config.ts +++ b/src/app/modules/merchant-config/merchant-config.ts @@ -3,7 +3,8 @@ 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, 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'; @@ -13,6 +14,8 @@ 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, @@ -47,12 +50,17 @@ 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(); + private minioService = inject(MinioService); + private sanitizer = inject(DomSanitizer); + + // Cache des URLs de logos + private logoUrlCache = new Map(); + // Configuration readonly UserRole = UserRole; readonly ConfigType = ConfigType; @@ -63,6 +71,20 @@ 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'; selectedMerchantId: number | null = null; @@ -594,6 +616,7 @@ export class MerchantConfigManagement implements OnInit, OnDestroy { } this.resetMerchantForm(); + this.removeSelectedLogo(); this.createMerchantError = ''; this.openModal(this.createMerchantModal); } @@ -655,9 +678,125 @@ export class MerchantConfigManagement implements OnInit, OnDestroy { }); } + createMerchant(): void { + this.createMerchantWithLogo(); + } + + updateMerchant(): void { + this.updateMerchantWithLogo(); + } + + /** + * Génère une URL de logo par défaut basée sur les initiales + */ + getDefaultLogoUrl(merchantName: string): string { + // Créer un avatar avec les initiales + const initials = merchantName + .split(' ') + .map(word => word[0]) + .join('') + .substring(0, 2) + .toUpperCase(); + + // Utiliser un service comme UI Avatars + return `https://ui-avatars.com/api/?name=${encodeURIComponent(initials)}&background=random&size=200&bold=true`; + } + + /** + * 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 ==================== - createMerchant(): void { + // ==================== 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 = ''; + } + } + + /** + * Upload le logo et crée le marchand + */ + createMerchantWithLogo(): void { if (!this.canCreateMerchants) { this.createMerchantError = 'Vous n\'avez pas la permission de créer des marchands'; return; @@ -672,7 +811,41 @@ export class MerchantConfigManagement implements OnInit, OnDestroy { this.creatingMerchant = true; this.createMerchantError = ''; - // Conversion pour l'API + // Si un logo est sélectionné, l'uploader d'abord + if (this.selectedLogoFile) { + + this.uploadingLogo = true; + this.minioService.uploadMerchantLogo(this.selectedLogoFile) + .pipe(takeUntil(this.destroy$)) + .subscribe({ + next: (uploadResponse) => { + console.log('✅ Logo uploaded:', uploadResponse); + this.uploadingLogo = false; + + // Ajouter le nom du fichier au DTO + this.newMerchant.logo = uploadResponse.fileName; + + // Créer le marchand avec le logo + this.createMerchantApiCall(); + }, + error: (error) => { + console.error('❌ Error uploading logo:', error); + this.uploadingLogo = false; + this.creatingMerchant = false; + this.createMerchantError = 'Erreur lors de l\'upload du logo: ' + (error.message || 'Erreur inconnue'); + this.cdRef.detectChanges(); + } + }); + } else { + // Pas de logo, créer directement + this.createMerchantApiCall(); + } + } + + /** + * Appel API pour créer le marchand + */ + private createMerchantApiCall(): void { const createDto = this.convertMerchantToBackend(this.newMerchant); console.log('📤 Creating merchant:', createDto); @@ -681,13 +854,17 @@ 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); 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 +876,69 @@ export class MerchantConfigManagement implements OnInit, OnDestroy { }); } - // Mise à jour COMPLÈTE du merchant - updateMerchant(): void { + // ==================== 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 = ''; + } + } + + /** + * Met à jour le marchand avec le nouveau logo + */ + updateMerchantWithLogo(): 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,9 +948,56 @@ 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); + // Si un nouveau logo est sélectionné + if (this.editLogoFile && this.logoChanged) { + this.uploadingLogo = true; + const merchantId = this.selectedMerchantForEdit.id!; + + this.minioService.uploadMerchantLogo(this.editLogoFile, merchantId) + .pipe(takeUntil(this.destroy$)) + .subscribe({ + next: (uploadResponse) => { + console.log('✅ New logo uploaded:', uploadResponse); + this.uploadingLogo = false; + + // Supprimer l'ancien logo si différent + const oldLogo = this.selectedMerchantForEdit!.logo; + if (oldLogo && oldLogo !== uploadResponse.fileName) { + this.minioService.deleteMerchantLogo(oldLogo).subscribe({ + next: () => console.log('🗑️ Old logo deleted'), + error: (err) => console.error('⚠️ Error deleting old logo:', err) + }); + } + + // Mettre à jour le logo dans le DTO + this.selectedMerchantForEdit!.logo = uploadResponse.fileName; + + // Mettre à jour le marchand + this.updateMerchantApiCall(); + }, + error: (error) => { + console.error('❌ Error uploading new logo:', error); + this.uploadingLogo = false; + this.updatingMerchant = false; + this.updateMerchantError = 'Erreur lors de l\'upload du logo: ' + (error.message || 'Erreur inconnue'); + this.cdRef.detectChanges(); + } + }); + } else { + // Pas de changement de logo, mettre à jour directement + this.updateMerchantApiCall(); + } + } + + /** + * Appel API pour mettre à jour le marchand + */ + private updateMerchantApiCall(): void { + const merchantId = this.selectedMerchantForEdit!.id!; + const updateDto = this.convertUpdateMerchantToBackend( + this.selectedMerchantForEdit!, + this.selectedMerchantForEdit! + ); console.log('📤 Updating merchant with full data:', updateDto); @@ -726,7 +1005,6 @@ export class MerchantConfigManagement implements OnInit, OnDestroy { .pipe(takeUntil(this.destroy$)) .subscribe({ next: (updatedMerchant) => { - // Conversion pour Angular const frontendMerchant = this.convertMerchantToFrontend(updatedMerchant); this.updatingMerchant = false; @@ -737,15 +1015,22 @@ export class MerchantConfigManagement implements OnInit, OnDestroy { // Mettre à jour le cache if (this.selectedMerchantId) { this.merchantProfiles[this.selectedMerchantId] = frontendMerchant; + + // Invalider le cache de l'URL du logo + if (frontendMerchant.logo) { + this.logoUrlCache.delete(frontendMerchant.logo); + } } - // Mettre à jour le marchand de l'utilisateur si nécessaire 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) => { @@ -757,6 +1042,47 @@ export class MerchantConfigManagement implements OnInit, OnDestroy { }); } + // ==================== AFFICHAGE DU LOGO ==================== + + /** + * Récupère l'URL du logo pour affichage + */ + getMerchantLogoUrl(logoFileName: string): Observable { + // Vérifier le cache + if (this.logoUrlCache.has(logoFileName)) { + return of(this.logoUrlCache.get(logoFileName)!); + } + + // Récupérer l'URL depuis MinIO + return this.minioService.getMerchantLogoUrl(logoFileName).pipe( + tap(url => { + // Mettre en cache + this.logoUrlCache.set(logoFileName, url); + }) + ); + } + + /** + * Charge le logo pour l'édition + */ + loadMerchantLogoForEdit(merchant: Merchant): void { + if (!merchant.logo) { + this.currentLogoUrl = null; + return; + } + + this.getMerchantLogoUrl(merchant.logo).subscribe({ + next: (url) => { + this.currentLogoUrl = url; + this.cdRef.detectChanges(); + }, + error: (error) => { + console.error('Error loading logo:', error); + this.currentLogoUrl = null; + } + }); + } + // Validation complète pour la mise à jour validateMerchantUpdate(merchant: UpdateMerchantDto): { isValid: boolean; errors: string[] } { const errors: string[] = []; @@ -891,19 +1217,30 @@ 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 { if (this.merchantConfigsList && typeof this.merchantConfigsList.refreshData === 'function') { console.log('🔄 Refreshing merchants list...'); @@ -1025,8 +1362,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(); } } \ No newline at end of file diff --git a/src/environments/environment.preprod.ts b/src/environments/environment.preprod.ts index 37445a5..d7ddd92 100644 --- a/src/environments/environment.preprod.ts +++ b/src/environments/environment.preprod.ts @@ -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/' }; diff --git a/src/environments/environment.prod.ts b/src/environments/environment.prod.ts index 37445a5..3c7a908 100644 --- a/src/environments/environment.prod.ts +++ b/src/environments/environment.prod.ts @@ -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/" }; diff --git a/src/environments/environment.ts b/src/environments/environment.ts index 0f44a19..a047dd6 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -4,5 +4,5 @@ export const environment = { 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/", + reportingApiUrl: "https://api-reporting-service.dcb.pixpay.sn/api/v1/" }