feat: Manage Images using Minio Service

This commit is contained in:
diallolatoile 2026-01-08 01:22:36 +00:00
parent 754244345a
commit 47f09b3c4e
12 changed files with 1301 additions and 71 deletions

506
package-lock.json generated
View File

@ -54,6 +54,7 @@
"leaflet": "^1.9.4", "leaflet": "^1.9.4",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"mermaid": "^11.12.2", "mermaid": "^11.12.2",
"minio": "^8.0.6",
"ng-otp-input": "^2.0.9", "ng-otp-input": "^2.0.9",
"ng2-charts": "^8.0.0", "ng2-charts": "^8.0.0",
"ngx-countup": "^13.2.0", "ngx-countup": "^13.2.0",
@ -74,6 +75,7 @@
"@types/jquery": "^3.5.33", "@types/jquery": "^3.5.33",
"@types/leaflet": "^1.9.21", "@types/leaflet": "^1.9.21",
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
"@types/node": "^25.0.3",
"baseline-browser-mapping": "^2.9.11", "baseline-browser-mapping": "^2.9.11",
"jasmine-core": "~5.12.0", "jasmine-core": "~5.12.0",
"karma": "~6.4.4", "karma": "~6.4.4",
@ -4698,9 +4700,9 @@
} }
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "24.9.1", "version": "25.0.3",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.1.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz",
"integrity": "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==", "integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -4747,6 +4749,13 @@
"dev": true, "dev": true,
"license": "BSD-2-Clause" "license": "BSD-2-Clause"
}, },
"node_modules/@zxing/text-encoding": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/@zxing/text-encoding/-/text-encoding-0.9.0.tgz",
"integrity": "sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==",
"license": "(Unlicense OR Apache-2.0)",
"optional": true
},
"node_modules/abbrev": { "node_modules/abbrev": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.1.tgz", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.1.tgz",
@ -4954,6 +4963,27 @@
"url": "https://github.com/sponsors/jonschlinkert" "url": "https://github.com/sponsors/jonschlinkert"
} }
}, },
"node_modules/async": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
"integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
"license": "MIT"
},
"node_modules/available-typed-arrays": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
"integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==",
"license": "MIT",
"dependencies": {
"possible-typed-array-names": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/balanced-match": { "node_modules/balanced-match": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@ -5013,6 +5043,29 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/block-stream2": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/block-stream2/-/block-stream2-2.1.0.tgz",
"integrity": "sha512-suhjmLI57Ewpmq00qaygS8UgEq2ly2PCItenIyhMqVjo4t4pGzqMvfgJuX8iWTeSDdfSSqS6j38fL4ToNL7Pfg==",
"license": "MIT",
"dependencies": {
"readable-stream": "^3.4.0"
}
},
"node_modules/block-stream2/node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"license": "MIT",
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/body-parser": { "node_modules/body-parser": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz",
@ -5097,6 +5150,12 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/browser-or-node": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/browser-or-node/-/browser-or-node-2.1.1.tgz",
"integrity": "sha512-8CVjaLJGuSKMVTxJ2DpBl5XnlNDiT4cQFeuCJJrvJmts9YrTZDizTX7PjC2s6W4x+MBGZeEY6dGMrF04/6Hgqg==",
"license": "MIT"
},
"node_modules/browserslist": { "node_modules/browserslist": {
"version": "4.27.0", "version": "4.27.0",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.27.0.tgz", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.27.0.tgz",
@ -5130,6 +5189,15 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
} }
}, },
"node_modules/buffer-crc32": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz",
"integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==",
"license": "MIT",
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/buffer-from": { "node_modules/buffer-from": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
@ -5262,11 +5330,28 @@
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/call-bind": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
"integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.0",
"es-define-property": "^1.0.0",
"get-intrinsic": "^1.2.4",
"set-function-length": "^1.2.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/call-bind-apply-helpers": { "node_modules/call-bind-apply-helpers": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"es-errors": "^1.3.0", "es-errors": "^1.3.0",
@ -5280,7 +5365,6 @@
"version": "1.0.4", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"call-bind-apply-helpers": "^1.0.2", "call-bind-apply-helpers": "^1.0.2",
@ -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": { "node_modules/delaunator": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz",
@ -6591,7 +6701,6 @@
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"call-bind-apply-helpers": "^1.0.1", "call-bind-apply-helpers": "^1.0.1",
@ -6822,7 +6931,6 @@
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
@ -6832,7 +6940,6 @@
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
@ -6842,7 +6949,6 @@
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"es-errors": "^1.3.0" "es-errors": "^1.3.0"
@ -7058,6 +7164,24 @@
], ],
"license": "BSD-3-Clause" "license": "BSD-3-Clause"
}, },
"node_modules/fast-xml-parser": {
"version": "4.5.3",
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.3.tgz",
"integrity": "sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/NaturalIntelligence"
}
],
"license": "MIT",
"dependencies": {
"strnum": "^1.1.1"
},
"bin": {
"fxparser": "src/cli/cli.js"
}
},
"node_modules/fdir": { "node_modules/fdir": {
"version": "6.5.0", "version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
@ -7088,6 +7212,15 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/filter-obj": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz",
"integrity": "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/finalhandler": { "node_modules/finalhandler": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz",
@ -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": { "node_modules/foreground-child": {
"version": "3.3.1", "version": "3.3.1",
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
@ -7231,7 +7379,6 @@
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"dev": true,
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
@ -7246,6 +7393,15 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/generator-function": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz",
"integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/gensync": { "node_modules/gensync": {
"version": "1.0.0-beta.2", "version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
@ -7280,7 +7436,6 @@
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"call-bind-apply-helpers": "^1.0.2", "call-bind-apply-helpers": "^1.0.2",
@ -7305,7 +7460,6 @@
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"dunder-proto": "^1.0.1", "dunder-proto": "^1.0.1",
@ -7361,7 +7515,6 @@
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
@ -7393,11 +7546,22 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/has-property-descriptors": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
"integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
"license": "MIT",
"dependencies": {
"es-define-property": "^1.0.0"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-symbols": { "node_modules/has-symbols": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
@ -7410,7 +7574,6 @@
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"has-symbols": "^1.0.3" "has-symbols": "^1.0.3"
@ -7426,7 +7589,6 @@
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"function-bind": "^1.1.2" "function-bind": "^1.1.2"
@ -7701,6 +7863,22 @@
"node": ">= 0.10" "node": ">= 0.10"
} }
}, },
"node_modules/is-arguments": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz",
"integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==",
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"has-tostringtag": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-binary-path": { "node_modules/is-binary-path": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
@ -7714,6 +7892,18 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/is-callable": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
"integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-core-module": { "node_modules/is-core-module": {
"version": "2.16.1", "version": "2.16.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
@ -7753,6 +7943,25 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/is-generator-function": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz",
"integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==",
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.4",
"generator-function": "^2.0.0",
"get-proto": "^1.0.1",
"has-tostringtag": "^1.0.2",
"safe-regex-test": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-glob": { "node_modules/is-glob": {
"version": "4.0.3", "version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
@ -7800,7 +8009,6 @@
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
"integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"call-bound": "^1.0.2", "call-bound": "^1.0.2",
@ -7815,6 +8023,21 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/is-typed-array": {
"version": "1.1.15",
"resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz",
"integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==",
"license": "MIT",
"dependencies": {
"which-typed-array": "^1.1.16"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-unicode-supported": { "node_modules/is-unicode-supported": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz",
@ -8918,7 +9141,6 @@
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
@ -9076,6 +9298,67 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/minio": {
"version": "8.0.6",
"resolved": "https://registry.npmjs.org/minio/-/minio-8.0.6.tgz",
"integrity": "sha512-sOeh2/b/XprRmEtYsnNRFtOqNRTPDvYtMWh+spWlfsuCV/+IdxNeKVUMKLqI7b5Dr07ZqCPuaRGU/rB9pZYVdQ==",
"license": "Apache-2.0",
"dependencies": {
"async": "^3.2.4",
"block-stream2": "^2.1.0",
"browser-or-node": "^2.1.1",
"buffer-crc32": "^1.0.0",
"eventemitter3": "^5.0.1",
"fast-xml-parser": "^4.4.1",
"ipaddr.js": "^2.0.1",
"lodash": "^4.17.21",
"mime-types": "^2.1.35",
"query-string": "^7.1.3",
"stream-json": "^1.8.0",
"through2": "^4.0.2",
"web-encoding": "^1.1.5",
"xml2js": "^0.5.0 || ^0.6.2"
},
"engines": {
"node": "^16 || ^18 || >=20"
}
},
"node_modules/minio/node_modules/eventemitter3": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
"integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
"license": "MIT"
},
"node_modules/minio/node_modules/ipaddr.js": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.3.0.tgz",
"integrity": "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==",
"license": "MIT",
"engines": {
"node": ">= 10"
}
},
"node_modules/minio/node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/minio/node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/minipass": { "node_modules/minipass": {
"version": "7.1.2", "version": "7.1.2",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
@ -10183,6 +10466,15 @@
"points-on-curve": "0.2.0" "points-on-curve": "0.2.0"
} }
}, },
"node_modules/possible-typed-array-names": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
"integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.5.6", "version": "8.5.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
@ -10321,6 +10613,24 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/query-string": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz",
"integrity": "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==",
"license": "MIT",
"dependencies": {
"decode-uri-component": "^0.2.2",
"filter-obj": "^1.1.0",
"split-on-first": "^1.0.0",
"strict-uri-encode": "^2.0.0"
},
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/quill": { "node_modules/quill": {
"version": "2.0.3", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/quill/-/quill-2.0.3.tgz", "resolved": "https://registry.npmjs.org/quill/-/quill-2.0.3.tgz",
@ -10640,7 +10950,6 @@
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz",
"integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"call-bound": "^1.0.2", "call-bound": "^1.0.2",
@ -10681,6 +10990,12 @@
"@parcel/watcher": "^2.4.1" "@parcel/watcher": "^2.4.1"
} }
}, },
"node_modules/sax": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.4.3.tgz",
"integrity": "sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ==",
"license": "BlueOak-1.0.0"
},
"node_modules/semver": { "node_modules/semver": {
"version": "7.7.2", "version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
@ -10733,6 +11048,23 @@
"node": ">= 18" "node": ">= 18"
} }
}, },
"node_modules/set-function-length": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
"integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
"license": "MIT",
"dependencies": {
"define-data-property": "^1.1.4",
"es-errors": "^1.3.0",
"function-bind": "^1.1.2",
"get-intrinsic": "^1.2.4",
"gopd": "^1.0.1",
"has-property-descriptors": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/setimmediate": { "node_modules/setimmediate": {
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
@ -11180,6 +11512,15 @@
"dev": true, "dev": true,
"license": "CC0-1.0" "license": "CC0-1.0"
}, },
"node_modules/split-on-first": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz",
"integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/ssri": { "node_modules/ssri": {
"version": "12.0.0", "version": "12.0.0",
"resolved": "https://registry.npmjs.org/ssri/-/ssri-12.0.0.tgz", "resolved": "https://registry.npmjs.org/ssri/-/ssri-12.0.0.tgz",
@ -11216,6 +11557,21 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/stream-chain": {
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/stream-chain/-/stream-chain-2.2.5.tgz",
"integrity": "sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA==",
"license": "BSD-3-Clause"
},
"node_modules/stream-json": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/stream-json/-/stream-json-1.9.1.tgz",
"integrity": "sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw==",
"license": "BSD-3-Clause",
"dependencies": {
"stream-chain": "^2.2.5"
}
},
"node_modules/streamroller": { "node_modules/streamroller": {
"version": "3.1.5", "version": "3.1.5",
"resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.1.5.tgz", "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.1.5.tgz",
@ -11231,6 +11587,15 @@
"node": ">=8.0" "node": ">=8.0"
} }
}, },
"node_modules/strict-uri-encode": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz",
"integrity": "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==",
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/string_decoder": { "node_modules/string_decoder": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
@ -11358,6 +11723,18 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/strnum": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz",
"integrity": "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/NaturalIntelligence"
}
],
"license": "MIT"
},
"node_modules/stylis": { "node_modules/stylis": {
"version": "4.3.6", "version": "4.3.6",
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz", "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz",
@ -11491,6 +11868,29 @@
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/through2": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz",
"integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==",
"license": "MIT",
"dependencies": {
"readable-stream": "3"
}
},
"node_modules/through2/node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"license": "MIT",
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/tinyexec": { "node_modules/tinyexec": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz",
@ -11744,6 +12144,19 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/util": {
"version": "0.12.5",
"resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz",
"integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==",
"license": "MIT",
"dependencies": {
"inherits": "^2.0.3",
"is-arguments": "^1.0.4",
"is-generator-function": "^1.0.7",
"is-typed-array": "^1.1.3",
"which-typed-array": "^1.1.2"
}
},
"node_modules/util-deprecate": { "node_modules/util-deprecate": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@ -11986,6 +12399,18 @@
"license": "MIT", "license": "MIT",
"optional": true "optional": true
}, },
"node_modules/web-encoding": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/web-encoding/-/web-encoding-1.1.5.tgz",
"integrity": "sha512-HYLeVCdJ0+lBYV2FvNZmv3HJ2Nt0QYXqZojk3d9FJOLkwnuhzM9tmamh8d7HPM8QqjKH8DeHkFTx+CFlWpZZDA==",
"license": "MIT",
"dependencies": {
"util": "^0.12.3"
},
"optionalDependencies": {
"@zxing/text-encoding": "0.9.0"
}
},
"node_modules/which": { "node_modules/which": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@ -12002,6 +12427,27 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/which-typed-array": {
"version": "1.1.19",
"resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz",
"integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==",
"license": "MIT",
"dependencies": {
"available-typed-arrays": "^1.0.7",
"call-bind": "^1.0.8",
"call-bound": "^1.0.4",
"for-each": "^0.3.5",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-tostringtag": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/wrap-ansi": { "node_modules/wrap-ansi": {
"version": "6.2.0", "version": "6.2.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
@ -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": { "node_modules/y18n": {
"version": "5.0.8", "version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",

View File

@ -57,6 +57,7 @@
"leaflet": "^1.9.4", "leaflet": "^1.9.4",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"mermaid": "^11.12.2", "mermaid": "^11.12.2",
"minio": "^8.0.6",
"ng-otp-input": "^2.0.9", "ng-otp-input": "^2.0.9",
"ng2-charts": "^8.0.0", "ng2-charts": "^8.0.0",
"ngx-countup": "^13.2.0", "ngx-countup": "^13.2.0",
@ -77,6 +78,7 @@
"@types/jquery": "^3.5.33", "@types/jquery": "^3.5.33",
"@types/leaflet": "^1.9.21", "@types/leaflet": "^1.9.21",
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
"@types/node": "^25.0.3",
"baseline-browser-mapping": "^2.9.11", "baseline-browser-mapping": "^2.9.11",
"jasmine-core": "~5.12.0", "jasmine-core": "~5.12.0",
"karma": "~6.4.4", "karma": "~6.4.4",

View File

@ -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<UploadLogoResponse> {
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<UploadLogoResponse>(`${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<string> {
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<any> {
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<string> {
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);
});
}
}

View File

@ -180,17 +180,20 @@
<tr> <tr>
<td> <td>
<div class="d-flex align-items-center"> <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"> <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> <ng-icon name="lucideStore" class="text-primary fs-12"></ng-icon>
</div> </div>
<!-- Logo du marchand -->
<div class="avatar-container me-3 position-relative">
<ng-container *ngIf="merchant.logo;">
<!-- Image du logo -->
<img
[src]="getMerchantLogoUrl(merchant.logo) | async"
[alt]="merchant.name + ' logo'"
class="merchant-logo avatar-lg rounded-circle"
/>
</ng-container>
</div>
<div> <div>
<strong class="d-block">{{ merchant.name }}</strong> <strong class="d-block">{{ merchant.name }}</strong>
<small class="text-muted">{{ merchant.adresse }}</small> <small class="text-muted">{{ merchant.adresse }}</small>

View File

@ -4,7 +4,7 @@ import { FormsModule } from '@angular/forms';
import { NgIcon } from '@ng-icons/core'; import { NgIcon } from '@ng-icons/core';
import { NgbPaginationModule } from '@ng-bootstrap/ng-bootstrap'; import { NgbPaginationModule } from '@ng-bootstrap/ng-bootstrap';
import { Observable, Subject, of } from 'rxjs'; import { Observable, Subject, of } from 'rxjs';
import { catchError, takeUntil } from 'rxjs/operators'; import { catchError, takeUntil, tap } from 'rxjs/operators';
import { import {
Merchant, Merchant,
@ -20,6 +20,8 @@ import { MerchantConfigService } from '../merchant-config.service';
import { RoleManagementService } from '@core/services/hub-users-roles-management.service'; import { RoleManagementService } from '@core/services/hub-users-roles-management.service';
import { AuthService } from '@core/services/auth.service'; import { AuthService } from '@core/services/auth.service';
import { UiCard } from '@app/components/ui-card'; import { UiCard } from '@app/components/ui-card';
import { DomSanitizer } from '@angular/platform-browser';
import { MinioService } from '@core/services/minio.service';
@Component({ @Component({
selector: 'app-merchant-config-list', selector: 'app-merchant-config-list',
@ -40,6 +42,14 @@ export class MerchantConfigsList implements OnInit, OnDestroy {
private cdRef = inject(ChangeDetectorRef); private cdRef = inject(ChangeDetectorRef);
private destroy$ = new Subject<void>(); private destroy$ = new Subject<void>();
private minioService = inject(MinioService);
private sanitizer = inject(DomSanitizer);
// Cache des URLs de logos
private logoUrlCache = new Map<string, string>();
// Ajouter un cache pour les logos non trouvés
private logoErrorCache = new Set<string>();
// Configuration // Configuration
readonly ConfigType = ConfigType; readonly ConfigType = ConfigType;
readonly Operator = Operator; readonly Operator = Operator;
@ -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<string> {
// 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 { private buildSearchParams(): SearchMerchantsParams {
const params: SearchMerchantsParams = {}; const params: SearchMerchantsParams = {};

View File

@ -110,10 +110,21 @@
<div class="profile-section"> <div class="profile-section">
<div class="profile-header"> <div class="profile-header">
<div class="row align-items-center"> <div class="row align-items-center">
<div class="col-md-8"> <div class="col-md-4">
<h2 class="mb-2">{{ merchant.name }}</h2> <h2 class="mb-2">{{ merchant.name }}</h2>
<p class="mb-0 opacity-75">{{ merchant.description || 'Aucune description' }}</p> <p class="mb-0 opacity-75">{{ merchant.description || 'Aucune description' }}</p>
</div> </div>
<div class="col-md-4">
<ng-container *ngIf="merchant.logo;">
<img
[src]="getMerchantLogoUrl(merchant.logo) | async"
[alt]="merchant.name"
class="merchant-logo-large"
/>
</ng-container>
</div>
<div class="col-md-4 text-md-end"> <div class="col-md-4 text-md-end">
@if (canEditMerchant()) { @if (canEditMerchant()) {
<button <button

View File

@ -3,7 +3,7 @@ import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { NgIcon } from '@ng-icons/core'; import { NgIcon } from '@ng-icons/core';
import { NgbAlertModule, NgbPaginationModule, NgbNavModule, NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { NgbAlertModule, NgbPaginationModule, NgbNavModule, NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { Subject, takeUntil } from 'rxjs'; import { catchError, Observable, of, Subject, takeUntil, tap } from 'rxjs';
import { import {
Merchant, Merchant,
@ -22,6 +22,8 @@ import { MerchantDataAdapter } from '../merchant-data-adapter.service';
import { RoleManagementService } from '@core/services/hub-users-roles-management.service'; import { RoleManagementService } from '@core/services/hub-users-roles-management.service';
import { AuthService } from '@core/services/auth.service'; import { AuthService } from '@core/services/auth.service';
import { UserRole } from '@core/models/dcb-bo-hub-user.model'; import { UserRole } from '@core/models/dcb-bo-hub-user.model';
import { MinioService } from '@core/services/minio.service';
import { DomSanitizer } from '@angular/platform-browser';
@Component({ @Component({
selector: 'app-merchant-config-view', selector: 'app-merchant-config-view',
@ -180,6 +182,14 @@ export class MerchantConfigView implements OnInit, OnDestroy {
private modalService = inject(NgbModal); private modalService = inject(NgbModal);
private destroy$ = new Subject<void>(); private destroy$ = new Subject<void>();
private minioService = inject(MinioService);
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>();
readonly ConfigType = ConfigType; readonly ConfigType = ConfigType;
readonly Operator = Operator; readonly Operator = Operator;
readonly MerchantUtils = MerchantUtils; readonly MerchantUtils = MerchantUtils;
@ -281,6 +291,84 @@ export class MerchantConfigView implements OnInit, OnDestroy {
}); });
} }
// ==================== AFFICHAGE DU LOGO ====================
/**
* Récupère l'URL du logo avec fallback automatique
*/
getMerchantLogoUrl(logoFileName: string, merchantName?: string): Observable<string> {
// 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();
}
/** /**
* Charge les permissions de l'utilisateur courant * Charge les permissions de l'utilisateur courant
*/ */

View File

@ -176,16 +176,69 @@
} }
</div> </div>
<!-- ==================== MODAL CRÉATION - SECTION LOGO ==================== -->
<!-- À ajouter dans votre modal de création de marchand -->
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label">Logo URL</label> <div class="form-group logo-upload-section">
<input <label class="form-label">
type="text" <i class="fas fa-image"></i> Logo du marchand (optionnel)
class="form-control" </label>
placeholder="https://exemple.com/logo.png"
[(ngModel)]="newMerchant.logo" <div class="logo-upload-container">
name="logo" <!-- Zone de prévisualisation -->
[disabled]="creatingMerchant" <div class="logo-preview-area" *ngIf="logoPreviewUrl || !selectedLogoFile">
<div class="logo-preview" *ngIf="logoPreviewUrl">
<img [src]="logoPreviewUrl" alt="Prévisualisation du logo">
<button
type="button"
class="btn-remove-preview"
(click)="removeSelectedLogo()"
title="Supprimer"
> >
<i class="fas fa-times"></i>
</button>
</div>
<div class="logo-placeholder" *ngIf="!logoPreviewUrl">
<i class="fas fa-image fa-3x"></i>
<p>Aucun logo sélectionné</p>
</div>
</div>
<!-- Bouton de sélection -->
<div class="logo-upload-actions">
<input
type="file"
id="logoInput"
accept="image/*"
(change)="onLogoSelected($event)"
style="display: none;"
/>
<label for="logoInput" class="btn btn-outline-primary btn-select-logo">
<i class="fas fa-upload"></i>
{{ logoPreviewUrl ? 'Changer le logo' : 'Sélectionner un logo' }}
</label>
<small class="text-muted d-block mt-2">
Formats: JPG, PNG, GIF, WebP, SVG | Taille max: 2MB
</small>
</div>
</div>
<!-- Erreur upload logo -->
<div class="alert alert-danger mt-2" *ngIf="logoUploadError">
<i class="fas fa-exclamation-circle"></i> {{ logoUploadError }}
</div>
<!-- Indicateur d'upload -->
<div class="upload-progress" *ngIf="uploadingLogo">
<div class="spinner-border spinner-border-sm" role="status">
<span class="sr-only">Upload en cours...</span>
</div>
<span class="ml-2">Upload du logo en cours...</span>
</div>
</div>
</div> </div>
<div class="col-12"> <div class="col-12">
@ -517,15 +570,79 @@
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label">Logo URL</label> <div class="form-group logo-edit-section">
<input <label class="form-label">
type="text" <i class="fas fa-image"></i> Logo du marchand
class="form-control" </label>
[(ngModel)]="selectedMerchantForEdit.logo"
name="logo" <div class="logo-edit-container">
[disabled]="updatingMerchant" <!-- Logo actuel ou nouveau -->
placeholder="https://exemple.com/logo.png" <div class="logo-display-area">
<!-- Nouveau logo sélectionné -->
<div class="logo-preview" *ngIf="editLogoPreviewUrl">
<img [src]="editLogoPreviewUrl" alt="Nouveau logo">
<div class="logo-badge badge-new">Nouveau</div>
<button
type="button"
class="btn-remove-preview"
(click)="cancelEditLogo()"
title="Annuler"
> >
<i class="fas fa-times"></i>
</button>
</div>
<!-- Logo actuel -->
<div class="logo-preview" *ngIf="!editLogoPreviewUrl && currentLogoUrl">
<img [src]="currentLogoUrl" alt="Logo actuel">
<div class="logo-badge badge-current">Actuel</div>
</div>
<!-- Pas de logo -->
<div class="logo-placeholder" *ngIf="!editLogoPreviewUrl && !currentLogoUrl">
<i class="fas fa-image fa-3x"></i>
<p>Aucun logo</p>
</div>
</div>
<!-- Actions -->
<div class="logo-edit-actions">
<input
type="file"
id="editLogoInput"
accept="image/*"
(change)="onEditLogoSelected($event)"
style="display: none;"
/>
<label for="editLogoInput" class="btn btn-outline-primary btn-change-logo">
<i class="fas fa-sync-alt"></i>
{{ currentLogoUrl ? 'Changer le logo' : 'Ajouter un logo' }}
</label>
<button
type="button"
class="btn btn-outline-danger btn-remove-logo"
*ngIf="currentLogoUrl && !editLogoPreviewUrl"
(click)="selectedMerchantForEdit!.logo = ''; currentLogoUrl = null"
>
<i class="fas fa-trash"></i>
Supprimer le logo
</button>
<small class="text-muted d-block mt-2">
Formats: JPG, PNG, GIF, WebP, SVG | Taille max: 2MB
</small>
</div>
</div>
<!-- Indicateur d'upload -->
<div class="upload-progress" *ngIf="uploadingLogo">
<div class="spinner-border spinner-border-sm" role="status">
<span class="sr-only">Upload en cours...</span>
</div>
<span class="ml-2">Upload du nouveau logo en cours...</span>
</div>
</div>
</div> </div>
<div class="col-12"> <div class="col-12">

View File

@ -3,7 +3,8 @@ import { CommonModule } from '@angular/common';
import { FormsModule, ReactiveFormsModule, FormBuilder, Validators, FormArray, FormGroup } from '@angular/forms'; import { FormsModule, ReactiveFormsModule, FormBuilder, Validators, FormArray, FormGroup } from '@angular/forms';
import { NgIcon } from '@ng-icons/core'; import { NgIcon } from '@ng-icons/core';
import { NgbNavModule, NgbModal, NgbModalModule } from '@ng-bootstrap/ng-bootstrap'; import { NgbNavModule, NgbModal, NgbModalModule } from '@ng-bootstrap/ng-bootstrap';
import { catchError, finalize, map, of, Subject, takeUntil } from 'rxjs'; import { catchError, finalize, map, Observable, of, Subject, takeUntil, tap } from 'rxjs';
import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
import { MerchantConfigService } from './merchant-config.service'; import { MerchantConfigService } from './merchant-config.service';
import { RoleManagementService } from '@core/services/hub-users-roles-management.service'; import { RoleManagementService } from '@core/services/hub-users-roles-management.service';
@ -13,6 +14,8 @@ import { PageTitle } from '@app/components/page-title/page-title';
import { MerchantConfigsList } from './merchant-config-list/merchant-config-list'; import { MerchantConfigsList } from './merchant-config-list/merchant-config-list';
import { MerchantConfigView } from './merchant-config-view/merchant-config-view'; import { MerchantConfigView } from './merchant-config-view/merchant-config-view';
import { MinioService } from '@core/services/minio.service';
import { import {
CreateMerchantDto, CreateMerchantDto,
MerchantUtils, MerchantUtils,
@ -47,12 +50,17 @@ export class MerchantConfigManagement implements OnInit, OnDestroy {
private modalService = inject(NgbModal); private modalService = inject(NgbModal);
private authService = inject(AuthService); private authService = inject(AuthService);
private merchantConfigService = inject(MerchantConfigService); private merchantConfigService = inject(MerchantConfigService);
private merchantSyncService = inject(MerchantSyncService);
private dataAdapter = inject(MerchantDataAdapter); private dataAdapter = inject(MerchantDataAdapter);
protected roleService = inject(RoleManagementService); protected roleService = inject(RoleManagementService);
private cdRef = inject(ChangeDetectorRef); private cdRef = inject(ChangeDetectorRef);
private destroy$ = new Subject<void>(); private destroy$ = new Subject<void>();
private minioService = inject(MinioService);
private sanitizer = inject(DomSanitizer);
// Cache des URLs de logos
private logoUrlCache = new Map<string, string>();
// Configuration // Configuration
readonly UserRole = UserRole; readonly UserRole = UserRole;
readonly ConfigType = ConfigType; readonly ConfigType = ConfigType;
@ -63,6 +71,20 @@ export class MerchantConfigManagement implements OnInit, OnDestroy {
pageSubtitle: string = 'Administrez les marchands et leurs configurations techniques'; pageSubtitle: string = 'Administrez les marchands et leurs configurations techniques';
badge: any = { icon: 'lucideSettings', text: 'Merchant Management' }; badge: any = { icon: 'lucideSettings', text: 'Merchant Management' };
// ==================== GESTION DES LOGOS ====================
// Logo pour création
selectedLogoFile: File | null = null;
logoPreviewUrl: string | null = null;
uploadingLogo = false;
logoUploadError = '';
// Logo pour édition
editLogoFile: File | null = null;
editLogoPreviewUrl: string | null = null;
currentLogoUrl: string | null = null;
logoChanged = false;
// État de l'interface // État de l'interface
activeTab: 'list' | 'merchant-profile' = 'list'; activeTab: 'list' | 'merchant-profile' = 'list';
selectedMerchantId: number | null = null; selectedMerchantId: number | null = null;
@ -594,6 +616,7 @@ export class MerchantConfigManagement implements OnInit, OnDestroy {
} }
this.resetMerchantForm(); this.resetMerchantForm();
this.removeSelectedLogo();
this.createMerchantError = ''; this.createMerchantError = '';
this.openModal(this.createMerchantModal); this.openModal(this.createMerchantModal);
} }
@ -655,9 +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 ==================== // ==================== 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) { if (!this.canCreateMerchants) {
this.createMerchantError = 'Vous n\'avez pas la permission de créer des marchands'; this.createMerchantError = 'Vous n\'avez pas la permission de créer des marchands';
return; return;
@ -672,7 +811,41 @@ export class MerchantConfigManagement implements OnInit, OnDestroy {
this.creatingMerchant = true; this.creatingMerchant = true;
this.createMerchantError = ''; 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); const createDto = this.convertMerchantToBackend(this.newMerchant);
console.log('📤 Creating merchant:', createDto); console.log('📤 Creating merchant:', createDto);
@ -681,13 +854,17 @@ export class MerchantConfigManagement implements OnInit, OnDestroy {
.pipe(takeUntil(this.destroy$)) .pipe(takeUntil(this.destroy$))
.subscribe({ .subscribe({
next: (createdMerchant) => { next: (createdMerchant) => {
// Conversion de la réponse pour Angular
const frontendMerchant = this.convertMerchantToFrontend(createdMerchant); const frontendMerchant = this.convertMerchantToFrontend(createdMerchant);
console.log('✅ Merchant created successfully:', frontendMerchant); console.log('✅ Merchant created successfully:', frontendMerchant);
this.creatingMerchant = false; this.creatingMerchant = false;
this.modalService.dismissAll(); this.modalService.dismissAll();
this.refreshMerchantsList(); this.refreshMerchantsList();
// Reset le formulaire et le logo
this.resetMerchantForm();
this.removeSelectedLogo();
this.cdRef.detectChanges(); this.cdRef.detectChanges();
}, },
error: (error) => { error: (error) => {
@ -699,14 +876,69 @@ export class MerchantConfigManagement implements OnInit, OnDestroy {
}); });
} }
// Mise à jour COMPLÈTE du merchant // ==================== GESTION DU LOGO LORS DE L'ÉDITION ====================
updateMerchant(): void {
/**
* 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) { if (!this.selectedMerchantForEdit) {
this.updateMerchantError = 'Aucun marchand sélectionné pour modification'; this.updateMerchantError = 'Aucun marchand sélectionné pour modification';
return; return;
} }
// Validation des données complètes
const validation = this.validateMerchantUpdate(this.selectedMerchantForEdit); const validation = this.validateMerchantUpdate(this.selectedMerchantForEdit);
if (!validation.isValid) { if (!validation.isValid) {
this.updateMerchantError = validation.errors.join(', '); this.updateMerchantError = validation.errors.join(', ');
@ -716,9 +948,56 @@ export class MerchantConfigManagement implements OnInit, OnDestroy {
this.updatingMerchant = true; this.updatingMerchant = true;
this.updateMerchantError = ''; this.updateMerchantError = '';
// Conversion pour l'API avec TOUTES les données // Si un nouveau logo est sélectionné
if (this.editLogoFile && this.logoChanged) {
this.uploadingLogo = true;
const merchantId = this.selectedMerchantForEdit.id!; const merchantId = this.selectedMerchantForEdit.id!;
const updateDto = this.convertUpdateMerchantToBackend(this.selectedMerchantForEdit, this.selectedMerchantForEdit);
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); console.log('📤 Updating merchant with full data:', updateDto);
@ -726,7 +1005,6 @@ export class MerchantConfigManagement implements OnInit, OnDestroy {
.pipe(takeUntil(this.destroy$)) .pipe(takeUntil(this.destroy$))
.subscribe({ .subscribe({
next: (updatedMerchant) => { next: (updatedMerchant) => {
// Conversion pour Angular
const frontendMerchant = this.convertMerchantToFrontend(updatedMerchant); const frontendMerchant = this.convertMerchantToFrontend(updatedMerchant);
this.updatingMerchant = false; this.updatingMerchant = false;
@ -737,15 +1015,22 @@ export class MerchantConfigManagement implements OnInit, OnDestroy {
// Mettre à jour le cache // Mettre à jour le cache
if (this.selectedMerchantId) { if (this.selectedMerchantId) {
this.merchantProfiles[this.selectedMerchantId] = frontendMerchant; 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) { if (this.isMerchantUser && this.userMerchantId === merchantId) {
this.userMerchant = frontendMerchant; this.userMerchant = frontendMerchant;
} }
this.successMessage = 'Marchand modifié avec succès'; this.successMessage = 'Marchand modifié avec succès';
// Reset les états du logo
this.cancelEditLogo();
this.cdRef.detectChanges(); this.cdRef.detectChanges();
}, },
error: (error) => { 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<string> {
// 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 // Validation complète pour la mise à jour
validateMerchantUpdate(merchant: UpdateMerchantDto): { isValid: boolean; errors: string[] } { validateMerchantUpdate(merchant: UpdateMerchantDto): { isValid: boolean; errors: string[] } {
const errors: string[] = []; const errors: string[] = [];
@ -891,17 +1217,28 @@ export class MerchantConfigManagement implements OnInit, OnDestroy {
}; };
} }
private resetMerchantForm(): void {
this.newMerchant = this.getDefaultMerchantForm();
console.log('🔄 Merchant form reset');
}
/**
* Override de populateEditForm pour charger le logo
*/
private populateEditForm(merchant: Merchant): void { private populateEditForm(merchant: Merchant): void {
this.selectedMerchantForEdit = { this.selectedMerchantForEdit = {
...merchant, ...merchant,
configs: merchant.configs.map(config => ({ ...config })), configs: merchant.configs.map(config => ({ ...config })),
technicalContacts: merchant.technicalContacts.map(contact => ({ ...contact })) technicalContacts: merchant.technicalContacts.map(contact => ({ ...contact }))
}; };
// Charger le logo actuel
this.loadMerchantLogoForEdit(merchant);
this.editLogoFile = null;
this.editLogoPreviewUrl = null;
this.logoChanged = false;
}
private resetMerchantForm(): void {
this.newMerchant = this.getDefaultMerchantForm();
this.removeSelectedLogo();
console.log('🔄 Merchant form reset');
} }
private refreshMerchantsList(): void { private refreshMerchantsList(): void {
@ -1025,8 +1362,14 @@ export class MerchantConfigManagement implements OnInit, OnDestroy {
return !this.isMerchantUser && this.activeTab === 'list'; return !this.isMerchantUser && this.activeTab === 'list';
} }
// ==================== NETTOYAGE ====================
ngOnDestroy(): void { ngOnDestroy(): void {
this.destroy$.next(); this.destroy$.next();
this.destroy$.complete(); this.destroy$.complete();
// Nettoyer les logos
this.cleanupLogoResources();
this.logoUrlCache.clear();
} }
} }

View File

@ -2,7 +2,7 @@ export const environment = {
production: true, production: true,
localServiceTestApiUrl: "https://backoffice.dcb.pixpay.sn/api/v1", localServiceTestApiUrl: "https://backoffice.dcb.pixpay.sn/api/v1",
iamApiUrl: "https://api-user-service.dcb.pixpay.sn/api/v1", iamApiUrl: "https://api-user-service.dcb.pixpay.sn/api/v1",
configApiUrl: "https://api-merchant-config-service.dcb.pixpay.sn/api/v1", configApiUrl: 'https://api-merchant-config-service.dcb.pixpay.sn/api/v1',
apiCoreUrl: "https://api-core-service.dcb.pixpay.sn/api/v1", apiCoreUrl: 'https://api-core-service.dcb.pixpay.sn/api/v1',
reportingApiUrl: "https://api-reporting-service.dcb.pixpay.sn/api/v1/", reportingApiUrl: 'https://api-reporting-service.dcb.pixpay.sn/api/v1/'
}; };

View File

@ -4,5 +4,5 @@ export const environment = {
iamApiUrl: "https://api-user-service.dcb.pixpay.sn/api/v1", iamApiUrl: "https://api-user-service.dcb.pixpay.sn/api/v1",
configApiUrl: "https://api-merchant-config-service.dcb.pixpay.sn/api/v1", configApiUrl: "https://api-merchant-config-service.dcb.pixpay.sn/api/v1",
apiCoreUrl: "https://api-core-service.dcb.pixpay.sn/api/v1", apiCoreUrl: "https://api-core-service.dcb.pixpay.sn/api/v1",
reportingApiUrl: "https://api-reporting-service.dcb.pixpay.sn/api/v1/", reportingApiUrl: "https://api-reporting-service.dcb.pixpay.sn/api/v1/"
}; };

View File

@ -4,5 +4,5 @@ export const environment = {
iamApiUrl: "http://localhost:3001/api/v1", iamApiUrl: "http://localhost:3001/api/v1",
configApiUrl: "http://localhost:3000/api/v1", configApiUrl: "http://localhost:3000/api/v1",
apiCoreUrl: "https://api-core-service.dcb.pixpay.sn/api/v1", apiCoreUrl: "https://api-core-service.dcb.pixpay.sn/api/v1",
reportingApiUrl: "https://api-reporting-service.dcb.pixpay.sn/api/v1/", reportingApiUrl: "https://api-reporting-service.dcb.pixpay.sn/api/v1/"
} }