Giant Update regsiter
NPM Installation / build (16.x, ubuntu-latest) (push) Waiting to run
NPM Installation / build (16.x, windows-latest) (push) Waiting to run
NPM Installation / build (17.x, ubuntu-latest) (push) Waiting to run
NPM Installation / build (17.x, windows-latest) (push) Waiting to run
NPM Installation / build (18.x, ubuntu-latest) (push) Waiting to run
NPM Installation / build (18.x, windows-latest) (push) Waiting to run
NPM Installation / build (16.x, ubuntu-latest) (push) Waiting to run
NPM Installation / build (16.x, windows-latest) (push) Waiting to run
NPM Installation / build (17.x, ubuntu-latest) (push) Waiting to run
NPM Installation / build (17.x, windows-latest) (push) Waiting to run
NPM Installation / build (18.x, ubuntu-latest) (push) Waiting to run
NPM Installation / build (18.x, windows-latest) (push) Waiting to run
This commit is contained in:
Generated
+264
-4
@@ -23,6 +23,7 @@
|
||||
"chart.js": "^4.5.1",
|
||||
"maplibre-gl": "^3.6.2",
|
||||
"pinia": "^3.0.4",
|
||||
"qrcode": "^1.5.4",
|
||||
"simplebar-vue": "^2.4.2",
|
||||
"sweetalert2": "^11.26.25",
|
||||
"vue": "^3.5.31",
|
||||
@@ -1520,11 +1521,19 @@
|
||||
"url": "https://github.com/sponsors/epoberezkin"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
@@ -1807,6 +1816,15 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/camelcase": {
|
||||
"version": "5.3.1",
|
||||
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
|
||||
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001799",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz",
|
||||
@@ -1872,11 +1890,21 @@
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
|
||||
"integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"string-width": "^4.2.0",
|
||||
"strip-ansi": "^6.0.0",
|
||||
"wrap-ansi": "^6.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-name": "~1.1.4"
|
||||
@@ -1889,7 +1917,6 @@
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
@@ -1983,6 +2010,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/decamelize": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
|
||||
"integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/deep-is": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
|
||||
@@ -2009,6 +2045,12 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/dijkstrajs": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
|
||||
"integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
@@ -2036,6 +2078,12 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/entities": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
|
||||
@@ -2494,6 +2542,15 @@
|
||||
"integrity": "sha512-EvGQQi/zPrDA6zr6BnJD/YhwAkBP8nnJ9emh3EnHQKVMfg/MRVtPbMYdgVy/IaEmn4UfagD2a6fafPDL5hbtwg==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/get-caller-file": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "6.* || 8.* || >= 10.*"
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
@@ -2795,6 +2852,15 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/is-glob": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
||||
@@ -3548,6 +3614,15 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/p-try": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
|
||||
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/parent-module": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
|
||||
@@ -3565,7 +3640,6 @@
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -3656,6 +3730,15 @@
|
||||
"pathe": "^2.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/pngjs": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
|
||||
"integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.15",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
|
||||
@@ -3746,6 +3829,23 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/qrcode": {
|
||||
"version": "1.5.4",
|
||||
"resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz",
|
||||
"integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dijkstrajs": "^1.0.1",
|
||||
"pngjs": "^5.0.0",
|
||||
"yargs": "^15.3.1"
|
||||
},
|
||||
"bin": {
|
||||
"qrcode": "bin/qrcode"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/quansync": {
|
||||
"version": "0.2.11",
|
||||
"resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz",
|
||||
@@ -3781,6 +3881,21 @@
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/require-directory": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/require-main-filename": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
|
||||
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/resolve-from": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
|
||||
@@ -3886,6 +4001,12 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/set-blocking": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
|
||||
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/set-value": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz",
|
||||
@@ -4063,6 +4184,32 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-json-comments": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
|
||||
@@ -4515,6 +4662,12 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/which-module": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
|
||||
"integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/word-wrap": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
|
||||
@@ -4525,6 +4678,20 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
|
||||
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/xml-name-validator": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz",
|
||||
@@ -4535,6 +4702,12 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/y18n": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
|
||||
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/yaml": {
|
||||
"version": "2.9.0",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz",
|
||||
@@ -4550,6 +4723,93 @@
|
||||
"url": "https://github.com/sponsors/eemeli"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs": {
|
||||
"version": "15.4.1",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
|
||||
"integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cliui": "^6.0.0",
|
||||
"decamelize": "^1.2.0",
|
||||
"find-up": "^4.1.0",
|
||||
"get-caller-file": "^2.0.1",
|
||||
"require-directory": "^2.1.1",
|
||||
"require-main-filename": "^2.0.0",
|
||||
"set-blocking": "^2.0.0",
|
||||
"string-width": "^4.2.0",
|
||||
"which-module": "^2.0.0",
|
||||
"y18n": "^4.0.0",
|
||||
"yargs-parser": "^18.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs-parser": {
|
||||
"version": "18.1.3",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
|
||||
"integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"camelcase": "^5.0.0",
|
||||
"decamelize": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs/node_modules/find-up": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
|
||||
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"locate-path": "^5.0.0",
|
||||
"path-exists": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs/node_modules/locate-path": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
|
||||
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"p-locate": "^4.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs/node_modules/p-limit": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
|
||||
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"p-try": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs/node_modules/p-locate": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
|
||||
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"p-limit": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/yocto-queue": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
"chart.js": "^4.5.1",
|
||||
"maplibre-gl": "^3.6.2",
|
||||
"pinia": "^3.0.4",
|
||||
"qrcode": "^1.5.4",
|
||||
"simplebar-vue": "^2.4.2",
|
||||
"sweetalert2": "^11.26.25",
|
||||
"vue": "^3.5.31",
|
||||
|
||||
@@ -43,6 +43,7 @@ import {
|
||||
cilCode,
|
||||
cilCommentSquare,
|
||||
cilContrast,
|
||||
cilCopy,
|
||||
cilCreditCard,
|
||||
cilCursor,
|
||||
cilDataTransferDown,
|
||||
@@ -55,6 +56,7 @@ import {
|
||||
cilExternalLink,
|
||||
cilGlobeAlt,
|
||||
cilGrid,
|
||||
cilInfo,
|
||||
cilFile,
|
||||
cilJustifyCenter,
|
||||
cilLaptop,
|
||||
@@ -114,6 +116,7 @@ export const iconsSet = Object.assign(
|
||||
cilCode,
|
||||
cilCommentSquare,
|
||||
cilContrast,
|
||||
cilCopy,
|
||||
cilCreditCard,
|
||||
cilCursor,
|
||||
cilDataTransferDown,
|
||||
@@ -126,6 +129,7 @@ export const iconsSet = Object.assign(
|
||||
cilExternalLink,
|
||||
cilGlobeAlt,
|
||||
cilGrid,
|
||||
cilInfo,
|
||||
cilFile,
|
||||
cilJustifyCenter,
|
||||
cilLaptop,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { cilList, cilViewModule } from '@coreui/icons'
|
||||
import { useColorModes } from '@coreui/vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import AppBreadcrumb from '@/components/AppBreadcrumb.vue'
|
||||
import AppHeaderDropdownAccnt from '@/components/AppHeaderDropdownAccnt.vue'
|
||||
@@ -16,6 +17,7 @@ import { getMyNotifications, markNotificationRead } from '@/services/notificatio
|
||||
const headerClassNames = ref('mb-2 p-0')
|
||||
const { colorMode, setColorMode } = useColorModes('coreui-free-vue-admin-template-theme')
|
||||
const sidebar = useSidebarStore()
|
||||
const router = useRouter()
|
||||
const viewMode = ref(getResponsiveViewMode())
|
||||
const appNotifications = ref([])
|
||||
const unreadCount = computed(() => appNotifications.value.filter((item) => !item.read_at).length)
|
||||
@@ -51,7 +53,10 @@ const openNotification = async (item) => {
|
||||
await markNotificationRead(item.uuid)
|
||||
item.read_at = new Date().toISOString()
|
||||
}
|
||||
if (item.action_url) window.location.assign(item.action_url)
|
||||
if (item.action_url) {
|
||||
if (item.action_url.startsWith('/')) await router.push(item.action_url)
|
||||
else window.location.assign(item.action_url)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
@@ -74,27 +79,40 @@ onBeforeUnmount(() => {
|
||||
</CHeaderToggler>
|
||||
<CHeaderNav class="d-none d-md-flex">
|
||||
<CNavItem>
|
||||
<CNavLink href="/dashboard"> Dashboard </CNavLink>
|
||||
</CNavItem>
|
||||
<CNavItem>
|
||||
<CNavLink href="#">Users</CNavLink>
|
||||
</CNavItem>
|
||||
<CNavItem>
|
||||
<CNavLink href="#">Settings</CNavLink>
|
||||
<CNavLink href="#/dashboard"> Dashboard </CNavLink>
|
||||
</CNavItem>
|
||||
</CHeaderNav>
|
||||
<CHeaderNav class="ms-auto">
|
||||
<CDropdown variant="nav-item" placement="bottom-end">
|
||||
<CDropdownToggle :caret="false" class="position-relative">
|
||||
<CIcon icon="cil-bell" size="lg" />
|
||||
<CBadge v-if="unreadCount" color="danger" shape="rounded-pill" class="header-notification-badge">{{ unreadCount > 9 ? '9+' : unreadCount }}</CBadge>
|
||||
<CBadge
|
||||
v-if="unreadCount"
|
||||
color="danger"
|
||||
shape="rounded-pill"
|
||||
class="header-notification-badge"
|
||||
>{{ unreadCount > 9 ? '9+' : unreadCount }}</CBadge
|
||||
>
|
||||
</CDropdownToggle>
|
||||
<CDropdownMenu class="header-notification-menu">
|
||||
<div class="px-3 py-2 border-bottom"><strong>Notifikasi</strong><small class="d-block text-body-secondary">{{ unreadCount }} belum dibaca</small></div>
|
||||
<button v-for="item in appNotifications" :key="item.uuid" type="button" class="header-notification-item" :class="{ unread: !item.read_at }" @click="openNotification(item)">
|
||||
<strong>{{ item.title }}</strong><span>{{ item.body }}</span>
|
||||
<div class="px-3 py-2 border-bottom">
|
||||
<strong>Notifikasi</strong
|
||||
><small class="d-block text-body-secondary">{{ unreadCount }} belum dibaca</small>
|
||||
</div>
|
||||
<button
|
||||
v-for="item in appNotifications"
|
||||
:key="item.uuid"
|
||||
type="button"
|
||||
class="header-notification-item"
|
||||
:class="{ unread: !item.read_at }"
|
||||
@click="openNotification(item)"
|
||||
>
|
||||
<strong>{{ item.title }}</strong
|
||||
><span>{{ item.body }}</span>
|
||||
</button>
|
||||
<div v-if="!appNotifications.length" class="px-3 py-3 text-body-secondary text-center">Belum ada notifikasi</div>
|
||||
<div v-if="!appNotifications.length" class="px-3 py-3 text-body-secondary text-center">
|
||||
Belum ada notifikasi
|
||||
</div>
|
||||
</CDropdownMenu>
|
||||
</CDropdown>
|
||||
<CNavItem>
|
||||
@@ -167,10 +185,41 @@ onBeforeUnmount(() => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.header-notification-badge { position: absolute; top: .15rem; right: .05rem; font-size: .6rem; }
|
||||
.header-notification-menu { width: min(22rem, calc(100vw - 1.5rem)); max-height: 28rem; overflow-y: auto; }
|
||||
.header-notification-item { display: flex; width: 100%; flex-direction: column; gap: .2rem; padding: .7rem 1rem; text-align: left; background: transparent; border: 0; border-bottom: 1px solid var(--cui-border-color); }
|
||||
.header-notification-item:hover { background: var(--cui-tertiary-bg); }
|
||||
.header-notification-item.unread { border-left: 3px solid var(--cui-primary); background: rgba(var(--cui-primary-rgb), .08); }
|
||||
.header-notification-item span { display: -webkit-box; overflow: hidden; color: var(--cui-secondary-color); font-size: .78rem; -webkit-box-orient: vertical; -webkit-line-clamp: 2; }
|
||||
.header-notification-badge {
|
||||
position: absolute;
|
||||
top: 0.15rem;
|
||||
right: 0.05rem;
|
||||
font-size: 0.6rem;
|
||||
}
|
||||
.header-notification-menu {
|
||||
width: min(22rem, calc(100vw - 1.5rem));
|
||||
max-height: 28rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.header-notification-item {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
gap: 0.2rem;
|
||||
padding: 0.7rem 1rem;
|
||||
text-align: left;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-bottom: 1px solid var(--cui-border-color);
|
||||
}
|
||||
.header-notification-item:hover {
|
||||
background: var(--cui-tertiary-bg);
|
||||
}
|
||||
.header-notification-item.unread {
|
||||
border-left: 3px solid var(--cui-primary);
|
||||
background: rgba(var(--cui-primary-rgb), 0.08);
|
||||
}
|
||||
.header-notification-item span {
|
||||
display: -webkit-box;
|
||||
overflow: hidden;
|
||||
color: var(--cui-secondary-color);
|
||||
font-size: 0.78rem;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -43,6 +43,10 @@ function openProfile() {
|
||||
router.push('/profile')
|
||||
}
|
||||
|
||||
function openMyApplications() {
|
||||
router.push('/my-applications')
|
||||
}
|
||||
|
||||
function onLogout() {
|
||||
authStore.logout()
|
||||
router.push('/pages/login')
|
||||
@@ -97,6 +101,9 @@ function onLogout() {
|
||||
Settings
|
||||
</CDropdownHeader>
|
||||
<CDropdownItem @click="openProfile"> <CIcon icon="cil-user" /> Profile </CDropdownItem>
|
||||
<CDropdownItem @click="openMyApplications">
|
||||
<CIcon icon="cil-user-follow" /> Pengajuan Saya
|
||||
</CDropdownItem>
|
||||
<CDropdownItem> <CIcon icon="cil-settings" /> Settings </CDropdownItem>
|
||||
<CDropdownItem>
|
||||
<CIcon icon="cil-dollar" /> Payments
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { RouterLink } from 'vue-router'
|
||||
|
||||
// import { logo } from '@/assets/brand/logo'
|
||||
@@ -6,9 +7,24 @@ import logo from '@/assets/images/Logo_big.png'
|
||||
import { sygnet } from '@/assets/brand/sygnet'
|
||||
import { AppSidebarNav } from '@/components/AppSidebarNav.js'
|
||||
import WalletHeaderWidget from '@/components/WalletHeaderWidget.vue'
|
||||
import { useMenuStore } from '@/stores/menu.js'
|
||||
import { useSidebarStore } from '@/stores/sidebar.js'
|
||||
|
||||
const sidebar = useSidebarStore()
|
||||
const menuStore = useMenuStore()
|
||||
|
||||
function containsWalletMenu(items = []) {
|
||||
return items.some(
|
||||
(item) =>
|
||||
item?.slug === 'wallet' ||
|
||||
item?.url === '/deposit-balance' ||
|
||||
containsWalletMenu(Array.isArray(item?.children) ? item.children : []),
|
||||
)
|
||||
}
|
||||
|
||||
const canViewWallet = computed(
|
||||
() => menuStore.initialized && containsWalletMenu(menuStore.dynamicMenus),
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -47,7 +63,7 @@ const sidebar = useSidebarStore()
|
||||
</RouterLink>
|
||||
<CCloseButton class="d-lg-none" dark @click="sidebar.toggleVisible()" />
|
||||
</CSidebarHeader>
|
||||
<div class="sidebar-wallet-slot">
|
||||
<div v-if="canViewWallet" class="sidebar-wallet-slot">
|
||||
<WalletHeaderWidget />
|
||||
</div>
|
||||
<AppSidebarNav />
|
||||
|
||||
@@ -20,10 +20,10 @@ onBeforeUnmount(() => window.clearInterval(timer))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button type="button" class="wallet-header" title="Buka dompet" @click="router.push('/wallet')">
|
||||
<button type="button" class="wallet-header" title="Buka saldo" @click="router.push('/deposit-balance')">
|
||||
<CIcon icon="cil-wallet" size="lg" />
|
||||
<span v-if="active"><small>{{ active.wallet_type === 'tenant' ? 'Dompet Tenant' : 'Dompet Saya' }}</small><strong>{{ money(active.available_balance) }}</strong></span>
|
||||
<span v-else><small>Dompet</small><strong>Rp 0</strong></span>
|
||||
<span v-if="active"><small>{{ active.label || 'Saldo Deposit' }}</small><strong>{{ money(active.available_balance) }}</strong></span>
|
||||
<span v-else><small>Saldo Deposit</small><strong>Rp 0</strong></span>
|
||||
<i v-if="wallets.length > 1">{{ activeIndex + 1 }}/{{ wallets.length }}</i>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
<script setup>
|
||||
import { ref, nextTick, watch, onMounted, onBeforeUnmount } from 'vue'
|
||||
import Swal from 'sweetalert2'
|
||||
import { showSuccessr, showError } from '@/utils/swal'
|
||||
import { showError } from '@/utils/swal'
|
||||
|
||||
const props = defineProps({ visible: Boolean, form: Object, saving: Boolean })
|
||||
const emit = defineEmits(['close', 'save'])
|
||||
|
||||
+77
-4
@@ -37,7 +37,13 @@ import { hasToken } from '@/utils/session'
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'Home',
|
||||
name: 'Website',
|
||||
component: () => import('@/views/website/LandingPage.vue'),
|
||||
meta: { guestLanding: true },
|
||||
},
|
||||
{
|
||||
path: '/app',
|
||||
name: 'Application',
|
||||
component: DefaultLayout,
|
||||
redirect: '/dashboard',
|
||||
meta: {
|
||||
@@ -58,6 +64,18 @@ const routes = [
|
||||
name: 'Profile',
|
||||
component: () => import('@/views/profile/Profile.vue'),
|
||||
},
|
||||
{
|
||||
path: '/forbidden',
|
||||
name: 'Forbidden',
|
||||
component: () => import('@/views/pages/Page403.vue'),
|
||||
meta: { publicWithinApp: true },
|
||||
},
|
||||
{
|
||||
path: '/my-applications',
|
||||
name: 'Pengajuan Saya',
|
||||
component: () => import('@/views/access-applications/MyApplications.vue'),
|
||||
meta: { publicWithinApp: true },
|
||||
},
|
||||
{
|
||||
path: '/tickets',
|
||||
name: 'Tickets',
|
||||
@@ -539,9 +557,44 @@ const routes = [
|
||||
component: () => import('@/views/payment-gateway/PaymentLogs.vue'),
|
||||
},
|
||||
{
|
||||
path: '/wallet',
|
||||
name: 'Dompet',
|
||||
path: '/deposit-balance',
|
||||
alias: '/wallet',
|
||||
name: 'Saldo Deposit',
|
||||
component: () => import('@/views/wallet/WalletDashboard.vue'),
|
||||
meta: { activeMenu: '/deposit-balance' },
|
||||
},
|
||||
{
|
||||
path: '/voucher-hotspot/sales',
|
||||
name: 'Jual Voucher Hotspot',
|
||||
component: () => import('@/views/voucher-hotspot/VoucherSales.vue'),
|
||||
},
|
||||
{
|
||||
path: '/voucher-hotspot/agents',
|
||||
name: 'Agen Voucher',
|
||||
component: () => import('@/views/voucher-hotspot/VoucherAgents.vue'),
|
||||
},
|
||||
{
|
||||
path: '/voucher-hotspot/login-page',
|
||||
name: 'Voucher LoginPage',
|
||||
component: () => import('@/views/voucher-hotspot/VoucherLoginPage.vue'),
|
||||
},
|
||||
{
|
||||
path: '/access-applications/tenants',
|
||||
name: 'Pengajuan Tenant',
|
||||
component: () => import('@/views/access-applications/ApplicationReviews.vue'),
|
||||
meta: { applicationType: 'tenant' },
|
||||
},
|
||||
{
|
||||
path: '/access-applications/agents',
|
||||
name: 'Pengajuan Agen',
|
||||
component: () => import('@/views/access-applications/ApplicationReviews.vue'),
|
||||
meta: { applicationType: 'agent' },
|
||||
},
|
||||
{
|
||||
path: '/access-applications/staff',
|
||||
name: 'Pengajuan Staff',
|
||||
component: () => import('@/views/access-applications/ApplicationReviews.vue'),
|
||||
meta: { applicationType: 'staff' },
|
||||
},
|
||||
{
|
||||
path: '/users',
|
||||
@@ -661,7 +714,27 @@ router.beforeEach(async (to) => {
|
||||
|
||||
if (requiresAuth) {
|
||||
try {
|
||||
await useMenuStore().fetchMenus()
|
||||
const menuStore = useMenuStore()
|
||||
await menuStore.fetchMenus()
|
||||
|
||||
const publicWithinApp = to.matched.some((record) => record.meta.publicWithinApp)
|
||||
const alwaysAllowed = ['/dashboard', '/profile', '/forbidden', '/my-applications']
|
||||
|
||||
if (!publicWithinApp && !alwaysAllowed.includes(to.path)) {
|
||||
let accessUrl = to.meta.activeMenu || to.path
|
||||
|
||||
if (to.meta.activeMenuBase && to.params.scope) {
|
||||
accessUrl = `${to.meta.activeMenuBase}/${to.params.scope}`
|
||||
}
|
||||
|
||||
if (!menuStore.canAccessUrl(accessUrl)) {
|
||||
return {
|
||||
name: 'Forbidden',
|
||||
query: { from: to.fullPath },
|
||||
replace: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
useAuthStore().logout()
|
||||
|
||||
|
||||
@@ -25,6 +25,35 @@ export async function updateNotificationChannel(channel, id, payload) {
|
||||
export async function testNotificationChannel(channel, id) {
|
||||
return (await api.post(`${channelBase(channel)}/settings/${id}/test`)).data
|
||||
}
|
||||
export async function deleteNotificationChannel(channel, id) {
|
||||
return (await api.delete(`${channelBase(channel)}/settings/${id}`)).data
|
||||
}
|
||||
export async function getSystemNotificationCampaigns(params = {}) {
|
||||
return (await api.get('/notifications/system/campaigns', { params })).data
|
||||
}
|
||||
export async function createSystemNotificationCampaign(payload) {
|
||||
return (await api.post('/notifications/system/campaigns', payload)).data
|
||||
}
|
||||
export async function getSystemAudienceOptions(params = {}) {
|
||||
return (await api.get('/notifications/system/audience-options', { params })).data
|
||||
}
|
||||
export async function getWhapiState(id) {
|
||||
return (await api.get(`/notifications/channels/whatsapp-unofficial/settings/${id}/state`)).data
|
||||
}
|
||||
export async function startWhapi(id) {
|
||||
return (await api.post(`/notifications/channels/whatsapp-unofficial/settings/${id}/start`)).data
|
||||
}
|
||||
export async function getWhapiQr(id) {
|
||||
return (await api.get(`/notifications/channels/whatsapp-unofficial/settings/${id}/qr`)).data
|
||||
}
|
||||
export async function sendWhapiTestMessage(id, payload) {
|
||||
return (
|
||||
await api.post(
|
||||
`/notifications/channels/whatsapp-unofficial/settings/${id}/test-message`,
|
||||
payload,
|
||||
)
|
||||
).data
|
||||
}
|
||||
export async function getNotificationTemplates(channel) {
|
||||
return (await api.get(`${channelBase(channel)}/templates`)).data
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import api from '@/api/axios'
|
||||
|
||||
export const getMyRoleApplications = async () => (await api.get('/me/role-applications')).data
|
||||
export const createRoleApplication = async (payload) =>
|
||||
(await api.post('/me/role-applications', payload)).data
|
||||
export const getRoleApplications = async (type, params = {}) =>
|
||||
(await api.get(`/role-applications/${type}`, { params })).data
|
||||
export const getTenantMenuGroupOptions = async () =>
|
||||
(await api.get('/role-applications/tenant/menu-group-options')).data
|
||||
export const getStaffMenuGroupOptions = async () =>
|
||||
(await api.get('/role-applications/staff/menu-group-options')).data
|
||||
export const reviewRoleApplication = async (id, payload) =>
|
||||
(await api.put(`/role-applications/${id}/review`, payload)).data
|
||||
export const linkUserCustomer = async (payload) =>
|
||||
(await api.post('/customer-user-links', payload)).data
|
||||
@@ -0,0 +1,12 @@
|
||||
import api from '@/api/axios'
|
||||
|
||||
export const getVoucherOptions = async (scope = 'sales') => (await api.get(`/voucher-hotspot/${scope === 'agents' ? 'agent-options' : scope === 'login-page' ? 'login-page-options' : 'sales-options'}`)).data
|
||||
export const getVoucherSales = async (params = {}) => (await api.get('/voucher-hotspot/sales', { params })).data
|
||||
export const getVoucherSale = async (id) => (await api.get(`/voucher-hotspot/sales/${id}`)).data
|
||||
export const createVoucherSale = async (payload) => (await api.post('/voucher-hotspot/sales', payload)).data
|
||||
export const getVoucherAgents = async (params = {}) => (await api.get('/voucher-hotspot/agents', { params })).data
|
||||
export const createVoucherAgent = async (payload) => (await api.post('/voucher-hotspot/agents', payload)).data
|
||||
export const updateVoucherAgent = async (id, payload) => (await api.put(`/voucher-hotspot/agents/${id}`, payload)).data
|
||||
export const deleteVoucherAgent = async (id) => (await api.delete(`/voucher-hotspot/agents/${id}`)).data
|
||||
export const getVoucherLoginPage = async () => (await api.get('/voucher-hotspot/login-page')).data
|
||||
export const saveVoucherLoginPage = async (payload) => (await api.put('/voucher-hotspot/login-page', payload)).data
|
||||
@@ -2,5 +2,4 @@ import api from '@/api/axios'
|
||||
|
||||
export const getMyWallets = async () => (await api.get('/wallets')).data
|
||||
export const getWalletLedger = async (id, params = {}) => (await api.get(`/wallets/${id}/ledger`, { params })).data
|
||||
export const transferWallet = async (id, payload) => (await api.post(`/wallets/${id}/transfer`, payload)).data
|
||||
export const withdrawWallet = async (id, payload) => (await api.post(`/wallets/${id}/withdraw`, payload)).data
|
||||
|
||||
@@ -46,6 +46,8 @@ export const useAuthStore = defineStore('auth', {
|
||||
pendingVerificationEmail: savedVerification.verification_target || '',
|
||||
verificationChannel: savedVerification.verification_channel || 'email',
|
||||
verificationTarget: savedVerification.verification_target || '',
|
||||
verificationCode: savedVerification.verification_code || '',
|
||||
verificationCodeExpiresAt: savedVerification.expires_at || null,
|
||||
}
|
||||
},
|
||||
|
||||
@@ -110,10 +112,15 @@ export const useAuthStore = defineStore('auth', {
|
||||
this.verificationChannel = payload?.verification_channel || 'email'
|
||||
this.verificationTarget = payload?.verification_target || payload?.email || ''
|
||||
this.pendingVerificationEmail = this.verificationTarget
|
||||
const verification = data?.data?.verification || data?.verification || {}
|
||||
this.verificationCode = verification?.code || ''
|
||||
this.verificationCodeExpiresAt = verification?.expires_at || null
|
||||
|
||||
setVerificationContext({
|
||||
verification_channel: this.verificationChannel,
|
||||
verification_target: this.verificationTarget,
|
||||
verification_code: this.verificationCode,
|
||||
expires_at: this.verificationCodeExpiresAt,
|
||||
})
|
||||
|
||||
return data
|
||||
@@ -141,6 +148,8 @@ export const useAuthStore = defineStore('auth', {
|
||||
this.pendingVerificationEmail = ''
|
||||
this.verificationChannel = 'email'
|
||||
this.verificationTarget = ''
|
||||
this.verificationCode = ''
|
||||
this.verificationCodeExpiresAt = null
|
||||
clearVerificationContext()
|
||||
|
||||
return data
|
||||
@@ -158,6 +167,16 @@ export const useAuthStore = defineStore('auth', {
|
||||
|
||||
try {
|
||||
const data = await resendVerificationCodeApi(payload)
|
||||
const verification = data?.data?.verification || data?.verification || {}
|
||||
this.verificationCode = verification?.code || ''
|
||||
this.verificationCodeExpiresAt = verification?.expires_at || null
|
||||
|
||||
setVerificationContext({
|
||||
verification_channel: payload?.verification_channel || this.verificationChannel,
|
||||
verification_target: payload?.verification_target || this.verificationTarget,
|
||||
verification_code: this.verificationCode,
|
||||
expires_at: this.verificationCodeExpiresAt,
|
||||
})
|
||||
return data
|
||||
} catch (error) {
|
||||
this.error = extractErrorMessage(error)
|
||||
@@ -176,6 +195,8 @@ export const useAuthStore = defineStore('auth', {
|
||||
this.pendingVerificationEmail = ''
|
||||
this.verificationChannel = 'email'
|
||||
this.verificationTarget = ''
|
||||
this.verificationCode = ''
|
||||
this.verificationCodeExpiresAt = null
|
||||
clearVerificationContext()
|
||||
},
|
||||
},
|
||||
|
||||
@@ -36,6 +36,13 @@ function mapMenuItem(item, isChild = false) {
|
||||
}
|
||||
}
|
||||
|
||||
function flattenMenus(items = []) {
|
||||
return items.flatMap((item) => [
|
||||
item,
|
||||
...flattenMenus(Array.isArray(item?.children) ? item.children : []),
|
||||
])
|
||||
}
|
||||
|
||||
export const useMenuStore = defineStore('menu', {
|
||||
state: () => ({
|
||||
isMaster: false,
|
||||
@@ -49,9 +56,24 @@ export const useMenuStore = defineStore('menu', {
|
||||
mappedMenus(state) {
|
||||
return state.dynamicMenus.map((item) => mapMenuItem(item))
|
||||
},
|
||||
accessibleMenus(state) {
|
||||
return flattenMenus(state.dynamicMenus)
|
||||
},
|
||||
accessibleUrls() {
|
||||
return this.accessibleMenus
|
||||
.filter((item) => item?.permissions?.can_view !== false && item?.url && item.url !== '#')
|
||||
.map((item) => item.url)
|
||||
},
|
||||
},
|
||||
|
||||
actions: {
|
||||
canAccessUrl(url) {
|
||||
if (!url) return false
|
||||
|
||||
return this.accessibleUrls.some(
|
||||
(menuUrl) => url === menuUrl || url.startsWith(`${menuUrl}/`),
|
||||
)
|
||||
},
|
||||
async fetchMenus({ force = false } = {}) {
|
||||
if (this.initialized && !force) {
|
||||
return this.dynamicMenus
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import BaseResponsiveDataView from '@/components/base/BaseResponsiveDataView.vue'
|
||||
import {
|
||||
getRoleApplications,
|
||||
getStaffMenuGroupOptions,
|
||||
getTenantMenuGroupOptions,
|
||||
reviewRoleApplication,
|
||||
} from '@/services/roleApplicationService'
|
||||
import { showError, showSuccess } from '@/utils/swal'
|
||||
|
||||
const route = useRoute(),
|
||||
type = computed(() => route.meta.applicationType),
|
||||
loading = ref(true),
|
||||
loaded = ref(false),
|
||||
saving = ref(false),
|
||||
menuGroupsLoading = ref(false),
|
||||
menuGroups = ref([]),
|
||||
items = ref([]),
|
||||
modal = ref(false),
|
||||
selected = ref(null)
|
||||
const query = ref({ page: 1, per_page: 10, status: 'pending' }),
|
||||
pagination = ref({ current_page: 1, last_page: 1, per_page: 10, total: 0 })
|
||||
const form = reactive({
|
||||
decision: 'approved',
|
||||
review_notes: '',
|
||||
menu_group_id: '',
|
||||
payment_type: 'prepaid',
|
||||
price_mode: 'discount',
|
||||
discount_percent: 0,
|
||||
commission_percent: 0,
|
||||
credit_limit: 0,
|
||||
})
|
||||
const config = computed(
|
||||
() =>
|
||||
({
|
||||
tenant: ['Pengajuan Tenant', 'Persetujuan tenant baru oleh platform'],
|
||||
agent: ['Pengajuan Agen Voucher', 'Persetujuan agen pada tenant aktif'],
|
||||
staff: ['Pengajuan Staff', 'Persetujuan calon karyawan pada tenant aktif'],
|
||||
})[type.value],
|
||||
)
|
||||
const columns = [
|
||||
{ key: 'applicant_label', label: 'Pemohon' },
|
||||
{ key: 'target_label', label: 'Pengajuan' },
|
||||
{ key: 'reason', label: 'Alasan' },
|
||||
{ key: 'status_label', label: 'Status' },
|
||||
{ key: 'submitted_label', label: 'Diajukan' },
|
||||
]
|
||||
const actions = [{ key: 'review', label: 'Tinjau', color: 'primary' }]
|
||||
async function fetchData(params = query.value) {
|
||||
query.value = { ...query.value, ...params }
|
||||
loading.value = true
|
||||
try {
|
||||
const r = await getRoleApplications(type.value, query.value),
|
||||
rows = r?.data?.data || []
|
||||
items.value = rows.map((x) => ({
|
||||
...x,
|
||||
applicant_label: `${x.applicant?.name || '-'} · ${x.applicant?.email || ''}`,
|
||||
target_label: type.value === 'tenant' ? x.payload?.tenant_name : x.tenant?.tenant_name,
|
||||
reason: x.payload?.reason,
|
||||
status_label: { pending: 'Menunggu', approved: 'Disetujui', rejected: 'Ditolak' }[x.status],
|
||||
submitted_label: new Date(x.submitted_at).toLocaleString('id-ID'),
|
||||
}))
|
||||
pagination.value = {
|
||||
current_page: r?.data?.current_page || 1,
|
||||
last_page: r?.data?.last_page || 1,
|
||||
per_page: r?.data?.per_page || 10,
|
||||
total: r?.data?.total || 0,
|
||||
}
|
||||
} catch (e) {
|
||||
showError(e?.response?.data?.message || 'Gagal memuat pengajuan.')
|
||||
} finally {
|
||||
loading.value = false
|
||||
loaded.value = true
|
||||
}
|
||||
}
|
||||
async function loadMenuGroups() {
|
||||
if (!['tenant', 'staff'].includes(type.value)) return
|
||||
menuGroupsLoading.value = true
|
||||
try {
|
||||
const r =
|
||||
type.value === 'tenant' ? await getTenantMenuGroupOptions() : await getStaffMenuGroupOptions()
|
||||
menuGroups.value = r?.data || []
|
||||
} catch (e) {
|
||||
showError(e?.response?.data?.message || 'Gagal memuat pilihan Group Menu.')
|
||||
} finally {
|
||||
menuGroupsLoading.value = false
|
||||
}
|
||||
}
|
||||
async function open({ item }) {
|
||||
selected.value = item
|
||||
Object.assign(form, {
|
||||
decision: 'approved',
|
||||
review_notes: '',
|
||||
menu_group_id: '',
|
||||
payment_type: 'prepaid',
|
||||
price_mode: 'discount',
|
||||
discount_percent: 0,
|
||||
commission_percent: 0,
|
||||
credit_limit: 0,
|
||||
})
|
||||
modal.value = true
|
||||
await loadMenuGroups()
|
||||
}
|
||||
async function submit() {
|
||||
if (
|
||||
['tenant', 'staff'].includes(type.value) &&
|
||||
form.decision === 'approved' &&
|
||||
!form.menu_group_id
|
||||
) {
|
||||
showError('Group Menu wajib dipilih sebelum menyetujui pengajuan.')
|
||||
return
|
||||
}
|
||||
saving.value = true
|
||||
try {
|
||||
await reviewRoleApplication(selected.value.id, {
|
||||
...form,
|
||||
menu_group_id: form.menu_group_id || null,
|
||||
})
|
||||
modal.value = false
|
||||
showSuccess(`Pengajuan berhasil ${form.decision === 'approved' ? 'disetujui' : 'ditolak'}.`)
|
||||
await fetchData()
|
||||
} catch (e) {
|
||||
const errors = e?.response?.data?.errors
|
||||
showError(
|
||||
errors?.menu_group_id?.[0] || e?.response?.data?.message || 'Pengajuan gagal ditinjau.',
|
||||
)
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
watch(type, () => {
|
||||
loaded.value = false
|
||||
menuGroups.value = []
|
||||
fetchData()
|
||||
})
|
||||
onMounted(fetchData)
|
||||
</script>
|
||||
<template>
|
||||
<div class="brdvx-page-container">
|
||||
<div class="brdvx-page-header">
|
||||
<h5 class="mb-0 fw-semibold">{{ config[0] }}</h5>
|
||||
<small class="text-body-secondary">{{ config[1] }}</small>
|
||||
</div>
|
||||
<div v-if="loading && !loaded" class="brdvx-loading-state">
|
||||
<CSpinner color="primary" />
|
||||
<p class="brdvx-loading-text">Memuat data...</p>
|
||||
</div>
|
||||
<BaseResponsiveDataView
|
||||
v-else
|
||||
:columns="columns"
|
||||
:items="items"
|
||||
:actions="actions"
|
||||
server-side
|
||||
:pagination="pagination"
|
||||
:loading="loading"
|
||||
@query-change="fetchData"
|
||||
@action-click="open"
|
||||
/>
|
||||
</div>
|
||||
<CModal :visible="modal" @close="modal = false"
|
||||
><CModalHeader><CModalTitle>Tinjau Pengajuan</CModalTitle></CModalHeader
|
||||
><CModalBody
|
||||
><CAlert color="info"
|
||||
><strong>{{ selected?.applicant?.name }}</strong>
|
||||
<div>{{ selected?.reason }}</div></CAlert
|
||||
><CFormLabel>Keputusan</CFormLabel
|
||||
><CFormSelect v-model="form.decision" class="mb-3"
|
||||
><option value="approved">Setujui</option>
|
||||
<option value="rejected">Tolak</option></CFormSelect
|
||||
><template v-if="['tenant', 'staff'].includes(type) && form.decision === 'approved'"
|
||||
><CFormLabel>Group Menu <span class="text-danger">*</span></CFormLabel
|
||||
><CFormSelect
|
||||
v-model="form.menu_group_id"
|
||||
class="mb-2"
|
||||
:disabled="menuGroupsLoading"
|
||||
required
|
||||
><option value="">
|
||||
{{ menuGroupsLoading ? 'Memuat Group Menu...' : `Pilih Group Menu ${type}` }}
|
||||
</option>
|
||||
<option v-for="group in menuGroups" :key="group.id" :value="group.id">
|
||||
{{ group.name }} ({{ group.menus_count }} menu)
|
||||
</option></CFormSelect
|
||||
><small v-if="form.menu_group_id" class="d-block text-body-secondary mb-3">{{
|
||||
menuGroups.find((group) => group.id === Number(form.menu_group_id))?.description
|
||||
}}</small
|
||||
><CAlert v-if="!menuGroupsLoading && !menuGroups.length" color="warning"
|
||||
>Belum ada Group Menu yang dapat diberikan.</CAlert
|
||||
></template
|
||||
><template v-if="type === 'agent' && form.decision === 'approved'"
|
||||
><CFormLabel>Kategori Agen</CFormLabel
|
||||
><CFormSelect v-model="form.payment_type" class="mb-3"
|
||||
><option value="prepaid">Deposit dahulu</option>
|
||||
<option value="postpaid">Jual dahulu, bayar kemudian</option></CFormSelect
|
||||
><CFormLabel>Skema Harga</CFormLabel
|
||||
><CFormSelect v-model="form.price_mode" class="mb-3"
|
||||
><option value="discount">Diskon</option>
|
||||
<option value="commission">Komisi</option></CFormSelect
|
||||
><CRow class="g-2"
|
||||
><CCol :md="6"
|
||||
><CFormLabel>Diskon (%)</CFormLabel
|
||||
><CFormInput v-model.number="form.discount_percent" type="number" /></CCol
|
||||
><CCol :md="6"
|
||||
><CFormLabel>Komisi (%)</CFormLabel
|
||||
><CFormInput v-model.number="form.commission_percent" type="number" /></CCol
|
||||
><CCol v-if="form.payment_type === 'postpaid'" :xs="12"
|
||||
><CFormLabel>Limit Kredit</CFormLabel
|
||||
><CFormInput v-model.number="form.credit_limit" type="number" /></CCol></CRow></template
|
||||
><CFormLabel class="mt-3">Catatan Reviewer</CFormLabel
|
||||
><CFormTextarea v-model="form.review_notes" rows="3" /></CModalBody
|
||||
><CModalFooter
|
||||
><CButton color="secondary" @click="modal = false">Batal</CButton
|
||||
><CButton
|
||||
:color="form.decision === 'approved' ? 'success' : 'danger'"
|
||||
:disabled="
|
||||
saving ||
|
||||
(['tenant', 'staff'].includes(type) &&
|
||||
form.decision === 'approved' &&
|
||||
(!form.menu_group_id || menuGroupsLoading))
|
||||
"
|
||||
@click="submit"
|
||||
>Simpan Keputusan</CButton
|
||||
></CModalFooter
|
||||
></CModal
|
||||
>
|
||||
</template>
|
||||
@@ -0,0 +1,303 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { createRoleApplication, getMyRoleApplications } from '@/services/roleApplicationService'
|
||||
import { showError, showSuccess } from '@/utils/swal'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const loading = ref(true),
|
||||
saving = ref(false),
|
||||
items = ref([]),
|
||||
modal = ref('')
|
||||
const form = reactive({})
|
||||
const statusColor = { pending: 'warning', approved: 'success', rejected: 'danger' }
|
||||
const accessLevel = computed(() => authStore.user?.access_level || 'customer')
|
||||
const pendingTypes = computed(
|
||||
() =>
|
||||
new Set(
|
||||
items.value.filter((item) => item.status === 'pending').map((item) => item.application_type),
|
||||
),
|
||||
)
|
||||
const approvedTypes = computed(
|
||||
() =>
|
||||
new Set(
|
||||
items.value.filter((item) => item.status === 'approved').map((item) => item.application_type),
|
||||
),
|
||||
)
|
||||
const isTenantOwner = computed(
|
||||
() => accessLevel.value === 'tenant_owner' || approvedTypes.value.has('tenant'),
|
||||
)
|
||||
const isStaff = computed(() => accessLevel.value === 'staff' || approvedTypes.value.has('staff'))
|
||||
const hasPendingTenant = computed(() => pendingTypes.value.has('tenant'))
|
||||
const hasPendingStaff = computed(() => pendingTypes.value.has('staff'))
|
||||
const hasPendingAgent = computed(() => pendingTypes.value.has('agent'))
|
||||
const showTenantAction = computed(
|
||||
() => !isTenantOwner.value && !isStaff.value && !hasPendingTenant.value && !hasPendingStaff.value,
|
||||
)
|
||||
const showStaffAction = computed(
|
||||
() => !isTenantOwner.value && !hasPendingTenant.value && !hasPendingStaff.value,
|
||||
)
|
||||
const showAgentAction = computed(
|
||||
() => !isTenantOwner.value && !hasPendingTenant.value && !hasPendingAgent.value,
|
||||
)
|
||||
const contextMessage = computed(() => {
|
||||
if (isTenantOwner.value)
|
||||
return 'Akun Anda sudah menjadi Tenant Owner. Pengajuan Staff dan Agen tidak tersedia.'
|
||||
if (hasPendingTenant.value)
|
||||
return 'Pengajuan Tenant Owner sedang ditinjau. Pengajuan lain ditutup sementara sampai proses selesai.'
|
||||
if (isStaff.value)
|
||||
return 'Sebagai staff, Anda dapat mengajukan perubahan akses staff pada tenant atau menjadi agen.'
|
||||
if (hasPendingStaff.value)
|
||||
return 'Pengajuan Staff sedang ditinjau. Anda tetap dapat mengajukan diri sebagai agen.'
|
||||
return 'Pilih jenis akses yang ingin Anda ajukan.'
|
||||
})
|
||||
const defaults = (type) =>
|
||||
type === 'tenant'
|
||||
? {
|
||||
application_type: 'tenant',
|
||||
tenant_name: '',
|
||||
tenant_code: '',
|
||||
phone: '',
|
||||
email: '',
|
||||
address: '',
|
||||
reason: '',
|
||||
}
|
||||
: { application_type: type, tenant_id: '', reason: '' }
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
items.value = (await getMyRoleApplications())?.data || []
|
||||
} catch (e) {
|
||||
showError(e?.response?.data?.message || 'Gagal memuat pengajuan.')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
function open(type) {
|
||||
Object.keys(form).forEach((key) => delete form[key])
|
||||
Object.assign(form, defaults(type))
|
||||
modal.value = type
|
||||
}
|
||||
async function submit() {
|
||||
saving.value = true
|
||||
try {
|
||||
await createRoleApplication(form)
|
||||
modal.value = ''
|
||||
showSuccess('Pengajuan berhasil dikirim dan menunggu persetujuan.')
|
||||
await load()
|
||||
} catch (e) {
|
||||
showError(e?.response?.data?.message || 'Pengajuan gagal dikirim.')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
onMounted(load)
|
||||
</script>
|
||||
<template>
|
||||
<div class="brdvx-page-container">
|
||||
<div class="brdvx-page-header">
|
||||
<h5 class="mb-0 fw-semibold">Pengajuan Saya</h5>
|
||||
<small class="text-body-secondary"
|
||||
>Akun baru selalu berstatus customer. Ajukan akses tenant, staff, atau agen sesuai
|
||||
kebutuhan.</small
|
||||
>
|
||||
</div>
|
||||
<CAlert v-if="!loading" color="info" class="application-context">
|
||||
<CIcon icon="cil-info" />
|
||||
<span>{{ contextMessage }}</span>
|
||||
</CAlert>
|
||||
<div v-if="!loading" class="application-actions">
|
||||
<button v-if="showTenantAction" @click="open('tenant')">
|
||||
<span><CIcon icon="cil-building" size="xl" /></span>
|
||||
<div>
|
||||
<strong>Menjadi Pemilik Tenant</strong
|
||||
><small>Buat dan kelola layanan jaringan Anda sendiri</small>
|
||||
</div></button
|
||||
><button v-if="showAgentAction" @click="open('agent')">
|
||||
<span><CIcon icon="cil-user-follow" size="xl" /></span>
|
||||
<div>
|
||||
<strong>Menjadi Agen Voucher</strong
|
||||
><small>Bergabung sebagai agen pada tenant tertentu</small>
|
||||
</div>
|
||||
</button>
|
||||
<button v-if="showStaffAction" @click="open('staff')">
|
||||
<span><CIcon icon="cil-people" size="xl" /></span>
|
||||
<div>
|
||||
<strong>Menjadi Staff Tenant</strong
|
||||
><small>Bergabung sebagai karyawan pada tenant tertentu</small>
|
||||
</div>
|
||||
</button>
|
||||
<div
|
||||
v-if="!showTenantAction && !showStaffAction && !showAgentAction"
|
||||
class="application-actions-empty"
|
||||
>
|
||||
<CIcon :icon="hasPendingTenant ? 'cil-task' : 'cil-check-circle'" size="xl" />
|
||||
<strong>{{ hasPendingTenant ? 'Menunggu persetujuan' : 'Akses sudah aktif' }}</strong>
|
||||
<small>{{ contextMessage }}</small>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="loading" class="brdvx-loading-state">
|
||||
<CSpinner color="primary" />
|
||||
<p class="brdvx-loading-text">Memuat data...</p>
|
||||
</div>
|
||||
<div v-else class="history">
|
||||
<h6>Riwayat Pengajuan</h6>
|
||||
<div v-if="!items.length" class="empty">Belum ada pengajuan.</div>
|
||||
<article v-for="item in items" :key="item.id">
|
||||
<span class="type-icon"
|
||||
><CIcon
|
||||
:icon="
|
||||
item.application_type === 'tenant'
|
||||
? 'cil-building'
|
||||
: item.application_type === 'staff'
|
||||
? 'cil-people'
|
||||
: 'cil-user-follow'
|
||||
"
|
||||
/></span>
|
||||
<div>
|
||||
<strong>{{
|
||||
item.application_type === 'tenant'
|
||||
? item.payload?.tenant_name
|
||||
: `${item.application_type === 'staff' ? 'Staff' : 'Agen'} ${item.tenant?.tenant_name || `Tenant #${item.tenant_id}`}`
|
||||
}}</strong
|
||||
><small>Diajukan {{ new Date(item.submitted_at).toLocaleString('id-ID') }}</small>
|
||||
<p v-if="item.review_notes">Catatan: {{ item.review_notes }}</p>
|
||||
</div>
|
||||
<CBadge :color="statusColor[item.status]">{{
|
||||
{ pending: 'Menunggu', approved: 'Disetujui', rejected: 'Ditolak' }[item.status]
|
||||
}}</CBadge>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
<CModal :visible="Boolean(modal)" @close="modal = ''"
|
||||
><CModalHeader
|
||||
><CModalTitle>{{
|
||||
modal === 'tenant'
|
||||
? 'Pengajuan Tenant'
|
||||
: modal === 'staff'
|
||||
? 'Pengajuan Staff Tenant'
|
||||
: 'Pengajuan Agen Voucher'
|
||||
}}</CModalTitle></CModalHeader
|
||||
><CModalBody
|
||||
><template v-if="modal === 'tenant'"
|
||||
><CFormLabel>Nama Tenant/Usaha</CFormLabel
|
||||
><CFormInput v-model="form.tenant_name" class="mb-3" /><CFormLabel
|
||||
>Kode Tenant yang Diinginkan</CFormLabel
|
||||
><CFormInput v-model="form.tenant_code" placeholder="Opsional" class="mb-3" /><CFormLabel
|
||||
>Telepon/WhatsApp Usaha</CFormLabel
|
||||
><CFormInput v-model="form.phone" class="mb-3" /><CFormLabel>Email Usaha</CFormLabel
|
||||
><CFormInput v-model="form.email" type="email" class="mb-3" /><CFormLabel
|
||||
>Alamat Usaha</CFormLabel
|
||||
><CFormTextarea v-model="form.address" rows="2" class="mb-3" /></template
|
||||
><template v-else
|
||||
><CAlert color="info">Masukkan ID tenant yang diberikan oleh pemilik tenant.</CAlert
|
||||
><CFormLabel>ID Tenant</CFormLabel
|
||||
><CFormInput v-model.number="form.tenant_id" type="number" min="1" class="mb-3" /></template
|
||||
><CFormLabel>Alasan Pengajuan</CFormLabel
|
||||
><CFormTextarea v-model="form.reason" rows="3" /></CModalBody
|
||||
><CModalFooter
|
||||
><CButton color="secondary" @click="modal = ''">Batal</CButton
|
||||
><CButton color="primary" :disabled="saving" @click="submit"
|
||||
>Kirim Pengajuan</CButton
|
||||
></CModalFooter
|
||||
></CModal
|
||||
>
|
||||
</template>
|
||||
<style scoped>
|
||||
.application-context {
|
||||
display: flex;
|
||||
max-width: 850px;
|
||||
align-items: center;
|
||||
gap: 0.65rem;
|
||||
margin: 1rem auto 0;
|
||||
}
|
||||
.application-actions {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(230px, 340px));
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
margin: 1rem 0 2rem;
|
||||
}
|
||||
.application-actions-empty {
|
||||
display: flex;
|
||||
width: min(100%, 420px);
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
padding: 1.4rem;
|
||||
color: var(--cui-secondary-color);
|
||||
text-align: center;
|
||||
background: var(--cui-tertiary-bg);
|
||||
border: 1px dashed var(--cui-border-color);
|
||||
border-radius: 1rem;
|
||||
}
|
||||
.application-actions-empty strong {
|
||||
color: var(--cui-body-color);
|
||||
}
|
||||
.application-actions button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.8rem;
|
||||
padding: 1rem;
|
||||
color: var(--cui-body-color);
|
||||
text-align: left;
|
||||
background: var(--cui-body-bg);
|
||||
border: 1px solid var(--cui-border-color);
|
||||
border-radius: 1rem;
|
||||
box-shadow: 0 0.4rem 1rem rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
.application-actions button:hover {
|
||||
border-color: #ff8c00;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
.application-actions span,
|
||||
.type-icon {
|
||||
display: grid;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
flex: 0 0 48px;
|
||||
place-items: center;
|
||||
color: #fff;
|
||||
background: linear-gradient(135deg, #f97316, #ff8c00);
|
||||
border-radius: 0.8rem;
|
||||
}
|
||||
.application-actions div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.application-actions small,
|
||||
.history article small {
|
||||
color: var(--cui-secondary-color);
|
||||
font-size: 0.72rem;
|
||||
}
|
||||
.history {
|
||||
max-width: 850px;
|
||||
margin: auto;
|
||||
}
|
||||
.history > h6 {
|
||||
margin-bottom: 0.8rem;
|
||||
}
|
||||
.history article {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.8rem;
|
||||
padding: 0.8rem 0;
|
||||
border-bottom: 1px solid var(--cui-border-color);
|
||||
}
|
||||
.history article > div {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
.history article p {
|
||||
margin: 0.25rem 0 0;
|
||||
color: var(--cui-secondary-color);
|
||||
font-size: 0.72rem;
|
||||
}
|
||||
.empty {
|
||||
padding: 2rem;
|
||||
color: var(--cui-secondary-color);
|
||||
text-align: center;
|
||||
border: 1px dashed var(--cui-border-color);
|
||||
border-radius: 1rem;
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,31 @@
|
||||
<script setup>
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const router = useRouter()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="permission-page">
|
||||
<div class="permission-card">
|
||||
<span class="permission-icon"><CIcon icon="cil-shield-alt" size="3xl" /></span>
|
||||
<div>
|
||||
<p class="permission-code">403</p>
|
||||
<h1>Tidak Memiliki Akses</h1>
|
||||
<p>Menu atau halaman ini tidak tersedia untuk akun yang sedang digunakan.</p>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap justify-content-center gap-2">
|
||||
<CButton color="secondary" variant="outline" @click="router.back()">Kembali</CButton>
|
||||
<CButton color="primary" @click="router.push('/dashboard')">Ke Dashboard</CButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.permission-page { display: grid; min-height: calc(100vh - 9rem); place-items: center; padding: 1rem; }
|
||||
.permission-card { width: min(100%, 32rem); padding: 2rem; text-align: center; background: var(--cui-body-bg); border: 1px solid var(--cui-border-color); border-radius: 1rem; box-shadow: 0 .75rem 2rem rgba(0, 0, 0, .08); }
|
||||
.permission-icon { display: inline-flex; width: 5rem; height: 5rem; align-items: center; justify-content: center; margin-bottom: 1rem; color: #fff; background: linear-gradient(135deg, #f97316, #ff8c00); border-radius: 50%; }
|
||||
.permission-code { margin: 0; color: #ff8c00; font-size: .8rem; font-weight: 700; letter-spacing: .18em; }
|
||||
.permission-card h1 { margin: .25rem 0 .65rem; font-size: clamp(1.4rem, 4vw, 2rem); }
|
||||
.permission-card > div > p:last-child { color: var(--cui-secondary-color); }
|
||||
</style>
|
||||
@@ -14,6 +14,11 @@ const form = reactive({
|
||||
|
||||
const isLoading = computed(() => authStore.loading)
|
||||
const errorMessage = computed(() => authStore.error)
|
||||
const temporaryVerificationCode = computed(() => authStore.verificationCode)
|
||||
|
||||
function useTemporaryCode() {
|
||||
form.verification_code = temporaryVerificationCode.value
|
||||
}
|
||||
|
||||
async function onSubmit() {
|
||||
authStore.clearError()
|
||||
@@ -98,6 +103,23 @@ function goToLogin() {
|
||||
/>
|
||||
</CInputGroup>
|
||||
|
||||
<CAlert
|
||||
v-if="temporaryVerificationCode"
|
||||
color="warning"
|
||||
class="temporary-code-box mb-4"
|
||||
>
|
||||
<div>
|
||||
<small class="d-block text-body-secondary">Kode verifikasi sementara</small>
|
||||
<strong class="temporary-code-value">{{ temporaryVerificationCode }}</strong>
|
||||
<small class="d-block mt-1">
|
||||
Kode ditampilkan di sini karena channel pengiriman belum aktif.
|
||||
</small>
|
||||
</div>
|
||||
<CButton color="warning" size="sm" type="button" @click="useTemporaryCode">
|
||||
Gunakan kode
|
||||
</CButton>
|
||||
</CAlert>
|
||||
|
||||
<div class="d-grid gap-2">
|
||||
<CButton color="primary" type="submit" :disabled="isLoading">
|
||||
{{ isLoading ? 'Memverifikasi...' : 'Verifikasi' }}
|
||||
@@ -119,3 +141,24 @@ function goToLogin() {
|
||||
</CContainer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.temporary-code-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.temporary-code-value {
|
||||
font-size: 1.45rem;
|
||||
letter-spacing: 0.32rem;
|
||||
}
|
||||
|
||||
@media (max-width: 575.98px) {
|
||||
.temporary-code-box {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { useTicketIncidentStore } from '@/stores/ticketIncident.js'
|
||||
import { formatTanggal } from '@/utils/tglindo.js'
|
||||
import { showConfirm, showSuccess, showSuccessr, showError } from '@/utils/swal.js'
|
||||
import { showConfirm, showSuccess, showError } from '@/utils/swal.js'
|
||||
import { CButton } from '@coreui/vue'
|
||||
import api from '@/api/axios'
|
||||
|
||||
@@ -136,7 +136,7 @@ async function updateTicketIncidentType(id) {
|
||||
const response = await api.put(`/ticket-incident-types/${id}`, body)
|
||||
store.updateItem(editingIndex.value, response.data.data)
|
||||
modal.value = false
|
||||
showSuccessr('Data Berhasil Di Simpan')
|
||||
showSuccess('Data Berhasil Di Simpan')
|
||||
fetchTicketIncidentTypes()
|
||||
} catch (err) {
|
||||
showError(err.response?.data?.message || 'Gagal mengupdate data: ' + err.message)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { useTicketTypeStore } from '@/stores/ticketType.js'
|
||||
import { formatTanggal } from '@/utils/tglindo.js'
|
||||
import { showConfirm, showSuccess, showSuccessr, showError } from '@/utils/swal.js'
|
||||
import { showConfirm, showSuccess, showError } from '@/utils/swal.js'
|
||||
import { CButton } from '@coreui/vue'
|
||||
import { CBadge } from '@coreui/vue'
|
||||
import api from '@/api/axios'
|
||||
@@ -169,7 +169,7 @@ async function updateTicketType(id) {
|
||||
await api.put(`/ticket-types/${id}`, body)
|
||||
store.updateTicketType(editingIndex.value, { ...body })
|
||||
modal.value = false
|
||||
showSuccessr('Data berhasil diupdate')
|
||||
showSuccess('Data berhasil diupdate')
|
||||
fetchAllData()
|
||||
} catch (err) {
|
||||
showError(err.response?.data?.message || 'Gagal mengupdate data: ' + err.message)
|
||||
|
||||
@@ -68,6 +68,7 @@ function emptyForm() {
|
||||
whatsapp_number: '',
|
||||
password: '',
|
||||
access_level: 'staff',
|
||||
is_platform_staff: false,
|
||||
status: 'active',
|
||||
nik: '',
|
||||
address: '',
|
||||
@@ -263,6 +264,7 @@ async function hydrateForm(user) {
|
||||
email: user?.email || '',
|
||||
whatsapp_number: user?.whatsapp_number || '',
|
||||
access_level: user?.access_level || 'staff',
|
||||
is_platform_staff: Boolean(user?.is_platform_staff),
|
||||
status: user?.status || 'active',
|
||||
nik: profile.nik || '',
|
||||
address: profile.address || '',
|
||||
@@ -320,6 +322,7 @@ function buildPayload() {
|
||||
whatsapp_number: form.whatsapp_number.trim() || null,
|
||||
access_level: form.access_level,
|
||||
is_master: form.access_level === 'master_admin',
|
||||
is_platform_staff: isMasterActor.value ? Boolean(form.is_platform_staff) : false,
|
||||
status: form.status,
|
||||
user_profile: {
|
||||
nik: form.nik.trim() || null,
|
||||
@@ -482,6 +485,7 @@ onMounted(fetchUsers)
|
||||
<option value="active">Aktif</option><option value="inactive">Nonaktif</option><option value="suspended">Ditangguhkan</option>
|
||||
</CFormSelect>
|
||||
</CCol>
|
||||
<CCol v-if="isMasterActor" :md="4" class="d-flex align-items-end pb-2"><CFormCheck v-model="form.is_platform_staff" label="Staff Platform (dapat review pengajuan tenant)" /></CCol>
|
||||
<CCol :md="4"><CFormLabel>Password {{ isEdit ? '(opsional)' : '' }}</CFormLabel><CFormInput v-model="form.password" type="password" /></CCol>
|
||||
</CRow>
|
||||
</CCardBody>
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
<script setup>
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import BaseResponsiveDataView from '@/components/base/BaseResponsiveDataView.vue'
|
||||
import { createVoucherAgent, deleteVoucherAgent, getVoucherAgents, getVoucherOptions, updateVoucherAgent } from '@/services/voucherService'
|
||||
import { showConfirm, showError, showSuccess } from '@/utils/swal'
|
||||
|
||||
const loading = ref(true), loaded = ref(false), saving = ref(false), modal = ref(false), editing = ref(null), items = ref([]), users = ref([])
|
||||
const query = ref({ page: 1, per_page: 10 }), pagination = ref({ current_page: 1, last_page: 1, per_page: 10, total: 0 }), form = reactive({})
|
||||
const columns = [{ key: 'agent_code', label: 'Kode Agen' }, { key: 'user_label', label: 'User' }, { key: 'type_label', label: 'Kategori' }, { key: 'pricing_label', label: 'Parameter Harga' }, { key: 'limit_label', label: 'Limit/Piutang' }, { key: 'status', label: 'Status' }]
|
||||
const actions = [{ key: 'edit', label: 'Edit', color: 'success' }, { key: 'delete', label: 'Nonaktifkan', color: 'danger' }]
|
||||
const money = (v) => `Rp ${Number(v || 0).toLocaleString('id-ID')}`
|
||||
const defaults = () => ({ user_id: '', agent_code: '', payment_type: 'prepaid', price_mode: 'discount', discount_percent: 0, commission_percent: 0, credit_limit: 0, status: 'active', notes: '' })
|
||||
function normalize(x) { return { ...x, user_label: x.user?.name || '-', type_label: x.payment_type === 'prepaid' ? 'Deposit Dahulu' : 'Jual Dulu, Bayar Kemudian', pricing_label: x.price_mode === 'discount' ? `Diskon ${x.discount_percent}%` : `Komisi ${x.commission_percent}%`, limit_label: x.payment_type === 'postpaid' ? `${money(x.outstanding_balance)} / ${money(x.credit_limit)}` : 'Menggunakan saldo dompet' } }
|
||||
async function fetchData(params = query.value) { query.value = { ...query.value, ...params }; loading.value = true; try { const r = await getVoucherAgents(query.value), rows = r?.data?.data || []; items.value = rows.map(normalize); pagination.value = { current_page: r?.data?.current_page || 1, last_page: r?.data?.last_page || 1, per_page: r?.data?.per_page || 10, total: r?.data?.total || 0 } } catch (e) { showError(e?.response?.data?.message || 'Gagal memuat agen voucher.') } finally { loading.value = false; loaded.value = true } }
|
||||
async function open(item = null) { editing.value = item; Object.keys(form).forEach((key) => delete form[key]); Object.assign(form, defaults(), item || {}); try { users.value = (await getVoucherOptions('agents'))?.data?.users || []; modal.value = true } catch (e) { showError(e?.response?.data?.message || 'Gagal memuat user tenant.') } }
|
||||
async function save() { if (!form.user_id) return showError('User agen wajib dipilih.'); saving.value = true; try { if (editing.value) await updateVoucherAgent(editing.value.id, form); else await createVoucherAgent(form); modal.value = false; showSuccess('Agen voucher berhasil disimpan.'); await fetchData() } catch (e) { showError(e?.response?.data?.message || 'Agen voucher gagal disimpan.') } finally { saving.value = false } }
|
||||
async function action({ action, item }) { if (action === 'edit') return open(item); const result = await showConfirm('Nonaktifkan Agen', `Nonaktifkan agen ${item.user_label}?`); if (!result.isConfirmed) return; try { await deleteVoucherAgent(item.id); showSuccess('Agen berhasil dinonaktifkan.'); await fetchData() } catch (e) { showError(e?.response?.data?.message || 'Agen gagal dinonaktifkan.') } }
|
||||
onMounted(fetchData)
|
||||
</script>
|
||||
<template><div class="brdvx-page-container"><div class="brdvx-page-header d-flex justify-content-between align-items-center"><div><h5 class="mb-0 fw-semibold">Agen Voucher</h5><small class="text-body-secondary">Atur agen deposit dan agen dengan fasilitas kredit</small></div><CButton color="primary" size="sm" @click="open()">+ Tambah Agen</CButton></div><div v-if="loading && !loaded" class="brdvx-loading-state"><CSpinner color="primary" /><p class="brdvx-loading-text">Memuat data...</p></div><BaseResponsiveDataView v-else :columns="columns" :items="items" :actions="actions" server-side :pagination="pagination" :loading="loading" @query-change="fetchData" @action-click="action" /></div>
|
||||
<CModal :visible="modal" @close="modal = false"><CModalHeader><CModalTitle>{{ editing ? 'Edit' : 'Tambah' }} Agen Voucher</CModalTitle></CModalHeader><CModalBody><CFormLabel>User Tenant</CFormLabel><CFormSelect v-model="form.user_id" :disabled="Boolean(editing)" class="mb-3"><option value="">Pilih user</option><option v-for="user in users" :key="user.id" :value="user.id">{{ user.name }} · {{ user.username }}</option></CFormSelect><CFormLabel>Kode Agen</CFormLabel><CFormInput v-model="form.agent_code" placeholder="Otomatis jika dikosongkan" class="mb-3" /><CFormLabel>Kategori Pembayaran</CFormLabel><CFormSelect v-model="form.payment_type" class="mb-3"><option value="prepaid">Deposit dahulu, lalu beli voucher</option><option value="postpaid">Jual voucher dahulu, bayar kemudian</option></CFormSelect><CFormLabel>Skema Harga</CFormLabel><CFormSelect v-model="form.price_mode" class="mb-3"><option value="discount">Diskon harga beli</option><option value="commission">Komisi penjualan</option></CFormSelect><CRow class="g-3"><CCol :md="6"><CFormLabel>Diskon (%)</CFormLabel><CFormInput v-model.number="form.discount_percent" type="number" min="0" max="100" /></CCol><CCol :md="6"><CFormLabel>Komisi (%)</CFormLabel><CFormInput v-model.number="form.commission_percent" type="number" min="0" max="100" /></CCol><CCol v-if="form.payment_type === 'postpaid'" :xs="12"><CFormLabel>Limit Kredit</CFormLabel><CFormInput v-model.number="form.credit_limit" type="number" min="0" /></CCol></CRow><CFormLabel class="mt-3">Status</CFormLabel><CFormSelect v-model="form.status" class="mb-3"><option value="active">Aktif</option><option value="inactive">Nonaktif</option><option value="suspended">Ditangguhkan</option></CFormSelect><CFormLabel>Catatan</CFormLabel><CFormTextarea v-model="form.notes" rows="2" /></CModalBody><CModalFooter><CButton color="secondary" @click="modal = false">Batal</CButton><CButton color="primary" :disabled="saving" @click="save">Simpan</CButton></CModalFooter></CModal></template>
|
||||
@@ -0,0 +1,139 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { getVoucherLoginPage, getVoucherOptions, saveVoucherLoginPage } from '@/services/voucherService'
|
||||
import { showError, showSuccess } from '@/utils/swal'
|
||||
|
||||
const loading = ref(true), saving = ref(false), data = ref(null), packages = ref([]), form = reactive({ enabled: false, title: 'Beli Voucher Hotspot', theme_color: '#ff8c00', allowed_package_profile_ids: [], settings: {} })
|
||||
const setting = computed(() => data.value?.setting || {}), dummySales = computed(() => data.value?.dummy_sales || [])
|
||||
const money = (v) => `Rp ${Number(v || 0).toLocaleString('id-ID')}`
|
||||
async function load() { loading.value = true; try { const [page, options] = await Promise.all([getVoucherLoginPage(), getVoucherOptions('login-page')]); data.value = page?.data; packages.value = options?.data?.packages || []; Object.assign(form, { enabled: false, title: 'Beli Voucher Hotspot', theme_color: '#ff8c00', allowed_package_profile_ids: [], settings: {}, ...(data.value?.setting || {}) }) } catch (e) { showError(e?.response?.data?.message || 'Gagal memuat setting LoginPage.') } finally { loading.value = false } }
|
||||
async function save() { saving.value = true; try { data.value = (await saveVoucherLoginPage(form))?.data; Object.assign(form, data.value?.setting || {}); showSuccess(form.enabled ? 'Voucher LoginPage berhasil diaktifkan.' : 'Voucher LoginPage dinonaktifkan.') } catch (e) { form.enabled = Boolean(setting.value.enabled); showError(e?.response?.data?.message || 'Setting LoginPage gagal disimpan.') } finally { saving.value = false } }
|
||||
async function copyScript() { try { await navigator.clipboard.writeText(data.value?.embed_script || ''); showSuccess('Script berhasil disalin.') } catch { showError('Browser tidak mengizinkan menyalin script.') } }
|
||||
onMounted(load)
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="brdvx-page-container">
|
||||
<div class="brdvx-page-header">
|
||||
<h5 class="mb-0 fw-semibold">Voucher LoginPage
|
||||
</h5>
|
||||
<small class="text-body-secondary">Tampilkan pembelian voucher langsung pada halaman login hotspot
|
||||
</small>
|
||||
</div>
|
||||
<div v-if="loading" class="brdvx-loading-state">
|
||||
<CSpinner color="primary" />
|
||||
<p class="brdvx-loading-text">Memuat data...
|
||||
</p>
|
||||
</div>
|
||||
<CRow v-else class="g-3 justify-content-center">
|
||||
<CCol :lg="5">
|
||||
<CCard class="setting-card">
|
||||
<CCardBody>
|
||||
<div class="toggle-head">
|
||||
<span class="feature-icon">
|
||||
<CIcon icon="cil-barcode" size="xl" />
|
||||
</span>
|
||||
<div>
|
||||
<h6 class="mb-1">Penjualan di LoginPage
|
||||
</h6>
|
||||
<small class="text-body-secondary">Membutuhkan payment gateway tenant yang aktif
|
||||
</small>
|
||||
</div>
|
||||
<CFormSwitch v-model="form.enabled" size="xl" />
|
||||
</div>
|
||||
<hr />
|
||||
<CFormLabel>Judul Widget
|
||||
</CFormLabel>
|
||||
<CFormInput v-model="form.title" class="mb-3" />
|
||||
<CFormLabel>Warna Tema
|
||||
</CFormLabel>
|
||||
<CFormInput v-model="form.theme_color" type="color" class="mb-3 color-input" />
|
||||
<CFormLabel>Paket yang Ditampilkan
|
||||
</CFormLabel>
|
||||
<CFormSelect v-model="form.allowed_package_profile_ids" multiple class="mb-3">
|
||||
<option v-for="item in packages" :key="item.id" :value="item.id">{{ item.name }} · {{ money(item.price) }}
|
||||
</option>
|
||||
</CFormSelect>
|
||||
<CButton color="primary" class="w-100" :disabled="saving" @click="save">Simpan Pengaturan
|
||||
</CButton>
|
||||
</CCardBody>
|
||||
</CCard>
|
||||
<CCard v-if="form.enabled" class="mt-3 setting-card">
|
||||
<CCardHeader class="d-flex justify-content-between align-items-center">
|
||||
<strong>Script LoginPage
|
||||
</strong>
|
||||
<CButton color="secondary" variant="ghost" size="sm" title="Salin script" @click="copyScript">
|
||||
<CIcon icon="cil-copy" /> Salin
|
||||
</CButton>
|
||||
</CCardHeader>
|
||||
<CCardBody>
|
||||
<pre>
|
||||
<code>{{ data?.embed_script }}
|
||||
</code>
|
||||
</pre>
|
||||
<CAlert color="warning" class="mb-0">
|
||||
<strong>Petunjuk pemasangan
|
||||
</strong>
|
||||
<div>Masukkan script ini ke file login page MikroTik pada posisi tempat daftar voucher ingin ditampilkan. Pastikan elemen dan script tetap berurutan.
|
||||
</div>
|
||||
</CAlert>
|
||||
</CCardBody>
|
||||
</CCard>
|
||||
</CCol>
|
||||
<CCol v-if="form.enabled" :xs="7">
|
||||
<CCard>
|
||||
<CCardHeader>
|
||||
<strong>Riwayat Penjualan LoginPage
|
||||
</strong>
|
||||
<CBadge color="secondary">Data Dummy
|
||||
</CBadge>
|
||||
</CCardHeader>
|
||||
<CCardBody class="p-0">
|
||||
<div class="table-responsive">
|
||||
<CTable hover class="mb-0">
|
||||
<CTableHead>
|
||||
<CTableRow>
|
||||
<CTableHeaderCell>Referensi
|
||||
</CTableHeaderCell>
|
||||
<CTableHeaderCell>Pembeli
|
||||
</CTableHeaderCell>
|
||||
<CTableHeaderCell>Paket
|
||||
</CTableHeaderCell>
|
||||
<CTableHeaderCell>Nominal
|
||||
</CTableHeaderCell>
|
||||
<CTableHeaderCell>Status
|
||||
</CTableHeaderCell>
|
||||
<CTableHeaderCell>Waktu
|
||||
</CTableHeaderCell>
|
||||
</CTableRow>
|
||||
</CTableHead>
|
||||
<CTableBody>
|
||||
<CTableRow v-for="item in dummySales" :key="item.id">
|
||||
<CTableDataCell>{{ item.reference }}
|
||||
</CTableDataCell>
|
||||
<CTableDataCell>{{ item.buyer }}
|
||||
</CTableDataCell>
|
||||
<CTableDataCell>{{ item.package }}
|
||||
</CTableDataCell>
|
||||
<CTableDataCell>{{ money(item.amount) }}
|
||||
</CTableDataCell>
|
||||
<CTableDataCell>
|
||||
<CBadge :color="item.status === 'paid' ? 'success' : 'warning'">{{ item.status === 'paid' ? 'Lunas' : 'Menunggu' }}
|
||||
</CBadge>
|
||||
</CTableDataCell>
|
||||
<CTableDataCell>{{ new Date(item.created_at).toLocaleString('id-ID') }}
|
||||
</CTableDataCell>
|
||||
</CTableRow>
|
||||
</CTableBody>
|
||||
</CTable>
|
||||
</div>
|
||||
</CCardBody>
|
||||
</CCard>
|
||||
</CCol>
|
||||
</CRow>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>.setting-card{border-top:3px solid #ff8c00}.toggle-head{display:flex;align-items:center;gap:.75rem}.toggle-head>div{min-width:0;flex:1}.feature-icon{display:inline-flex;width:44px;height:44px;align-items:center;justify-content:center;color:#fff;background:linear-gradient(135deg,#f97316,#ff8c00);border-radius:.75rem}.color-input{width:100%;height:42px}pre{max-height:16rem;padding:1rem;overflow:auto;color:#e8e8e8;background:#1e1e2d;border-radius:.75rem;white-space:pre-wrap;word-break:break-all}
|
||||
</style>
|
||||
@@ -0,0 +1,27 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import BaseResponsiveDataView from '@/components/base/BaseResponsiveDataView.vue'
|
||||
import { createVoucherSale, getVoucherOptions, getVoucherSale, getVoucherSales } from '@/services/voucherService'
|
||||
import { showError, showSuccess } from '@/utils/swal'
|
||||
|
||||
const loading = ref(true), loaded = ref(false), saving = ref(false), modal = ref(false), detailModal = ref(false)
|
||||
const items = ref([]), packages = ref([]), mikrotiks = ref([]), detail = ref(null)
|
||||
const query = ref({ page: 1, per_page: 10 }), pagination = ref({ current_page: 1, last_page: 1, per_page: 10, total: 0 })
|
||||
const form = reactive({ nas_package_profile_id: '', nas_mikrotik_id: '', quantity: 1, notes: '' })
|
||||
const selectedPackage = computed(() => packages.value.find((x) => Number(x.id) === Number(form.nas_package_profile_id)))
|
||||
const estimatedTotal = computed(() => Number(selectedPackage.value?.price || 0) * Number(form.quantity || 0))
|
||||
const columns = [{ key: 'sale_number', label: 'Nomor Penjualan' }, { key: 'package_label', label: 'Paket' }, { key: 'quantity', label: 'Jumlah' }, { key: 'agent_label', label: 'Penjual/Agen' }, { key: 'total_label', label: 'Total' }, { key: 'payment_label', label: 'Pembayaran' }, { key: 'created_label', label: 'Tanggal' }]
|
||||
const actions = [{ key: 'detail', label: 'Lihat Voucher', color: 'primary' }]
|
||||
const money = (value) => `Rp ${Number(value || 0).toLocaleString('id-ID')}`
|
||||
|
||||
function normalize(x) { return { ...x, package_label: x.package_profile?.name || '-', agent_label: x.agent?.user?.name || x.creator?.name || 'Tenant', total_label: money(x.total_amount), payment_label: x.payment_status === 'credit' ? 'Kredit/Belum Dibayar' : 'Lunas', created_label: new Date(x.created_at).toLocaleString('id-ID') } }
|
||||
async function fetchData(params = query.value) { query.value = { ...query.value, ...params }; loading.value = true; try { const r = await getVoucherSales(query.value), rows = r?.data?.data || []; items.value = rows.map(normalize); pagination.value = { current_page: r?.data?.current_page || 1, last_page: r?.data?.last_page || 1, per_page: r?.data?.per_page || 10, total: r?.data?.total || 0 } } catch (e) { showError(e?.response?.data?.message || 'Gagal memuat penjualan voucher.') } finally { loading.value = false; loaded.value = true } }
|
||||
async function openCreate() { try { const r = await getVoucherOptions(); packages.value = r?.data?.packages || []; mikrotiks.value = r?.data?.mikrotiks || []; Object.assign(form, { nas_package_profile_id: packages.value[0]?.id || '', nas_mikrotik_id: '', quantity: 1, notes: '' }); modal.value = true } catch (e) { showError(e?.response?.data?.message || 'Gagal memuat pilihan voucher.') } }
|
||||
async function submit() { if (!form.nas_package_profile_id) return showError('Profile paket hotspot wajib dipilih.'); saving.value = true; try { const r = await createVoucherSale(form); modal.value = false; detail.value = r?.data; detailModal.value = true; showSuccess('Voucher berhasil dibuat dan menunggu sinkronisasi MikroTik.'); await fetchData() } catch (e) { showError(e?.response?.data?.message || 'Voucher gagal dibuat.') } finally { saving.value = false } }
|
||||
async function showDetail(item) { try { detail.value = (await getVoucherSale(item.id))?.data; detailModal.value = true } catch (e) { showError(e?.response?.data?.message || 'Detail voucher gagal dimuat.') } }
|
||||
onMounted(fetchData)
|
||||
</script>
|
||||
<template><div class="brdvx-page-container"><div class="brdvx-page-header d-flex justify-content-between align-items-center gap-2"><div><h5 class="mb-0 fw-semibold">Jual Voucher</h5><small class="text-body-secondary">Generate voucher hotspot; sinkronisasi MikroTik akan ditambahkan pada tahap integrasi</small></div><CButton color="primary" size="sm" @click="openCreate">+ Buat Voucher</CButton></div><div v-if="loading && !loaded" class="brdvx-loading-state"><CSpinner color="primary" /><p class="brdvx-loading-text">Memuat data...</p></div><BaseResponsiveDataView v-else :columns="columns" :items="items" :actions="actions" server-side :pagination="pagination" :loading="loading" @query-change="fetchData" @action-click="({ item }) => showDetail(item)" /></div>
|
||||
<CModal :visible="modal" @close="modal = false"><CModalHeader><CModalTitle>Buat Voucher Hotspot</CModalTitle></CModalHeader><CModalBody><CFormLabel>Profile Paket Hotspot</CFormLabel><CFormSelect v-model="form.nas_package_profile_id" class="mb-3"><option v-for="item in packages" :key="item.id" :value="item.id">{{ item.name }} · {{ money(item.price) }}</option></CFormSelect><CFormLabel>Target MikroTik</CFormLabel><CFormSelect v-model="form.nas_mikrotik_id" class="mb-3"><option value="">Belum ditentukan</option><option v-for="item in mikrotiks" :key="item.id" :value="item.id">{{ item.name }}</option></CFormSelect><CFormLabel>Jumlah Voucher</CFormLabel><CFormInput v-model.number="form.quantity" type="number" min="1" max="100" class="mb-3" /><div class="price-summary"><span>Estimasi harga normal</span><strong>{{ money(estimatedTotal) }}</strong><small>Harga akhir agen mengikuti diskon, saldo deposit, atau limit kredit yang ditetapkan tenant.</small></div><CFormLabel class="mt-3">Catatan</CFormLabel><CFormTextarea v-model="form.notes" rows="2" /></CModalBody><CModalFooter><CButton color="secondary" @click="modal = false">Batal</CButton><CButton color="primary" :disabled="saving" @click="submit">Generate Voucher</CButton></CModalFooter></CModal>
|
||||
<CModal :visible="detailModal" size="lg" @close="detailModal = false"><CModalHeader><CModalTitle>{{ detail?.sale_number || 'Detail Voucher' }}</CModalTitle></CModalHeader><CModalBody><div class="voucher-list"><div v-for="voucher in detail?.vouchers || []" :key="voucher.id" class="voucher-code"><span><small>USERNAME</small><strong>{{ voucher.username }}</strong></span><span><small>PASSWORD</small><strong>{{ voucher.password }}</strong></span><CBadge color="warning">{{ voucher.sync_status }}</CBadge></div></div></CModalBody></CModal></template>
|
||||
<style scoped>.price-summary{display:flex;flex-direction:column;padding:.8rem;color:#fff;background:linear-gradient(135deg,#f97316,#ff8c00);border-radius:.8rem}.price-summary strong{font-size:1.35rem}.price-summary small{opacity:.8}.voucher-list{display:grid;grid-template-columns:repeat(auto-fit,minmax(230px,1fr));gap:.75rem}.voucher-code{display:grid;grid-template-columns:1fr 1fr auto;align-items:center;gap:.6rem;padding:.8rem;border:1px dashed #ff8c00;border-radius:.75rem}.voucher-code span{display:flex;min-width:0;flex-direction:column}.voucher-code small{color:var(--cui-secondary-color);font-size:.62rem}.voucher-code strong{overflow:hidden;text-overflow:ellipsis}</style>
|
||||
@@ -1,171 +1,78 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||
import BaseResponsiveDataView from '@/components/base/BaseResponsiveDataView.vue'
|
||||
import { getMyWallets, getWalletLedger, transferWallet, withdrawWallet } from '@/services/walletService'
|
||||
import { getMyWallets, getWalletLedger, withdrawWallet } from '@/services/walletService'
|
||||
import { showError, showSuccess } from '@/utils/swal'
|
||||
|
||||
const loading = ref(true), wallets = ref([]), activeId = ref(null), ledger = ref([]), modal = ref(''), saving = ref(false)
|
||||
const loading = ref(true), wallets = ref([]), activeId = ref(null), ledger = ref([]), modal = ref(false), saving = ref(false)
|
||||
const pagination = ref({ current_page: 1, last_page: 1, per_page: 10, total: 0 }), query = ref({ page: 1, per_page: 10 })
|
||||
const form = reactive({ amount: '', destination_wallet_id: '', bank_code: '', account_number: '', account_name: '' })
|
||||
const active = computed(() => wallets.value.find((x) => x.id === activeId.value) || wallets.value[0])
|
||||
const otherWallets = computed(() => wallets.value.filter((x) => x.id !== active.value?.id))
|
||||
const form = reactive({ amount: '', bank_code: '', account_number: '', account_name: '' })
|
||||
const active = computed(() => wallets.value.find((item) => item.id === activeId.value) || wallets.value[0])
|
||||
const columns = [{ key: 'date', label: 'Tanggal' }, { key: 'number', label: 'Referensi' }, { key: 'type', label: 'Jenis' }, { key: 'debit', label: 'Debit' }, { key: 'credit', label: 'Kredit' }, { key: 'balance', label: 'Saldo' }]
|
||||
const money = (value) => `Rp ${Number(value || 0).toLocaleString('id-ID')}`
|
||||
async function loadWallets() { loading.value = true; try { wallets.value = (await getMyWallets())?.data || []; activeId.value ||= wallets.value[0]?.id; await loadLedger() } catch (e) { showError(e?.response?.data?.message || 'Gagal memuat dompet.') } finally { loading.value = false } }
|
||||
|
||||
async function loadBalances() {
|
||||
loading.value = true
|
||||
try {
|
||||
wallets.value = (await getMyWallets())?.data || []
|
||||
if (!wallets.value.some((item) => item.id === activeId.value)) activeId.value = wallets.value[0]?.id
|
||||
await loadLedger()
|
||||
} catch (error) { showError(error?.response?.data?.message || 'Gagal memuat saldo.') }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
async function loadLedger(params = query.value) {
|
||||
if (!active.value) return
|
||||
if (!active.value?.ledger_available) {
|
||||
ledger.value = []; pagination.value = { current_page: 1, last_page: 1, per_page: 10, total: 0 }; return
|
||||
}
|
||||
query.value = { ...query.value, ...params }
|
||||
const response = await getWalletLedger(active.value.id, query.value), rows = response?.data?.data || []
|
||||
ledger.value = rows.map((x) => ({ ...x, date: new Date(x.created_at).toLocaleString('id-ID'), number: x.transaction?.transaction_number, type: x.transaction?.type, debit: x.entry_type === 'debit' ? money(x.amount) : '-', credit: x.entry_type === 'credit' ? money(x.amount) : '-', balance: money(x.balance_after) }))
|
||||
ledger.value = rows.map((item) => ({ ...item, date: new Date(item.created_at).toLocaleString('id-ID'), number: item.transaction?.transaction_number, type: item.transaction?.type, debit: item.entry_type === 'debit' ? money(item.amount) : '-', credit: item.entry_type === 'credit' ? money(item.amount) : '-', balance: money(item.balance_after) }))
|
||||
pagination.value = { current_page: response?.data?.current_page || 1, last_page: response?.data?.last_page || 1, per_page: response?.data?.per_page || 10, total: response?.data?.total || 0 }
|
||||
}
|
||||
function open(type) { Object.assign(form, { amount: '', destination_wallet_id: otherWallets.value[0]?.id || '', bank_code: '', account_number: '', account_name: '' }); modal.value = type }
|
||||
async function submit() {
|
||||
function openWithdrawal() { Object.assign(form, { amount: '', bank_code: '', account_number: '', account_name: '' }); modal.value = true }
|
||||
async function submitWithdrawal() {
|
||||
saving.value = true
|
||||
try {
|
||||
if (modal.value === 'transfer') await transferWallet(active.value.id, { destination_wallet_id: form.destination_wallet_id, amount: form.amount })
|
||||
if (modal.value === 'withdraw') await withdrawWallet(active.value.id, { amount: form.amount, bank_code: form.bank_code, account_number: form.account_number, account_name: form.account_name })
|
||||
modal.value = ''; showSuccess('Permintaan berhasil diproses.'); await loadWallets()
|
||||
} catch (e) { showError(e?.response?.data?.message || 'Transaksi dompet gagal.') } finally { saving.value = false }
|
||||
try { await withdrawWallet(active.value.id, form); modal.value = false; showSuccess('Permintaan penarikan settlement berhasil dibuat.'); await loadBalances() }
|
||||
catch (error) { showError(error?.response?.data?.message || 'Penarikan saldo settlement gagal.') }
|
||||
finally { saving.value = false }
|
||||
}
|
||||
watch(activeId, loadLedger)
|
||||
onMounted(loadWallets)
|
||||
onMounted(loadBalances)
|
||||
</script>
|
||||
<template><div class="brdvx-page-container"><div class="brdvx-page-header"><h5 class="mb-0 fw-semibold">Dompet</h5><small class="text-body-secondary">Saldo user dan tenant dengan pencatatan ledger</small></div><div v-if="loading" class="brdvx-loading-state"><CSpinner color="primary" /><p class="brdvx-loading-text">Memuat data...</p></div><template v-else>
|
||||
<div class="wallet-grid"><button v-for="wallet in wallets" :key="wallet.id" class="wallet-card" :class="{ active: active?.id === wallet.id }" @click="activeId = wallet.id"><span>{{ wallet.wallet_type === 'tenant' ? 'DOMPET TENANT' : 'DOMPET USER' }}</span><strong>{{ money(wallet.available_balance) }}</strong><small>Pending {{ money(wallet.pending_balance) }} · IDR</small></button></div>
|
||||
<div class="wallet-actions">
|
||||
<button type="button" class="wallet-action" disabled>
|
||||
<span class="wallet-action-icon topup"><CIcon icon="cil-data-transfer-down" size="xl" /></span>
|
||||
<span><strong>Isi Saldo</strong><small>Tambahkan saldo melalui payment gateway</small></span>
|
||||
</button>
|
||||
<button type="button" class="wallet-action" :disabled="!active" @click="open('withdraw')">
|
||||
<span class="wallet-action-icon withdraw"><CIcon icon="cil-data-transfer-up" size="xl" /></span>
|
||||
<span><strong>Tarik Saldo</strong><small>Kirim saldo ke rekening bank</small></span>
|
||||
</button>
|
||||
<button type="button" class="wallet-action" :disabled="!otherWallets.length" @click="open('transfer')">
|
||||
<span class="wallet-action-icon transfer"><CIcon icon="cil-transfer" size="xl" /></span>
|
||||
<span><strong>Pindah Saldo</strong><small>Transfer antara dompet user dan tenant</small></span>
|
||||
</button>
|
||||
|
||||
<template>
|
||||
<div class="brdvx-page-container">
|
||||
<div class="brdvx-page-header"><h5 class="mb-0 fw-semibold">Saldo Deposit</h5><small class="text-body-secondary">Saldo layanan user, settlement tenant, dan fasilitas kredit voucher pada tenant aktif</small></div>
|
||||
<div v-if="loading" class="brdvx-loading-state"><CSpinner color="primary" /><p class="brdvx-loading-text">Memuat data...</p></div>
|
||||
<template v-else>
|
||||
<div class="wallet-grid">
|
||||
<button v-for="balance in wallets" :key="balance.id" type="button" class="wallet-card" :class="{ active: active?.id === balance.id, credit: balance.balance_kind === 'credit' }" @click="activeId = balance.id">
|
||||
<span>{{ (balance.label || 'Saldo Deposit').toUpperCase() }}</span><strong>{{ money(balance.available_balance) }}</strong>
|
||||
<template v-if="balance.balance_kind === 'credit'"><small>Limit {{ money(balance.credit_limit) }} · Terpakai {{ money(balance.used_credit) }}</small><small class="usage-note">Khusus pembelian voucher hotspot</small></template>
|
||||
<template v-else><small>Pending {{ money(balance.pending_balance) }} · IDR</small><small class="usage-note">{{ balance.usage_note }}</small></template>
|
||||
</button>
|
||||
</div>
|
||||
<div class="wallet-actions">
|
||||
<button v-if="active?.balance_kind === 'deposit'" type="button" class="wallet-action" disabled><span class="wallet-action-icon topup"><CIcon icon="cil-data-transfer-down" size="xl" /></span><span><strong>Isi Saldo Deposit</strong><small>Tambahkan saldo untuk transaksi layanan tenant</small></span></button>
|
||||
<button v-if="active?.can_withdraw" type="button" class="wallet-action" @click="openWithdrawal"><span class="wallet-action-icon withdraw"><CIcon icon="cil-data-transfer-up" size="xl" /></span><span><strong>Tarik Settlement</strong><small>Kirim saldo settlement tenant ke rekening bank</small></span></button>
|
||||
<div v-if="active?.balance_kind === 'credit'" class="credit-information"><CIcon icon="cil-info" size="xl" /><span><strong>Fasilitas Kredit Voucher</strong><small>Sisa kredit berkurang saat membeli voucher dan bertambah kembali setelah tagihan agen dibayar.</small></span></div>
|
||||
</div>
|
||||
<template v-if="active?.ledger_available"><h6 class="mt-4">Mutasi {{ active?.label }}</h6><BaseResponsiveDataView :columns="columns" :items="ledger" server-side :pagination="pagination" @query-change="loadLedger" /></template>
|
||||
<CAlert v-else-if="active?.balance_kind === 'credit'" color="info" class="mt-4 mb-0">Riwayat pemakaian saldo kredit tersedia pada transaksi penjualan Voucher Hotspot.</CAlert>
|
||||
</template>
|
||||
</div>
|
||||
<h6 class="mt-4">Mutasi {{ active?.wallet_type === 'tenant' ? 'Dompet Tenant' : 'Dompet User' }}</h6><BaseResponsiveDataView :columns="columns" :items="ledger" server-side :pagination="pagination" @query-change="loadLedger" />
|
||||
</template></div>
|
||||
<CModal :visible="Boolean(modal)" @close="modal = ''"><CModalHeader><CModalTitle>{{ modal === 'transfer' ? 'Pindah Dompet' : 'Tarik Saldo' }}</CModalTitle></CModalHeader><CModalBody><CFormLabel>Nominal</CFormLabel><CFormInput v-model.number="form.amount" type="number" min="1" class="mb-3" /><template v-if="modal === 'transfer'"><CFormLabel>Dompet Tujuan</CFormLabel><CFormSelect v-model="form.destination_wallet_id"><option v-for="item in otherWallets" :key="item.id" :value="item.id">{{ item.wallet_type === 'tenant' ? 'Dompet Tenant' : 'Dompet User' }}</option></CFormSelect></template><template v-else><CFormLabel>Bank</CFormLabel><CFormInput v-model="form.bank_code" class="mb-2" /><CFormLabel>Nomor Rekening</CFormLabel><CFormInput v-model="form.account_number" class="mb-2" /><CFormLabel>Nama Pemilik</CFormLabel><CFormInput v-model="form.account_name" /></template></CModalBody><CModalFooter><CButton color="secondary" @click="modal = ''">Batal</CButton><CButton color="primary" :disabled="saving" @click="submit">Proses</CButton></CModalFooter></CModal></template>
|
||||
<CModal :visible="modal" @close="modal = false"><CModalHeader><CModalTitle>Tarik Saldo Settlement</CModalTitle></CModalHeader><CModalBody><CFormLabel>Nominal</CFormLabel><CFormInput v-model.number="form.amount" type="number" min="1" class="mb-3" /><CFormLabel>Bank</CFormLabel><CFormInput v-model="form.bank_code" class="mb-2" /><CFormLabel>Nomor Rekening</CFormLabel><CFormInput v-model="form.account_number" class="mb-2" /><CFormLabel>Nama Pemilik</CFormLabel><CFormInput v-model="form.account_name" /></CModalBody><CModalFooter><CButton color="secondary" @click="modal = false">Batal</CButton><CButton color="primary" :disabled="saving" @click="submitWithdrawal">Proses</CButton></CModalFooter></CModal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.wallet-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(100%, 280px), 400px));
|
||||
gap: 1rem;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.wallet-card {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
min-height: 130px;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding: 1.25rem;
|
||||
color: white;
|
||||
text-align: left;
|
||||
background: linear-gradient(135deg, #f97316, #ff8c00 58%, #f9b115);
|
||||
border: 3px solid transparent;
|
||||
border-radius: 1rem;
|
||||
box-shadow: 0 0.5rem 1rem rgba(249, 115, 22, 0.22);
|
||||
}
|
||||
|
||||
.wallet-card.active {
|
||||
border-color: rgba(255, 255, 255, 0.85);
|
||||
}
|
||||
|
||||
.wallet-card span,
|
||||
.wallet-card small {
|
||||
opacity: 0.82;
|
||||
}
|
||||
|
||||
.wallet-card strong {
|
||||
margin: 0.4rem 0;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.wallet-actions {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(100%, 210px), 250px));
|
||||
justify-content: center;
|
||||
gap: 0.75rem;
|
||||
max-width: 800px;
|
||||
margin: 1.25rem auto 0;
|
||||
}
|
||||
|
||||
.wallet-action {
|
||||
display: flex;
|
||||
min-height: 78px;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem;
|
||||
color: var(--cui-body-color);
|
||||
text-align: left;
|
||||
background: var(--cui-body-bg);
|
||||
border: 1px solid var(--cui-border-color);
|
||||
border-radius: 0.85rem;
|
||||
box-shadow: 0 0.25rem 0.75rem rgba(0, 0, 0, 0.06);
|
||||
transition: transform 0.18s ease, border-color 0.18s ease, box-shadow 0.18s ease;
|
||||
}
|
||||
|
||||
.wallet-action:not(:disabled):hover {
|
||||
border-color: #ff8c00;
|
||||
box-shadow: 0 0.4rem 1rem rgba(249, 115, 22, 0.14);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.wallet-action:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.wallet-action > span:last-child {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.wallet-action strong {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.wallet-action small {
|
||||
margin-top: 0.15rem;
|
||||
color: var(--cui-secondary-color);
|
||||
font-size: 0.7rem;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.wallet-action-icon {
|
||||
display: inline-flex;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
flex: 0 0 44px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
border-radius: 0.75rem;
|
||||
}
|
||||
|
||||
.wallet-action-icon.topup {
|
||||
background: linear-gradient(135deg, #f97316, #f9b115);
|
||||
}
|
||||
|
||||
.wallet-action-icon.withdraw {
|
||||
background: linear-gradient(135deg, #2eb85c, #1f9d55);
|
||||
}
|
||||
|
||||
.wallet-action-icon.transfer {
|
||||
background: linear-gradient(135deg, #3399ff, #5856d6);
|
||||
}
|
||||
|
||||
@media (max-width: 575.98px) {
|
||||
.wallet-action {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
.wallet-grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(min(100%,280px),400px)); justify-content:center; gap:1rem; }
|
||||
.wallet-card { display:flex; width:100%; max-width:400px; min-height:150px; flex-direction:column; justify-content:center; padding:1.25rem; color:white; text-align:left; background:linear-gradient(135deg,#f97316,#ff8c00 58%,#f9b115); border:3px solid transparent; border-radius:1rem; box-shadow:0 .5rem 1rem rgba(249,115,22,.22); }
|
||||
.wallet-card.active { border-color:rgba(255,255,255,.88); }.wallet-card.credit { background:linear-gradient(135deg,#5856d6,#3399ff); box-shadow:0 .5rem 1rem rgba(51,153,255,.2); }
|
||||
.wallet-card span,.wallet-card small { opacity:.84; }.wallet-card strong { margin:.4rem 0; font-size:1.5rem; }.usage-note { margin-top:.4rem; font-size:.68rem; }
|
||||
.wallet-actions { display:grid; grid-template-columns:repeat(auto-fit,minmax(min(100%,230px),300px)); justify-content:center; gap:.75rem; max-width:800px; margin:1.25rem auto 0; }
|
||||
.wallet-action,.credit-information { display:flex; min-height:78px; align-items:center; gap:.75rem; padding:.75rem; text-align:left; border-radius:.85rem; }
|
||||
.wallet-action { color:var(--cui-body-color); background:var(--cui-body-bg); border:1px solid var(--cui-border-color); box-shadow:0 .25rem .75rem rgba(0,0,0,.06); }.wallet-action:disabled { cursor:not-allowed; opacity:.55; }
|
||||
.wallet-action>span:last-child,.credit-information span { display:flex; min-width:0; flex-direction:column; }.wallet-action small,.credit-information small { margin-top:.15rem; font-size:.7rem; line-height:1.25; }
|
||||
.wallet-action-icon { display:inline-flex; width:44px; height:44px; flex:0 0 44px; align-items:center; justify-content:center; color:white; border-radius:.75rem; }.wallet-action-icon.topup { background:linear-gradient(135deg,#f97316,#f9b115); }.wallet-action-icon.withdraw { background:linear-gradient(135deg,#2eb85c,#1f9d55); }
|
||||
.credit-information { color:var(--cui-info-text-emphasis); background:var(--cui-info-bg-subtle); border:1px solid var(--cui-info-border-subtle); }
|
||||
</style>
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user