feat: update admin page
This commit is contained in:
@@ -4,9 +4,14 @@
|
||||
"": {
|
||||
"name": "admin",
|
||||
"dependencies": {
|
||||
"@primeuix/themes": "^1.2.5",
|
||||
"@tailwindcss/vite": "^4.1.16",
|
||||
"@vueuse/core": "^14.0.0",
|
||||
"axios": "^1.13.1",
|
||||
"daisyui": "^5.4.2",
|
||||
"mitt": "^3.0.1",
|
||||
"pinia": "^3.0.3",
|
||||
"primevue": "^4.4.1",
|
||||
"tailwindcss": "^4.1.16",
|
||||
"vue": "^3.5.22",
|
||||
"vue-router": "^4.6.3",
|
||||
@@ -239,6 +244,18 @@
|
||||
|
||||
"@prettier/plugin-oxc": ["@prettier/plugin-oxc@0.0.4", "", { "dependencies": { "oxc-parser": "0.74.0" } }, "sha512-UGXe+g/rSRbglL0FOJiar+a+nUrst7KaFmsg05wYbKiInGWP6eAj/f8A2Uobgo5KxEtb2X10zeflNH6RK2xeIQ=="],
|
||||
|
||||
"@primeuix/styled": ["@primeuix/styled@0.7.4", "", { "dependencies": { "@primeuix/utils": "^0.6.1" } }, "sha512-QSO/NpOQg8e9BONWRBx9y8VGMCMYz0J/uKfNJEya/RGEu7ARx0oYW0ugI1N3/KB1AAvyGxzKBzGImbwg0KUiOQ=="],
|
||||
|
||||
"@primeuix/styles": ["@primeuix/styles@1.2.5", "", { "dependencies": { "@primeuix/styled": "^0.7.3" } }, "sha512-nypFRct/oaaBZqP4jinT0puW8ZIfs4u+l/vqUFmJEPU332fl5ePj6DoOpQgTLzo3OfmvSmz5a5/5b4OJJmmi7Q=="],
|
||||
|
||||
"@primeuix/themes": ["@primeuix/themes@1.2.5", "", { "dependencies": { "@primeuix/styled": "^0.7.3" } }, "sha512-n3YkwJrHQaEESc/D/A/iD815sxp8cKnmzscA6a8Tm8YvMtYU32eCahwLLe6h5rywghVwxASWuG36XBgISYOIjQ=="],
|
||||
|
||||
"@primeuix/utils": ["@primeuix/utils@0.6.2", "", {}, "sha512-l+li4z6UAwamqbu4xodN3lqr1G4S37DKEJ7Y/nE3MjEx7zJ+KIBkb5voF26Nuly1UCmIcRWQJokhLRF7+sRhyg=="],
|
||||
|
||||
"@primevue/core": ["@primevue/core@4.4.1", "", { "dependencies": { "@primeuix/styled": "^0.7.4", "@primeuix/utils": "^0.6.1" }, "peerDependencies": { "vue": "^3.5.0" } }, "sha512-RG56iDKIJT//EtntjQzOiWOHZZJczw/qWWtdL5vFvw8/QDS9DPKn8HLpXK7N5Le6KK1MLXUsxoiGTZK+poUFUg=="],
|
||||
|
||||
"@primevue/icons": ["@primevue/icons@4.4.1", "", { "dependencies": { "@primeuix/utils": "^0.6.1", "@primevue/core": "4.4.1" } }, "sha512-UfDimrIjVdY6EziwieyV4zPKzW6mnKHKhy4Dgyjv2oI6pNeuim+onbJo1ce22PEGXW78vfblG/3/JIzVHFweqQ=="],
|
||||
|
||||
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.29", "", {}, "sha512-NIJgOsMjbxAXvoGq/X0gD7VPMQ8j9g0BiDaNjVNVjvl+iKXxL3Jre0v31RmBYeLEmkbj2s02v8vFTbUXi5XS2Q=="],
|
||||
|
||||
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.52.5", "", { "os": "android", "cpu": "arm" }, "sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ=="],
|
||||
@@ -321,6 +338,8 @@
|
||||
|
||||
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
|
||||
|
||||
"@types/web-bluetooth": ["@types/web-bluetooth@0.0.21", "", {}, "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA=="],
|
||||
|
||||
"@vitejs/plugin-vue": ["@vitejs/plugin-vue@6.0.1", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-beta.29" }, "peerDependencies": { "vite": "^5.0.0 || ^6.0.0 || ^7.0.0", "vue": "^3.2.25" } }, "sha512-+MaE752hU0wfPFJEUAIxqw18+20euHHdxVtMvbFcOEpjEyfqXH/5DCoTHiVJ0J29EhTJdoTkjEv5YBKU9dnoTw=="],
|
||||
|
||||
"@vue/babel-helper-vue-transform-on": ["@vue/babel-helper-vue-transform-on@1.5.0", "", {}, "sha512-0dAYkerNhhHutHZ34JtTl2czVQHUNWv6xEbkdF5W+Yrv5pCWsqjeORdOgbtW2I9gWlt+wBmVn+ttqN9ZxR5tzA=="],
|
||||
@@ -357,6 +376,12 @@
|
||||
|
||||
"@vue/shared": ["@vue/shared@3.5.22", "", {}, "sha512-F4yc6palwq3TT0u+FYf0Ns4Tfl9GRFURDN2gWG7L1ecIaS/4fCIuFOjMTnCyjsu/OK6vaDKLCrGAa+KvvH+h4w=="],
|
||||
|
||||
"@vueuse/core": ["@vueuse/core@14.0.0", "", { "dependencies": { "@types/web-bluetooth": "^0.0.21", "@vueuse/metadata": "14.0.0", "@vueuse/shared": "14.0.0" }, "peerDependencies": { "vue": "^3.5.0" } }, "sha512-d6tKRWkZE8IQElX2aHBxXOMD478fHIYV+Dzm2y9Ag122ICBpNKtGICiXKOhWU3L1kKdttDD9dCMS4bGP3jhCTQ=="],
|
||||
|
||||
"@vueuse/metadata": ["@vueuse/metadata@14.0.0", "", {}, "sha512-6yoGqbJcMldVCevkFiHDBTB1V5Hq+G/haPlGIuaFZHpXC0HADB0EN1ryQAAceiW+ryS3niUwvdFbGiqHqBrfVA=="],
|
||||
|
||||
"@vueuse/shared": ["@vueuse/shared@14.0.0", "", { "peerDependencies": { "vue": "^3.5.0" } }, "sha512-mTCA0uczBgurRlwVaQHfG0Ja7UdGe4g9mwffiJmvLiTtp1G4AQyIjej6si/k8c8pUwTfVpNufck+23gXptPAkw=="],
|
||||
|
||||
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
|
||||
|
||||
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
|
||||
@@ -369,6 +394,10 @@
|
||||
|
||||
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
||||
|
||||
"asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
|
||||
|
||||
"axios": ["axios@1.13.1", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, "sha512-hU4EGxxt+j7TQijx1oYdAjw4xuIp1wRQSsbMFwSthCWeBQur1eF+qJ5iQ5sN3Tw8YRzQNKb8jszgBdMDVqwJcw=="],
|
||||
|
||||
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||
|
||||
"baseline-browser-mapping": ["baseline-browser-mapping@2.8.21", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-JU0h5APyQNsHOlAM7HnQnPToSDQoEBZqzu/YBlqDnEeymPnZDREeXJA3KBMQee+dKteAxZ2AtvQEvVYdZf241Q=="],
|
||||
@@ -383,6 +412,8 @@
|
||||
|
||||
"bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="],
|
||||
|
||||
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
|
||||
|
||||
"callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
|
||||
|
||||
"caniuse-lite": ["caniuse-lite@1.0.30001751", "", {}, "sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw=="],
|
||||
@@ -393,6 +424,8 @@
|
||||
|
||||
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
||||
|
||||
"combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
|
||||
|
||||
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
|
||||
|
||||
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
|
||||
@@ -417,8 +450,12 @@
|
||||
|
||||
"define-lazy-prop": ["define-lazy-prop@3.0.0", "", {}, "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg=="],
|
||||
|
||||
"delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
|
||||
|
||||
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||
|
||||
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
||||
|
||||
"electron-to-chromium": ["electron-to-chromium@1.5.243", "", {}, "sha512-ZCphxFW3Q1TVhcgS9blfut1PX8lusVi2SvXQgmEEnK4TCmE1JhH2JkjJN+DNt0pJJwfBri5AROBnz2b/C+YU9g=="],
|
||||
|
||||
"enhanced-resolve": ["enhanced-resolve@5.18.3", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww=="],
|
||||
@@ -427,6 +464,14 @@
|
||||
|
||||
"error-stack-parser-es": ["error-stack-parser-es@1.0.5", "", {}, "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA=="],
|
||||
|
||||
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
|
||||
|
||||
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
|
||||
|
||||
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
|
||||
|
||||
"es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="],
|
||||
|
||||
"esbuild": ["esbuild@0.25.11", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.11", "@esbuild/android-arm": "0.25.11", "@esbuild/android-arm64": "0.25.11", "@esbuild/android-x64": "0.25.11", "@esbuild/darwin-arm64": "0.25.11", "@esbuild/darwin-x64": "0.25.11", "@esbuild/freebsd-arm64": "0.25.11", "@esbuild/freebsd-x64": "0.25.11", "@esbuild/linux-arm": "0.25.11", "@esbuild/linux-arm64": "0.25.11", "@esbuild/linux-ia32": "0.25.11", "@esbuild/linux-loong64": "0.25.11", "@esbuild/linux-mips64el": "0.25.11", "@esbuild/linux-ppc64": "0.25.11", "@esbuild/linux-riscv64": "0.25.11", "@esbuild/linux-s390x": "0.25.11", "@esbuild/linux-x64": "0.25.11", "@esbuild/netbsd-arm64": "0.25.11", "@esbuild/netbsd-x64": "0.25.11", "@esbuild/openbsd-arm64": "0.25.11", "@esbuild/openbsd-x64": "0.25.11", "@esbuild/openharmony-arm64": "0.25.11", "@esbuild/sunos-x64": "0.25.11", "@esbuild/win32-arm64": "0.25.11", "@esbuild/win32-ia32": "0.25.11", "@esbuild/win32-x64": "0.25.11" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q=="],
|
||||
|
||||
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
||||
@@ -477,18 +522,36 @@
|
||||
|
||||
"flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="],
|
||||
|
||||
"follow-redirects": ["follow-redirects@1.15.11", "", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="],
|
||||
|
||||
"form-data": ["form-data@4.0.4", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow=="],
|
||||
|
||||
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||
|
||||
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||
|
||||
"gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
|
||||
|
||||
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
|
||||
|
||||
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
|
||||
|
||||
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
|
||||
|
||||
"globals": ["globals@16.4.0", "", {}, "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw=="],
|
||||
|
||||
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
|
||||
|
||||
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
||||
|
||||
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
|
||||
|
||||
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
|
||||
|
||||
"has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
|
||||
|
||||
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
||||
|
||||
"hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="],
|
||||
|
||||
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
||||
@@ -569,8 +632,14 @@
|
||||
|
||||
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
||||
|
||||
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
||||
|
||||
"memorystream": ["memorystream@0.3.1", "", {}, "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw=="],
|
||||
|
||||
"mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||
|
||||
"mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||
|
||||
"minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
|
||||
|
||||
"mitt": ["mitt@3.0.1", "", {}, "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="],
|
||||
@@ -633,6 +702,10 @@
|
||||
|
||||
"prettier-linter-helpers": ["prettier-linter-helpers@1.0.0", "", { "dependencies": { "fast-diff": "^1.1.2" } }, "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w=="],
|
||||
|
||||
"primevue": ["primevue@4.4.1", "", { "dependencies": { "@primeuix/styled": "^0.7.4", "@primeuix/styles": "^1.2.5", "@primeuix/utils": "^0.6.1", "@primevue/core": "4.4.1", "@primevue/icons": "4.4.1" } }, "sha512-JbHBa5k30pZ7mn/z4vYBOnyt5GrR15eM3X0wa3VanonxnFLYkTEx8OMh33aU6ndWeOfi7Ef57dOL3bTH+3f4hQ=="],
|
||||
|
||||
"proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="],
|
||||
|
||||
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
||||
|
||||
"read-package-json-fast": ["read-package-json-fast@4.0.0", "", { "dependencies": { "json-parse-even-better-errors": "^4.0.0", "npm-normalize-package-bin": "^4.0.0" } }, "sha512-qpt8EwugBWDw2cgE2W+/3oxC+KTez2uSVR8JU9Q36TXPAGCaozfQUs59v4j4GFpWTaw0i6hAZSvOmu1J0uOEUg=="],
|
||||
|
||||
@@ -16,9 +16,14 @@
|
||||
"format": "prettier --write src/"
|
||||
},
|
||||
"dependencies": {
|
||||
"@primeuix/themes": "^1.2.5",
|
||||
"@tailwindcss/vite": "^4.1.16",
|
||||
"@vueuse/core": "^14.0.0",
|
||||
"axios": "^1.13.1",
|
||||
"daisyui": "^5.4.2",
|
||||
"mitt": "^3.0.1",
|
||||
"pinia": "^3.0.3",
|
||||
"primevue": "^4.4.1",
|
||||
"tailwindcss": "^4.1.16",
|
||||
"vue": "^3.5.22",
|
||||
"vue-router": "^4.6.3"
|
||||
|
||||
@@ -1,10 +1,77 @@
|
||||
<template>
|
||||
<div v-if="! settings.error" class="tw:relative">
|
||||
<TopLead/>
|
||||
<ul class="nav nav-tabs">
|
||||
<li :class="{active: route.name === 'general'}">
|
||||
<RouterLink :to="{name: 'general'}">Общие</RouterLink>
|
||||
</li>
|
||||
|
||||
<li :class="{active: route.name === 'telegram'}">
|
||||
<RouterLink :to="{name: 'telegram'}">Telegram</RouterLink>
|
||||
</li>
|
||||
|
||||
<li :class="{active: route.name === 'metrics'}">
|
||||
<RouterLink :to="{name: 'metrics'}">Метрики</RouterLink>
|
||||
</li>
|
||||
|
||||
<li :class="{active: route.name === 'store'}">
|
||||
<RouterLink :to="{name: 'store'}">Магазин</RouterLink>
|
||||
</li>
|
||||
|
||||
<li :class="{active: route.name === 'texts'}">
|
||||
<RouterLink :to="{name: 'texts'}">Тексты</RouterLink>
|
||||
</li>
|
||||
|
||||
<li :class="{active: route.name === 'orders'}">
|
||||
<RouterLink :to="{name: 'orders'}">Заказы</RouterLink>
|
||||
</li>
|
||||
|
||||
<li :class="{active: route.name === 'slider'}">
|
||||
<RouterLink :to="{name: 'slider'}">Слайдер</RouterLink>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<section class="form-horizontal tab-content">
|
||||
<RouterView/>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<Button label="Сохранить настройки" @click="settings.saveSettings"/>
|
||||
</section>
|
||||
|
||||
<div v-if="settings.isLoading" class="tw:w-full tw:h-full tw:absolute tw:top-0 tw:left-0 tw:z-30 tw:backdrop-blur-sm">
|
||||
<div class="tw:fixed tw:top-0 tw:left-0 tw:w-full tw:h-full tw:flex tw:justify-center tw:items-center tw:z-40 tw:text-4xl">
|
||||
<i class="fa fa-spin fa-spinner tw:mr-5"></i>
|
||||
<div>Загрузка...</div>
|
||||
</div>
|
||||
</div>
|
||||
<Toast position="top-right"/>
|
||||
</div>
|
||||
|
||||
<div v-else class="tw:w-full tw:h-full tw:absolute tw:top-0 tw:left-0 tw:z-30 tw:backdrop-blur-sm">
|
||||
<div class="tw:fixed tw:top-0 tw:left-0 tw:w-full tw:h-full tw:flex tw:flex-col tw:justify-center tw:items-center tw:z-40">
|
||||
<i class="fa fa-ban tw:text-4xl"></i>
|
||||
<div class="tw:text-4xl">{{ settings.error }}</div>
|
||||
<div>Обратитесь в поддержку</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { RouterView } from 'vue-router'
|
||||
import {RouterView, useRoute} from 'vue-router';
|
||||
import {useSettingsStore} from "@/stores/settings.js";
|
||||
import Toast from 'primevue/toast';
|
||||
import { toastBus } from '@/utils/toastHelper';
|
||||
import {useToast} from "primevue";
|
||||
import Button from 'primevue/button';
|
||||
import TopLead from "@/components/TopLead.vue";
|
||||
|
||||
const route = useRoute();
|
||||
const settings = useSettingsStore();
|
||||
const toast = useToast();
|
||||
toastBus.on('show', (data) => toast.add(data));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<RouterView />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
|
||||
@@ -19,3 +19,23 @@
|
||||
all: unset !important;
|
||||
}
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
|
||||
.p-toast .p-toast-message-success {
|
||||
color: #3c763d;
|
||||
background-color: #dff0d8;
|
||||
border-color: #d6e9c6;
|
||||
}
|
||||
|
||||
.p-toggleswitch > input[type="checkbox"] {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
border-radius: unset;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
data-placeholder="/image/cache/no_image-100x100.png"
|
||||
alt="Image"
|
||||
@load="isLoaded = true"
|
||||
style="width: 100%; height: 100%;"
|
||||
>
|
||||
</a>
|
||||
<input ref="inputRef" type="hidden" value="" :id="`input-image-${id}`">
|
||||
@@ -48,11 +49,6 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.oc-image {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.loader {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
|
||||
27
frontend/admin/src/components/Settings/ItemBool.vue
Normal file
27
frontend/admin/src/components/Settings/ItemBool.vue
Normal file
@@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<SettingsItem :label="label">
|
||||
<template #default>
|
||||
<Switcher v-model="model"/>
|
||||
</template>
|
||||
<template #help>
|
||||
<slot></slot>
|
||||
</template>
|
||||
</SettingsItem>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import Switcher from "@/components/Switcher.vue";
|
||||
import SettingsItem from "@/components/SettingsItem.vue";
|
||||
|
||||
const model = defineModel();
|
||||
const props = defineProps({
|
||||
label: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
116
frontend/admin/src/components/Settings/ItemCategoriesSelect.vue
Normal file
116
frontend/admin/src/components/Settings/ItemCategoriesSelect.vue
Normal file
@@ -0,0 +1,116 @@
|
||||
<template>
|
||||
<SettingsItem :label="label">
|
||||
<template #default>
|
||||
<input
|
||||
ref="searchInput"
|
||||
type="text"
|
||||
placeholder="Начните вводить название категории..."
|
||||
class="form-control"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<div class="well well-sm tw:h-90 tw:overflow-auto">
|
||||
<div v-if="isLoading">
|
||||
<i class="fa fa-spinner fa-spin"></i>
|
||||
Загрузка списка категорий...
|
||||
</div>
|
||||
<div v-else v-for="(product, index) in selectedProducts"
|
||||
class="tw:flex tw:items-center tw:mb-1">
|
||||
<button
|
||||
@click.prevent="removeItem(index)"
|
||||
class="btn btn-xs btn-danger"
|
||||
>
|
||||
<i class="fa fa-minus-circle"></i>
|
||||
</button>
|
||||
<div class="tw:ml-3">{{ product.name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #help>
|
||||
<slot></slot>
|
||||
</template>
|
||||
</SettingsItem>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import SettingsItem from "@/components/SettingsItem.vue";
|
||||
import {nextTick, onMounted, ref, watch} from "vue";
|
||||
import {apiPost} from "@/utils/http.js";
|
||||
|
||||
const searchInput = ref(null);
|
||||
const isLoading = ref(false);
|
||||
|
||||
const props = defineProps({
|
||||
label: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
readonly: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
rows: {
|
||||
type: Number,
|
||||
default: 5,
|
||||
},
|
||||
});
|
||||
const model = defineModel();
|
||||
|
||||
function removeItem(index) {
|
||||
model.value.splice(index, 1);
|
||||
}
|
||||
|
||||
const selectedProducts = ref([]);
|
||||
watch(
|
||||
model.value,
|
||||
async (ids) => {
|
||||
if (!ids?.length) {
|
||||
selectedProducts.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
isLoading.value = true;
|
||||
const response = await apiPost('getCategoriesById', {
|
||||
category_ids: ids,
|
||||
});
|
||||
|
||||
selectedProducts.value = response.data.data;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
},
|
||||
{immediate: true}
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
if (searchInput.value) {
|
||||
$(searchInput.value).autocomplete({
|
||||
source: function (request, response) {
|
||||
$.ajax({
|
||||
url: `/admin/index.php?route=catalog/category/autocomplete&user_token=${window.TeleCart.user_token}&filter_name=${encodeURIComponent(request)}`,
|
||||
dataType: 'json',
|
||||
success: function (json) {
|
||||
response($.map(json, function (item) {
|
||||
return {
|
||||
label: item['name'],
|
||||
value: Number(item['category_id']),
|
||||
};
|
||||
}));
|
||||
}
|
||||
});
|
||||
},
|
||||
select: function (item) {
|
||||
model.value.push(item['value']);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
26
frontend/admin/src/components/Settings/ItemImage.vue
Normal file
26
frontend/admin/src/components/Settings/ItemImage.vue
Normal file
@@ -0,0 +1,26 @@
|
||||
<template>
|
||||
<SettingsItem :label="label">
|
||||
<template #default>
|
||||
<OcImagePicker v-model="model"/>
|
||||
</template>
|
||||
<template #help><slot></slot></template>
|
||||
</SettingsItem>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import OcImagePicker from "@/components/OcImagePicker.vue";
|
||||
import SettingsItem from "@/components/SettingsItem.vue";
|
||||
|
||||
const model = defineModel();
|
||||
const props = defineProps({
|
||||
label: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
45
frontend/admin/src/components/Settings/ItemInput.vue
Normal file
45
frontend/admin/src/components/Settings/ItemInput.vue
Normal file
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<SettingsItem :label="label">
|
||||
<template #default>
|
||||
<InputText
|
||||
:type="type"
|
||||
v-model="model"
|
||||
class="form-control"
|
||||
:placeholder="placeholder"
|
||||
:readonly="readonly"
|
||||
/>
|
||||
</template>
|
||||
<template #help>
|
||||
<slot></slot>
|
||||
</template>
|
||||
</SettingsItem>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import SettingsItem from "@/components/SettingsItem.vue";
|
||||
import InputText from 'primevue/inputtext';
|
||||
|
||||
const props = defineProps({
|
||||
label: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: 'Введите значение'
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'text',
|
||||
},
|
||||
readonly: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
const model = defineModel();
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
116
frontend/admin/src/components/Settings/ItemProductsSelect.vue
Normal file
116
frontend/admin/src/components/Settings/ItemProductsSelect.vue
Normal file
@@ -0,0 +1,116 @@
|
||||
<template>
|
||||
<SettingsItem :label="label">
|
||||
<template #default>
|
||||
<input
|
||||
ref="productsInput"
|
||||
type="text"
|
||||
placeholder="Начните вводить название товара..."
|
||||
class="form-control"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<div class="well well-sm tw:h-90 tw:overflow-auto">
|
||||
<div v-if="isLoading">
|
||||
<i class="fa fa-spinner fa-spin"></i>
|
||||
Загрузка списка товаров...
|
||||
</div>
|
||||
<div v-else v-for="(product, index) in selectedProducts"
|
||||
class="tw:flex tw:items-center tw:mb-1">
|
||||
<button
|
||||
@click.prevent="removeItem(index)"
|
||||
class="btn btn-xs btn-danger"
|
||||
>
|
||||
<i class="fa fa-minus-circle"></i>
|
||||
</button>
|
||||
<div class="tw:ml-3">{{ product.name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #help>
|
||||
<slot></slot>
|
||||
</template>
|
||||
</SettingsItem>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import SettingsItem from "@/components/SettingsItem.vue";
|
||||
import {nextTick, onMounted, ref, watch} from "vue";
|
||||
import {apiPost} from "@/utils/http.js";
|
||||
|
||||
const productsInput = ref(null);
|
||||
const isLoading = ref(false);
|
||||
|
||||
const props = defineProps({
|
||||
label: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
readonly: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
rows: {
|
||||
type: Number,
|
||||
default: 5,
|
||||
},
|
||||
});
|
||||
const model = defineModel();
|
||||
|
||||
function removeItem(index) {
|
||||
model.value.splice(index, 1);
|
||||
}
|
||||
|
||||
const selectedProducts = ref([]);
|
||||
watch(
|
||||
model.value,
|
||||
async (ids) => {
|
||||
if (!ids?.length) {
|
||||
selectedProducts.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
isLoading.value = true;
|
||||
const response = await apiPost('getProductsById', {
|
||||
product_ids: ids,
|
||||
});
|
||||
|
||||
selectedProducts.value = response.data.data;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
},
|
||||
{immediate: true}
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
if (productsInput.value) {
|
||||
$(productsInput.value).autocomplete({
|
||||
source: function (request, response) {
|
||||
$.ajax({
|
||||
url: `/admin/index.php?route=catalog/product/autocomplete&user_token=${window.TeleCart.user_token}&filter_name=${encodeURIComponent(request)}`,
|
||||
dataType: 'json',
|
||||
success: function (json) {
|
||||
response($.map(json, function (item) {
|
||||
return {
|
||||
label: item['name'],
|
||||
value: Number(item['product_id']),
|
||||
};
|
||||
}));
|
||||
}
|
||||
});
|
||||
},
|
||||
select: function (item) {
|
||||
model.value.push(item['value']);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
34
frontend/admin/src/components/Settings/ItemSelect.vue
Normal file
34
frontend/admin/src/components/Settings/ItemSelect.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<SettingsItem :label="label">
|
||||
<template #default>
|
||||
<select class="form-control" v-model="model">
|
||||
<option v-for="(value, key) in items" :value="key" :key="key">
|
||||
{{ value }}
|
||||
</option>
|
||||
</select>
|
||||
</template>
|
||||
<template #help>
|
||||
<slot/>
|
||||
</template>
|
||||
</SettingsItem>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import SettingsItem from "@/components/SettingsItem.vue";
|
||||
|
||||
const model = defineModel();
|
||||
const props = defineProps({
|
||||
items: {
|
||||
type: Object,
|
||||
default: {},
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
41
frontend/admin/src/components/Settings/ItemTextarea.vue
Normal file
41
frontend/admin/src/components/Settings/ItemTextarea.vue
Normal file
@@ -0,0 +1,41 @@
|
||||
<template>
|
||||
<SettingsItem :label="label">
|
||||
<template #default>
|
||||
<Textarea
|
||||
v-model="model"
|
||||
class="form-control"
|
||||
:placeholder="placeholder"
|
||||
:readonly="readonly"
|
||||
:rows="rows"
|
||||
/>
|
||||
</template>
|
||||
<template #help>
|
||||
<slot></slot>
|
||||
</template>
|
||||
</SettingsItem>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import SettingsItem from "@/components/SettingsItem.vue";
|
||||
import Textarea from 'primevue/textarea';
|
||||
|
||||
const props = defineProps({
|
||||
label: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
readonly: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
rows: {
|
||||
type: Number,
|
||||
default: 5,
|
||||
},
|
||||
});
|
||||
const model = defineModel();
|
||||
</script>
|
||||
145
frontend/admin/src/components/Settings/ItemTgBotToken.vue
Normal file
145
frontend/admin/src/components/Settings/ItemTgBotToken.vue
Normal file
@@ -0,0 +1,145 @@
|
||||
<template>
|
||||
<SettingsItem :label="label">
|
||||
<template #default>
|
||||
<div class="tw:flex tw:w-full">
|
||||
<span class="tw:flex">
|
||||
<button
|
||||
class="btn btn-primary tw:whitespace-nowrap"
|
||||
type="button"
|
||||
@click="validateBotToken"
|
||||
:disabled="isLoading || ! settings.items.telegram.bot_token"
|
||||
:class="{
|
||||
'tw:opacity-60 tw:cursor-not-allowed': isLoading
|
||||
}"
|
||||
>
|
||||
<i
|
||||
:class="isLoading ? 'fa fa-spinner fa-spin tw:mr-1' : 'fa fa-refresh tw:mr-1'"
|
||||
></i>
|
||||
{{ isLoading ? 'Проверяю...' : 'Проверить Bot Token' }}
|
||||
</button>
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
v-model="model"
|
||||
@input="handleInput"
|
||||
@blur="validateBotToken"
|
||||
placeholder="Введите токен от Telegram бота"
|
||||
class="form-control"
|
||||
:readonly="isLoading"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="validationStatus"
|
||||
class="alert"
|
||||
:class="validationStatusClass"
|
||||
>
|
||||
{{ validationStatus }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #help>
|
||||
Подробная инструкция доступна в
|
||||
<a href="https://telecart-labs.github.io/docs/telegram/telegram/#%D1%81%D0%BE%D0%B7%D0%B4%D0%B0%D0%BD%D0%B8%D0%B5-%D0%B1%D0%BE%D1%82%D0%B0" target="_blank">документации
|
||||
<i class="fa fa-external-link"></i>
|
||||
</a>.
|
||||
</template>
|
||||
</SettingsItem>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {useSettingsStore} from "@/stores/settings.js";
|
||||
import {ref, computed} from "vue";
|
||||
import SettingsItem from "@/components/SettingsItem.vue";
|
||||
import {apiPost} from "@/utils/http.js";
|
||||
|
||||
const model = defineModel();
|
||||
const settings = useSettingsStore();
|
||||
const validationStatus = ref(null);
|
||||
const isLoading = ref(false);
|
||||
|
||||
const props = defineProps({
|
||||
label: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
|
||||
const validationStatusClass = computed(() => {
|
||||
if (!validationStatus.value) return '';
|
||||
|
||||
if (validationStatus.value.startsWith('✅')) {
|
||||
return 'alert-success';
|
||||
}
|
||||
|
||||
if (validationStatus.value.startsWith('❌')) {
|
||||
return 'alert-danger';
|
||||
}
|
||||
|
||||
return 'alert-info';
|
||||
});
|
||||
|
||||
function handleInput(event) {
|
||||
model.value = event.target.value;
|
||||
// Сбрасываем статус валидации при изменении токена
|
||||
if (validationStatus.value) {
|
||||
validationStatus.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function validateBotToken() {
|
||||
const botToken = model.value?.trim() || '';
|
||||
|
||||
// Валидация пустого токена
|
||||
if (botToken.length === 0) {
|
||||
validationStatus.value = '❌ Введите Bot Token!';
|
||||
return;
|
||||
}
|
||||
|
||||
// Сбрасываем предыдущий статус
|
||||
validationStatus.value = null;
|
||||
isLoading.value = true;
|
||||
|
||||
try {
|
||||
const result = await apiPost('configureBotToken', { botToken });
|
||||
|
||||
if (!result.success) {
|
||||
// Обработка ошибок
|
||||
if (result.status === 422) {
|
||||
validationStatus.value = `❌ Ошибка: ${result.error || 'Неверный токен'}`;
|
||||
} else {
|
||||
validationStatus.value = `❌ Ошибка проверки BotToken: ${result.error || 'Неизвестная ошибка'}`;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const response = result.data;
|
||||
|
||||
// Проверка наличия обязательных полей в ответе
|
||||
if (!response?.id) {
|
||||
validationStatus.value = '❌ Ошибка: bot token не найден в ответе сервера.';
|
||||
console.error('Неожиданный формат ответа:', response);
|
||||
return;
|
||||
}
|
||||
|
||||
// Успешная валидация
|
||||
const username = response.username ? `@${response.username}` : 'не указан';
|
||||
const webhookUrl = response.webhook_url || 'не настроен';
|
||||
validationStatus.value = `✅ Бот: ${username} (id: ${response.id}) webhook: ${webhookUrl}`;
|
||||
|
||||
// Обновляем токен в store, если нужно (на случай если сервер что-то изменил)
|
||||
if (response.bot_token && response.bot_token !== botToken) {
|
||||
model.value = response.bot_token;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка при валидации BotToken:', error);
|
||||
validationStatus.value = '❌ Ошибка проверки BotToken. Проверьте подключение к серверу.';
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
173
frontend/admin/src/components/Settings/ItemTgChatID.vue
Normal file
173
frontend/admin/src/components/Settings/ItemTgChatID.vue
Normal file
@@ -0,0 +1,173 @@
|
||||
<template>
|
||||
<SettingsItem :label="label">
|
||||
<template #default>
|
||||
<template v-if="settings.items.telegram.bot_token">
|
||||
<div class="tw:flex tw:w-full">
|
||||
<span class="tw:flex">
|
||||
<button
|
||||
class="btn btn-primary tw:whitespace-nowrap"
|
||||
type="button"
|
||||
@click="getChatId"
|
||||
:disabled="isLoading || !settings.items.telegram.bot_token"
|
||||
:class="{
|
||||
'tw:opacity-60 tw:cursor-not-allowed': isLoading
|
||||
}"
|
||||
>
|
||||
<i
|
||||
:class="isLoading ? 'fa fa-spinner fa-spin tw:mr-1' : 'fa fa-refresh tw:mr-1'"
|
||||
></i>
|
||||
{{ isLoading ? 'Получаю...' : 'Получить Chat ID' }}
|
||||
</button>
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
v-model="model"
|
||||
@input="handleInput"
|
||||
:placeholder="placeholder"
|
||||
class="form-control"
|
||||
:readonly="isLoading"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="statusMessage"
|
||||
class="alert"
|
||||
:class="statusMessageClass"
|
||||
>
|
||||
{{ statusMessage }}
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="btn btn-link btn-xs"
|
||||
type="button"
|
||||
data-toggle="collapse"
|
||||
:data-target="`#${collapseId}`"
|
||||
aria-expanded="false"
|
||||
:aria-controls="collapseId"
|
||||
>
|
||||
Инструкция как получить ChatID.
|
||||
</button>
|
||||
<div class="collapse" :id="collapseId">
|
||||
<div class="well">
|
||||
<p class="text-primary">Как получить Chat ID</p>
|
||||
<ol>
|
||||
<li>Убедитесь, что Telegram Bot Token введён выше.</li>
|
||||
<li>Откройте вашего бота в Telegram и отправьте ему кодовое слово: <code>opencart_get_chatid</code>. Важно отправить именно такое сообщение, иначе не сработает.</li>
|
||||
<li>Вернитесь сюда и нажмите кнопку «Получить Chat ID» — скрипт автоматически подставит его в поле ниже.</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-else class="alert alert-warning">
|
||||
<strong>BotToken</strong> не указан. Пожалуйста, введите корректный BotToken. После этого здесь станет доступна настройка ChatID.
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #help>
|
||||
Идентификатор Telegram-чата, куда будут отправляться уведомления о новых заказах. Если оставить поле пустым, уведомления отправляться не будут.
|
||||
</template>
|
||||
</SettingsItem>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {useSettingsStore} from "@/stores/settings.js";
|
||||
import {ref, computed, useId} from "vue";
|
||||
import SettingsItem from "@/components/SettingsItem.vue";
|
||||
import {apiGet} from "@/utils/http.js";
|
||||
|
||||
const model = defineModel();
|
||||
const settings = useSettingsStore();
|
||||
const statusMessage = ref(null);
|
||||
const isLoading = ref(false);
|
||||
const collapseId = useId();
|
||||
|
||||
const props = defineProps({
|
||||
label: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: 'Chat ID будет получен автоматически',
|
||||
},
|
||||
});
|
||||
|
||||
const statusMessageClass = computed(() => {
|
||||
if (!statusMessage.value) return '';
|
||||
|
||||
if (statusMessage.value.startsWith('✅')) {
|
||||
return 'alert-success';
|
||||
}
|
||||
|
||||
if (statusMessage.value.startsWith('❌')) {
|
||||
return 'alert-danger';
|
||||
}
|
||||
|
||||
return 'alert-info';
|
||||
});
|
||||
|
||||
function handleInput(event) {
|
||||
model.value = event.target.value;
|
||||
// Сбрасываем статус сообщения при изменении значения
|
||||
if (statusMessage.value) {
|
||||
statusMessage.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function getChatId() {
|
||||
// Проверка наличия bot_token
|
||||
if (!settings.items.telegram.bot_token?.trim()) {
|
||||
alert('Сначала введите Telegram Bot Token!');
|
||||
return;
|
||||
}
|
||||
|
||||
// Сбрасываем предыдущее сообщение
|
||||
statusMessage.value = null;
|
||||
isLoading.value = true;
|
||||
|
||||
try {
|
||||
const response = await apiGet('getChatId');
|
||||
|
||||
if (!response.success) {
|
||||
// Обработка ошибок
|
||||
const errorMessage = response.data?.message || response.error || 'Неизвестная ошибка';
|
||||
|
||||
if (response.status === 422) {
|
||||
statusMessage.value = `❌ ${errorMessage}`;
|
||||
} else {
|
||||
statusMessage.value = `❌ Ошибка получения Chat ID: ${errorMessage}`;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Проверка наличия chat_id в ответе
|
||||
if (!response.data?.chat_id) {
|
||||
statusMessage.value = '❌ Ошибка: Chat ID не найден в ответе сервера.';
|
||||
console.error('Неожиданный формат ответа:', response);
|
||||
return;
|
||||
}
|
||||
|
||||
// Успешное получение Chat ID
|
||||
const chatId = response.data.chat_id;
|
||||
model.value = chatId;
|
||||
statusMessage.value = '✅ ChatID успешно получен и подставлен в поле. Не забудьте сохранить настройки!';
|
||||
} catch (error) {
|
||||
console.error('Ошибка при получении Chat ID:', error);
|
||||
statusMessage.value = '❌ Ошибка получения Chat ID. Проверьте подключение к серверу.';
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
code {
|
||||
background-color: #f5f5f5;
|
||||
padding: 2px 4px;
|
||||
border-radius: 3px;
|
||||
font-family: monospace;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
</style>
|
||||
|
||||
169
frontend/admin/src/components/Settings/ItemTgMessageTemplate.vue
Normal file
169
frontend/admin/src/components/Settings/ItemTgMessageTemplate.vue
Normal file
@@ -0,0 +1,169 @@
|
||||
<template>
|
||||
<SettingsItem :label="label">
|
||||
<template #default>
|
||||
<div style="margin-bottom: 10px;">
|
||||
<textarea
|
||||
v-model="model"
|
||||
:rows="rows"
|
||||
:placeholder="placeholder"
|
||||
class="form-control"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
class="btn btn-link"
|
||||
type="button"
|
||||
data-toggle="collapse"
|
||||
:data-target="`#${collapseId}`"
|
||||
aria-expanded="false"
|
||||
:aria-controls="collapseId"
|
||||
>
|
||||
Документация
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary btn-sm"
|
||||
@click="sendTestMessage"
|
||||
:disabled="isSending"
|
||||
:class="{
|
||||
'tw:opacity-60 tw:cursor-not-allowed': isSending
|
||||
}"
|
||||
>
|
||||
<i :class="isSending ? 'fa fa-spinner fa-spin' : 'fa fa-envelope'"></i>
|
||||
{{ isSending ? 'Отправляю...' : 'Отправить тестовое уведомление' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="collapse" :id="collapseId" style="margin-top: 15px">
|
||||
<div class="well">
|
||||
<p>Вы можете использовать переменные:</p>
|
||||
<ul>
|
||||
<li><code>{store_name}</code> — название магазина</li>
|
||||
<li><code>{order_id}</code> — номер заказа</li>
|
||||
<li><code>{customer}</code> — имя и фамилия покупателя</li>
|
||||
<li><code>{email}</code> — email покупателя</li>
|
||||
<li><code>{phone}</code> — телефон</li>
|
||||
<li><code>{comment}</code> — комментарий к заказу</li>
|
||||
<li><code>{address}</code> — адрес доставки</li>
|
||||
<li><code>{total}</code> — сумма заказа</li>
|
||||
<li><code>{ip}</code> — IP покупателя</li>
|
||||
<li><code>{created_at}</code> — дата и время создания заказа</li>
|
||||
</ul>
|
||||
<p>
|
||||
Форматирование: поддерживается
|
||||
<a href="https://core.telegram.org/bots/api#markdownv2-style" target="_blank">
|
||||
*MarkdownV2*
|
||||
<i class="fa fa-external-link"></i>
|
||||
</a>.
|
||||
</p>
|
||||
<p>Символы, которые нужно экранировать в тексте:</p>
|
||||
<pre>_ * [ ] ( ) ~ ` > # + - = | { } . !</pre>
|
||||
<p>
|
||||
Каждый из них нужно экранировать обратным слэшем \, если он не используется для форматирования.
|
||||
Например вместо <code>Заказ #123</code> нужно писать <code>Заказ \#123</code>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #help>
|
||||
<slot></slot>
|
||||
</template>
|
||||
</SettingsItem>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {useSettingsStore} from "@/stores/settings.js";
|
||||
import {ref, toRaw, useId} from "vue";
|
||||
import SettingsItem from "@/components/SettingsItem.vue";
|
||||
import {apiPost} from "@/utils/http.js";
|
||||
|
||||
const model = defineModel();
|
||||
const settings = useSettingsStore();
|
||||
const isSending = ref(false);
|
||||
const collapseId = useId();
|
||||
|
||||
const props = defineProps({
|
||||
label: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: 'Введите шаблон сообщения',
|
||||
},
|
||||
rows: {
|
||||
type: Number,
|
||||
default: 5,
|
||||
},
|
||||
});
|
||||
|
||||
async function sendTestMessage() {
|
||||
console.log(toRaw(settings.items.telegram));
|
||||
const telegramToken = settings.items.telegram.bot_token?.trim();
|
||||
|
||||
if (!telegramToken) {
|
||||
alert('Сначала введите Telegram Bot Token!');
|
||||
return;
|
||||
}
|
||||
|
||||
const chatId = settings.items.telegram.chat_id;
|
||||
|
||||
if (!chatId) {
|
||||
alert('Сначала введите Chat ID!');
|
||||
return;
|
||||
}
|
||||
|
||||
const template = model.value?.trim();
|
||||
|
||||
if (!template) {
|
||||
alert('Сначала задайте шаблон!');
|
||||
return;
|
||||
}
|
||||
|
||||
isSending.value = true;
|
||||
|
||||
try {
|
||||
const result = await apiPost('testTgMessage', {
|
||||
token: telegramToken,
|
||||
chat_id: chatId,
|
||||
template: template,
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
const errorMessage = result.data?.message || result.error || 'Неизвестная ошибка';
|
||||
alert(`Ошибка: ${errorMessage}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const response = result.data;
|
||||
alert(response.message || 'Уведомление успешно отправлено');
|
||||
} catch (error) {
|
||||
console.error('Ошибка при отправке тестового сообщения:', error);
|
||||
alert('Ошибка при отправке тестового сообщения');
|
||||
} finally {
|
||||
isSending.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
code {
|
||||
background-color: #f5f5f5;
|
||||
padding: 2px 4px;
|
||||
border-radius: 3px;
|
||||
font-family: monospace;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
pre {
|
||||
background-color: #f5f5f5;
|
||||
padding: 10px;
|
||||
border-radius: 3px;
|
||||
font-family: monospace;
|
||||
font-size: 0.9em;
|
||||
overflow-x: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
31
frontend/admin/src/components/Settings/ItemTgMiniAppLink.vue
Normal file
31
frontend/admin/src/components/Settings/ItemTgMiniAppLink.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<ItemInput
|
||||
:label="label"
|
||||
type="text"
|
||||
:readonly="true"
|
||||
:modelValue="model"
|
||||
>
|
||||
Токен, полученный при создании бота через @BotFather.
|
||||
Он используется для взаимодействия модуля с Telegram API.
|
||||
Подробная инструкция доступна в
|
||||
<a href="https://nikitakiselev.github.io/telecart-docs/#telegram" target="_blank">
|
||||
документации <i class="fa fa-external-link"></i>
|
||||
</a>.
|
||||
</ItemInput>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import ItemInput from "@/components/Settings/ItemInput.vue";
|
||||
|
||||
const props = defineProps({
|
||||
label: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
const model = defineModel();
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
||||
@@ -157,7 +157,7 @@ import LinkSelector from "@/components/Slider/LinkSelector.vue";
|
||||
import SettingsItem from "@/components/SettingsItem.vue";
|
||||
import Switcher from "@/components/Switcher.vue";
|
||||
|
||||
const slider = ref({});
|
||||
const slider = defineModel();
|
||||
|
||||
function removeSlide(index) {
|
||||
slider.value.slides.splice(index, 1);
|
||||
@@ -173,10 +173,6 @@ function addSlide() {
|
||||
image: '',
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
slider.value = JSON.parse(window.TeleCart.mainpage_slider);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -1,23 +1,10 @@
|
||||
<template>
|
||||
<div class="btn-group btn-toggle tw:mt-3">
|
||||
<button
|
||||
class="btn btn-xs"
|
||||
:class="{active: model === true, 'btn-success': model === true, 'btn-default' : model === false }"
|
||||
@click.prevent="model = true"
|
||||
>
|
||||
Вкл
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-xs"
|
||||
:class="{active: model === false, 'btn-danger': model === false, 'btn-default' : model === true }"
|
||||
@click.prevent="model = false"
|
||||
>
|
||||
Выкл
|
||||
</button>
|
||||
</div>
|
||||
<ToggleSwitch v-model="model" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import ToggleSwitch from 'primevue/toggleswitch';
|
||||
|
||||
const model = defineModel({
|
||||
default: false,
|
||||
});
|
||||
|
||||
112
frontend/admin/src/components/TopLead.vue
Normal file
112
frontend/admin/src/components/TopLead.vue
Normal file
@@ -0,0 +1,112 @@
|
||||
<template>
|
||||
<div class="tw:bg-surface-0 tw:dark:bg-surface-950 tw:px-6 tw:py-8 tw:md:px-12 tw:lg:px-20">
|
||||
<div class="tw:flex tw:items-center tw:flex-col tw:lg:flex-row tw:lg:justify-between">
|
||||
<div class="tw:flex tw:items-start tw:flex-col tw:lg:flex-row tw:gap-8">
|
||||
<OcImagePicker v-model="settings.items.app.app_icon" class="tw:w-[6.42rem] tw:h-[6.42rem]"/>
|
||||
<div class="tw:flex tw:flex-col tw:gap-4">
|
||||
<div class="tw:flex tw:items-center">
|
||||
<span class="tw:text-surface-900 tw:dark:text-surface-0 tw:font-bold tw:text-3xl">
|
||||
{{ settings.items.app.app_name }}
|
||||
</span>
|
||||
<a
|
||||
v-if="tgMe?.result?.first_name"
|
||||
:href="`https://t.me/${tgMe?.result?.username}`"
|
||||
class="tw:ml-2 tw:text-surface-900 tw:dark:text-surface-0 tw:text-xl">
|
||||
@{{ tgMe?.result?.first_name }}
|
||||
</a>
|
||||
</div>
|
||||
<div class="tw:flex tw:items-center tw:flex-wrap tw:gap-8">
|
||||
<div>
|
||||
<span class="tw:text-surface-500 tw:dark:text-surface-300">Количество заказов</span>
|
||||
<div
|
||||
class="tw:text-surface-700 tw:dark:text-surface-100 tw:mt-1 tw:text-sm tw:font-semibold">
|
||||
{{ stats.items.orders_count ?? '-' }}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span class="tw:text-surface-500 tw:dark:text-surface-300">Общая сумма</span>
|
||||
<div
|
||||
class="tw:text-surface-700 tw:dark:text-surface-100 tw:mt-1 tw:text-sm tw:font-semibold">
|
||||
{{ stats.items.orders_total_amount ?? '-' }}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span class="tw:text-surface-500 tw:dark:text-surface-300">Уникальные товары</span>
|
||||
<div
|
||||
class="tw:text-surface-700 tw:dark:text-surface-100 tw:mt-1 tw:text-sm tw:font-semibold">
|
||||
{{ stats.items.order_products_count ?? '-' }}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span class="tw:text-surface-500 tw:dark:text-surface-300">Статус магазина</span>
|
||||
<div
|
||||
class="tw:text-surface-700 tw:dark:text-surface-100 tw:mt-1 tw:text-sm tw:font-semibold">
|
||||
<div v-if="settings.items.app.app_enabled" class="tw:flex tw:items-center">
|
||||
<div class="tw:h-2 tw:w-2 tw:rounded-full tw:bg-green-400 tw:flex tw:mr-2">
|
||||
<span
|
||||
class="tw:inline-flex tw:h-full tw:w-full tw:animate-ping tw:rounded-full tw:bg-green-400 tw:opacity-75"></span>
|
||||
</div>
|
||||
<div>Online</div>
|
||||
</div>
|
||||
|
||||
<div v-else
|
||||
class="tw:text-surface-700 tw:dark:text-surface-100 tw:mt-1 tw:text-sm tw:font-semibold">
|
||||
<div class="tw:flex tw:items-center">
|
||||
<div class="tw:h-2 tw:w-2 tw:rounded-full tw:bg-red-400 tw:flex tw:mr-2">
|
||||
<span
|
||||
class="tw:inline-flex tw:h-full tw:w-full tw:animate-ping tw:rounded-full tw:bg-red-400 tw:opacity-75"></span>
|
||||
</div>
|
||||
<div>Offline</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tw:mt-6 tw:lg:mt-0 tw:flex tw:items-center tw:gap-4">
|
||||
<div class="btn-group">
|
||||
<a
|
||||
class="btn btn-primary"
|
||||
:class="{'disabled': (tgMe?.result?.has_main_web_app !== true)}"
|
||||
rounded
|
||||
:href="`https://t.me/${tgMe?.result?.username}?startapp`"
|
||||
target="_blank"
|
||||
:title="(tgMe?.result?.has_main_web_app !== true) ? 'Вы не привязали Telegram Mini App к боту.' : 'Открыть Telegram магазин'"
|
||||
>
|
||||
<i class="fa fa-play"></i>
|
||||
</a>
|
||||
<a class="btn btn-default" target="_blank" href="https://telecart-labs.github.io/docs/" title="Документация по модулю TeleCart">
|
||||
<i class="fa fa-book"></i>
|
||||
</a>
|
||||
<a class="btn btn-default" target="_blank" href="https://t.me/ocstore3" title="Официальная Telegram группа модуля TeleCart">
|
||||
<i class="fa fa-group"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import Button from "primevue/button";
|
||||
import {useSettingsStore} from "@/stores/settings.js";
|
||||
import {useStatsStore} from "@/stores/stats.js";
|
||||
import {onMounted, ref} from "vue";
|
||||
import OcImagePicker from "@/components/OcImagePicker.vue";
|
||||
import {apiGet} from "@/utils/http.js";
|
||||
|
||||
const settings = useSettingsStore();
|
||||
const stats = useStatsStore();
|
||||
const tgMe = ref(null);
|
||||
|
||||
onMounted(async () => {
|
||||
await stats.fetchStats();
|
||||
const response = await apiGet('tgGetMe');
|
||||
tgMe.value = response.data;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
||||
@@ -3,6 +3,15 @@ import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import {useSettingsStore} from "@/stores/settings.js";
|
||||
import PrimeVue from 'primevue/config';
|
||||
import Aura from '@primeuix/themes/aura';
|
||||
import ToastService from 'primevue/toastservice';
|
||||
import {definePreset} from "@primeuix/themes";
|
||||
|
||||
const MyPreset = definePreset(Aura, {
|
||||
|
||||
});
|
||||
|
||||
function onReady(fn) {
|
||||
if (document.readyState === 'loading') {
|
||||
@@ -12,9 +21,20 @@ function onReady(fn) {
|
||||
}
|
||||
}
|
||||
|
||||
onReady(() => {
|
||||
onReady(async () => {
|
||||
const app = createApp(App);
|
||||
app.use(createPinia());
|
||||
app.use(router);
|
||||
app.use(PrimeVue, {
|
||||
theme: {
|
||||
preset: MyPreset,
|
||||
options: {
|
||||
cssLayer: false, // если используешь Tailwind, отключает layering
|
||||
},
|
||||
}
|
||||
});
|
||||
app.use(ToastService);
|
||||
|
||||
app.mount('#app');
|
||||
await useSettingsStore().fetchSettings();
|
||||
});
|
||||
|
||||
@@ -1,15 +1,23 @@
|
||||
import {createMemoryHistory, createRouter} from 'vue-router'
|
||||
import HomeView from '../views/HomeView.vue'
|
||||
import {createMemoryHistory, createRouter} from 'vue-router';
|
||||
import SliderView from "@/views/SliderView.vue";
|
||||
import GeneralView from "@/views/GeneralView.vue";
|
||||
import TextsView from "@/views/TextsView.vue";
|
||||
import OrdersView from "@/views/OrdersView.vue";
|
||||
import TelegramView from "@/views/TelegramView.vue";
|
||||
import MetricsView from "@/views/MetricsView.vue";
|
||||
import StoreView from "@/views/StoreView.vue";
|
||||
|
||||
const router = createRouter({
|
||||
history: createMemoryHistory(),
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
name: 'home',
|
||||
component: HomeView,
|
||||
},
|
||||
{path: '/', name: 'general', component: GeneralView},
|
||||
{path: '/slider', name: 'slider', component: SliderView},
|
||||
{path: '/orders', name: 'orders', component: OrdersView},
|
||||
{path: '/texts', name: 'texts', component: TextsView},
|
||||
{path: '/telegram', name: 'telegram', component: TelegramView},
|
||||
{path: '/metrics', name: 'metrics', component: MetricsView},
|
||||
{path: '/store', name: 'store', component: StoreView},
|
||||
],
|
||||
})
|
||||
});
|
||||
|
||||
export default router
|
||||
export default router;
|
||||
|
||||
125
frontend/admin/src/stores/settings.js
Normal file
125
frontend/admin/src/stores/settings.js
Normal file
@@ -0,0 +1,125 @@
|
||||
import {defineStore} from "pinia";
|
||||
import {apiGet, apiPost} from "@/utils/http.js";
|
||||
import {toastBus} from "@/utils/toastHelper.js";
|
||||
|
||||
export const useSettingsStore = defineStore('settings', {
|
||||
state: () => ({
|
||||
isLoading: false,
|
||||
error: null,
|
||||
|
||||
items: {
|
||||
app: {
|
||||
app_enabled: true,
|
||||
app_name: '',
|
||||
app_icon: null,
|
||||
theme_light: 'light',
|
||||
theme_dark: 'dark',
|
||||
app_debug: false,
|
||||
},
|
||||
|
||||
telegram: {
|
||||
mini_app_url: '',
|
||||
bot_token: '',
|
||||
chat_id: '',
|
||||
owner_notification_template: '',
|
||||
customer_notification_template: '',
|
||||
},
|
||||
|
||||
metrics: {
|
||||
yandex_metrika_enabled: false,
|
||||
yandex_metrika_counter: '',
|
||||
},
|
||||
|
||||
store: {
|
||||
enable_store: true,
|
||||
mainpage_products: 'most_viewed',
|
||||
featured_products: [],
|
||||
mainpage_categories: 'latest10',
|
||||
featured_categories: [],
|
||||
feature_coupons: true,
|
||||
feature_vouchers: true,
|
||||
},
|
||||
|
||||
orders: {
|
||||
order_default_status_id: 1,
|
||||
},
|
||||
|
||||
texts: {
|
||||
text_no_more_products: '',
|
||||
text_empty_cart: '',
|
||||
text_order_created_success: '',
|
||||
},
|
||||
|
||||
sliders: {
|
||||
mainpage_slider: {
|
||||
is_enabled: false,
|
||||
effect: "slide",
|
||||
pagination: true,
|
||||
scrollbar: false,
|
||||
free_mode: false,
|
||||
space_between: 30,
|
||||
autoplay: false,
|
||||
loop: false,
|
||||
slides: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
|
||||
getters: {
|
||||
app_icon_preview: (state) => {
|
||||
if (!state.items.app.app_icon) return '/image/cache/no_image-100x100.png';
|
||||
const extIndex = state.items.app.app_icon.lastIndexOf('.');
|
||||
const ext = state.items.app.app_icon.substring(extIndex);
|
||||
const filename = state.items.app.app_icon.substring(0, extIndex);
|
||||
return `/image/cache/${filename}-100x100${ext}`;
|
||||
},
|
||||
},
|
||||
|
||||
actions: {
|
||||
async fetchSettings() {
|
||||
this.isLoading = true;
|
||||
this.error = null;
|
||||
const response = await apiGet('getSettingsForm');
|
||||
if (response.success) {
|
||||
this.items = {
|
||||
...this.items,
|
||||
...response.data,
|
||||
};
|
||||
} else {
|
||||
this.error = 'Возникли проблемы при загрузке настроек.';
|
||||
}
|
||||
this.isLoading = false;
|
||||
},
|
||||
|
||||
async saveSettings() {
|
||||
this.isLoading = true;
|
||||
const settings = this.transformSettingsToStore(this.items);
|
||||
const response = await apiPost('saveSettingsForm', settings);
|
||||
|
||||
if (response.success === true) {
|
||||
toastBus.emit('show', {
|
||||
severity: 'success',
|
||||
summary: 'Готово!',
|
||||
detail: 'Настройки сохранены.',
|
||||
life: 2000,
|
||||
});
|
||||
} else {
|
||||
toastBus.emit('show', {
|
||||
severity: 'error',
|
||||
summary: 'Ошибка',
|
||||
detail: 'Возникли проблемы при сохранении настроек на сервере.',
|
||||
life: 2000,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
this.isLoading = false;
|
||||
},
|
||||
|
||||
transformSettingsToStore(items) {
|
||||
return items;
|
||||
},
|
||||
},
|
||||
});
|
||||
22
frontend/admin/src/stores/stats.js
Normal file
22
frontend/admin/src/stores/stats.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import {defineStore} from "pinia";
|
||||
import {apiGet, apiPost} from "@/utils/http.js";
|
||||
|
||||
export const useStatsStore = defineStore('stats', {
|
||||
state: () => ({
|
||||
items: {
|
||||
orders_count: null,
|
||||
orders_total_amount: null,
|
||||
order_products_count: null,
|
||||
}
|
||||
}),
|
||||
|
||||
actions: {
|
||||
async fetchStats() {
|
||||
const response = await apiPost('getDashboardStats');
|
||||
this.items.orders_count = response.data?.data?.orders_count;
|
||||
this.items.orders_total_amount = response.data?.data?.orders_total_amount;
|
||||
this.items.order_products_count = response.data?.data?.order_products_count;
|
||||
}
|
||||
},
|
||||
|
||||
});
|
||||
142
frontend/admin/src/utils/http.js
Normal file
142
frontend/admin/src/utils/http.js
Normal file
@@ -0,0 +1,142 @@
|
||||
import axios from 'axios';
|
||||
|
||||
/**
|
||||
* Получает user_token из глобального объекта TeleCart
|
||||
*/
|
||||
function getUserToken() {
|
||||
if (typeof window !== 'undefined' && window.TeleCart?.user_token) {
|
||||
return window.TeleCart.user_token;
|
||||
}
|
||||
|
||||
// Fallback: пытаемся получить из URL как запасной вариант
|
||||
if (typeof window !== 'undefined') {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
return urlParams.get('user_token') || '';
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Базовый URL для API запросов
|
||||
*/
|
||||
function getBaseUrl() {
|
||||
return '/admin/index.php';
|
||||
}
|
||||
|
||||
/**
|
||||
* Создает URL для API запроса
|
||||
* @param {string} apiAction - действие API (например, 'configureBotToken')
|
||||
* @returns {string} полный URL
|
||||
*/
|
||||
function buildApiUrl(apiAction) {
|
||||
const baseUrl = getBaseUrl();
|
||||
const userToken = getUserToken();
|
||||
return `${baseUrl}?route=extension/module/tgshop/handle&api_action=${apiAction}&user_token=${userToken}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP клиент для работы с API
|
||||
*/
|
||||
const httpClient = axios.create({
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Выполняет POST запрос к API
|
||||
* @param {string} apiAction - действие API
|
||||
* @param {object} data - данные для отправки
|
||||
* @returns {Promise} результат запроса
|
||||
*/
|
||||
export async function apiPost(apiAction, data = {}) {
|
||||
const url = buildApiUrl(apiAction);
|
||||
|
||||
try {
|
||||
const response = await httpClient.post(url, data);
|
||||
return {
|
||||
success: true,
|
||||
data: response.data,
|
||||
status: response.status,
|
||||
};
|
||||
} catch (error) {
|
||||
// Обработка ошибок axios
|
||||
if (error.response) {
|
||||
// Сервер вернул ошибку
|
||||
const status = error.response.status;
|
||||
const errorData = error.response.data;
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: errorData?.error || error.response.statusText,
|
||||
status,
|
||||
data: errorData,
|
||||
};
|
||||
} else if (error.request) {
|
||||
// Запрос был отправлен, но ответа не получено
|
||||
return {
|
||||
success: false,
|
||||
error: 'Не удалось получить ответ от сервера',
|
||||
status: 0,
|
||||
};
|
||||
} else {
|
||||
// Ошибка при настройке запроса
|
||||
return {
|
||||
success: false,
|
||||
error: error.message || 'Произошла неизвестная ошибка',
|
||||
status: 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Выполняет GET запрос к API
|
||||
* @param {string} apiAction - действие API
|
||||
* @param {object} params - query параметры
|
||||
* @returns {Promise} результат запроса
|
||||
*/
|
||||
export async function apiGet(apiAction, params = {}) {
|
||||
const url = buildApiUrl(apiAction);
|
||||
|
||||
try {
|
||||
const response = await httpClient.get(url, { params: params });
|
||||
return {
|
||||
success: true,
|
||||
data: response.data.data,
|
||||
status: response.status,
|
||||
};
|
||||
} catch (error) {
|
||||
if (error.response) {
|
||||
const status = error.response.status;
|
||||
const errorData = error.response.data;
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: errorData?.error || error.response.statusText,
|
||||
status,
|
||||
data: errorData,
|
||||
};
|
||||
} else if (error.request) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Не удалось получить ответ от сервера',
|
||||
status: 0,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message || 'Произошла неизвестная ошибка',
|
||||
status: 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
apiPost,
|
||||
apiGet,
|
||||
getUserToken,
|
||||
};
|
||||
|
||||
2
frontend/admin/src/utils/toastHelper.js
Normal file
2
frontend/admin/src/utils/toastHelper.js
Normal file
@@ -0,0 +1,2 @@
|
||||
import mitt from 'mitt';
|
||||
export const toastBus = mitt();
|
||||
56
frontend/admin/src/views/GeneralView.vue
Normal file
56
frontend/admin/src/views/GeneralView.vue
Normal file
@@ -0,0 +1,56 @@
|
||||
<template>
|
||||
<ItemBool label="Статус" v-model="settings.items.app.app_enabled">
|
||||
Если выключено, покупатели в Telegram увидят сообщение, что магазин временно закрыт.
|
||||
Заказы и просмотр товаров будут недоступны.
|
||||
</ItemBool>
|
||||
|
||||
<ItemInput label="Название приложения"
|
||||
v-model="settings.items.app.app_name"
|
||||
placeholder="Без названия"
|
||||
>
|
||||
Отображается в заголовке Telegram Mini App при запуске, а также используется как подпись
|
||||
под иконкой, если пользователь добавит приложение на главный экран своего устройства.
|
||||
Рекомендуется короткое и понятное название (до 20 символов).
|
||||
Если оставить пустым, то название выводиться не будет.
|
||||
</ItemInput>
|
||||
|
||||
<ItemImage label="Иконка приложения" v-model="settings.items.app.app_icon">
|
||||
Изображение, которое будет отображаться в Telegram Mini App.
|
||||
</ItemImage>
|
||||
|
||||
<ItemSelect label="Светлая тема" v-model="settings.items.app.theme_light" :items="themes">
|
||||
Выберите стиль, который будет использоваться при отображении вашего магазина
|
||||
в Telegram для дневного режима.
|
||||
<a href="https://daisyui.com/docs/themes/#list-of-themes" target="_blank">
|
||||
Посмотреть как выглядят темы
|
||||
</a>
|
||||
</ItemSelect>
|
||||
|
||||
<ItemSelect label="Тёмная тема" v-model="settings.items.app.theme_dark" :items="themes">
|
||||
Выберите стиль, который будет использоваться при отображении вашего магазина
|
||||
в Telegram для ночного режима.
|
||||
<a href="https://daisyui.com/docs/themes/#list-of-themes" target="_blank">
|
||||
Посмотреть как выглядят темы
|
||||
</a>
|
||||
</ItemSelect>
|
||||
|
||||
<ItemBool label="Режим разработчика" v-model="settings.items.app.app_debug">
|
||||
Режим разработчика. Рекомендуется включать только по необходимости.
|
||||
В остальных случаях, для нормальной работы магазина, должен быть выключен.
|
||||
</ItemBool>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {useSettingsStore} from "@/stores/settings.js";
|
||||
import ItemBool from "@/components/Settings/ItemBool.vue";
|
||||
import ItemImage from "@/components/Settings/ItemImage.vue";
|
||||
import ItemSelect from "@/components/Settings/ItemSelect.vue";
|
||||
import ItemInput from "@/components/Settings/ItemInput.vue";
|
||||
|
||||
const settings = useSettingsStore();
|
||||
const themes = JSON.parse(window.TeleCart.themes);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -1,7 +0,0 @@
|
||||
<template>
|
||||
<Slider/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import Slider from "@/components/Slider/Slider.vue";
|
||||
</script>
|
||||
29
frontend/admin/src/views/MetricsView.vue
Normal file
29
frontend/admin/src/views/MetricsView.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<ItemBool
|
||||
label="Яндекс.Метрика"
|
||||
v-model="settings.items.metrics.yandex_metrika_enabled"
|
||||
>
|
||||
Задействовать Яндекс.Метрику для Telegram магазина.
|
||||
</ItemBool>
|
||||
|
||||
<ItemTextarea
|
||||
label="Код счётчика Яндекс Метрики"
|
||||
v-model="settings.items.metrics.yandex_metrika_counter"
|
||||
placeholder="Вставьте код счётчика Яндекс.Метрики"
|
||||
>
|
||||
<p>Код счётчика нужно предварительно настроить, чтобы он работал корректно с Telegram Mini App.
|
||||
<a href="https://telecart-labs.github.io/docs/analitycs/start/" target="_blank">
|
||||
Инструкция как настроить i.fa.fa-external-link
|
||||
</a>.</p>
|
||||
<p>Для проверки интеграции через кнопку "Проверить" в интерфейсе Яндекс Метрики,
|
||||
необходимо сначала включить "Режим разработчика" на вкладке "Общие".</p>
|
||||
</ItemTextarea>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {useSettingsStore} from "@/stores/settings.js";
|
||||
import ItemTextarea from "@/components/Settings/ItemTextarea.vue";
|
||||
import ItemBool from "@/components/Settings/ItemBool.vue";
|
||||
|
||||
const settings = useSettingsStore();
|
||||
</script>
|
||||
17
frontend/admin/src/views/OrdersView.vue
Normal file
17
frontend/admin/src/views/OrdersView.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<template>
|
||||
<ItemSelect
|
||||
label="Статус заказов"
|
||||
v-model="settings.items.orders.order_default_status_id"
|
||||
:items="orderStatuses"
|
||||
>
|
||||
Статус, с которым будут создаваться заказы через Telegram по умолчанию.
|
||||
</ItemSelect>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {useSettingsStore} from "@/stores/settings.js";
|
||||
import ItemSelect from "@/components/Settings/ItemSelect.vue";
|
||||
|
||||
const settings = useSettingsStore();
|
||||
const orderStatuses = JSON.parse(window.TeleCart.order_statuses);
|
||||
</script>
|
||||
10
frontend/admin/src/views/SliderView.vue
Normal file
10
frontend/admin/src/views/SliderView.vue
Normal file
@@ -0,0 +1,10 @@
|
||||
<template>
|
||||
<Slider v-model="settings.items.sliders.mainpage_slider"/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import Slider from "@/components/Slider/Slider.vue";
|
||||
import {useSettingsStore} from "@/stores/settings.js";
|
||||
|
||||
const settings = useSettingsStore();
|
||||
</script>
|
||||
80
frontend/admin/src/views/StoreView.vue
Normal file
80
frontend/admin/src/views/StoreView.vue
Normal file
@@ -0,0 +1,80 @@
|
||||
<template>
|
||||
<ItemBool label="Разрешить покупки" v-model="settings.items.store.enable_store">
|
||||
<p>Если опция <strong>включена</strong> — пользователи смогут оформлять
|
||||
заказы прямо в Telegram-магазине. <br>
|
||||
Если <strong>выключена</strong> — оформление заказов будет недоступно. Вместо кнопки «Добавить
|
||||
в корзину» пользователи увидят кнопку «Перейти к товару», которая откроет страницу товара на
|
||||
вашем сайте. В этом режиме Telecart работает как каталог.</p>
|
||||
</ItemBool>
|
||||
|
||||
<ItemSelect
|
||||
label="Товары на главной"
|
||||
v-model="settings.items.store.mainpage_products"
|
||||
:items="mainpage_products_options"
|
||||
>
|
||||
Выберите, какие товары показывать на главной странице магазина в Telegram.
|
||||
Это влияет на первую видимую секцию каталога для пользователя.
|
||||
</ItemSelect>
|
||||
|
||||
<ItemProductsSelect
|
||||
label="Избранные товары"
|
||||
v-model="settings.items.store.featured_products"
|
||||
>
|
||||
На главной странице будут отображаться избранные товары, если вы выберете этот вариант в
|
||||
настройке “Товары на главной”. Если товары не выбраны, то будут показаны популярные товары.
|
||||
</ItemProductsSelect>
|
||||
|
||||
<ItemSelect
|
||||
label="Категории на главной"
|
||||
v-model="settings.items.store.mainpage_categories"
|
||||
:items="mainpage_categories_options"
|
||||
>
|
||||
Выберите, какие товары показывать на главной странице магазина в Telegram.
|
||||
Это влияет на первую видимую секцию каталога для пользователя.
|
||||
</ItemSelect>
|
||||
|
||||
<ItemCategoriesSelect
|
||||
label="Избранные категории"
|
||||
v-model="settings.items.store.featured_categories"
|
||||
>
|
||||
На главной странице будут отображаться эти категории,
|
||||
если вы выберете этот вариант в настройке “Категории на главной”.
|
||||
</ItemCategoriesSelect>
|
||||
|
||||
<ItemBool label="Промокоды" v-model="settings.items.store.feature_coupons">
|
||||
<p>
|
||||
Позволяет использовать стандартные
|
||||
<a :href="`/admin/index.php?route=marketing/coupon&user_token=${userToken}`"
|
||||
target="_blank">купоны OpenCart</a>
|
||||
для предоставления скидок при оформлении заказа.</p>
|
||||
</ItemBool>
|
||||
|
||||
<ItemBool label="Подарочные сертификаты" v-model="settings.items.store.feature_vouchers">
|
||||
<p>
|
||||
Позволяет использовать стандартные
|
||||
<a :href="`/admin/index.php?route=sale/voucher&user_token=${userToken}`"
|
||||
target="_blank">подарочные сертификаты OpenCart</a> при оформлении заказа.</p>
|
||||
</ItemBool>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {useSettingsStore} from "@/stores/settings.js";
|
||||
import ItemBool from "@/components/Settings/ItemBool.vue";
|
||||
import ItemSelect from "@/components/Settings/ItemSelect.vue";
|
||||
import ItemProductsSelect from "@/components/Settings/ItemProductsSelect.vue";
|
||||
import ItemCategoriesSelect from "@/components/Settings/ItemCategoriesSelect.vue";
|
||||
|
||||
const settings = useSettingsStore();
|
||||
const mainpage_products_options = {
|
||||
most_viewed: 'Популярные товары',
|
||||
latest: 'Последние сверху',
|
||||
featured: 'Избранные товары (задать в поле ниже)',
|
||||
};
|
||||
const mainpage_categories_options = {
|
||||
no_categories: 'Отображать только кнопку "Каталог"',
|
||||
latest10: 'Последние 10 категорий',
|
||||
featured: 'Избранные категории (задать в поле ниже)',
|
||||
};
|
||||
|
||||
const userToken = window.TeleCart.user_token;
|
||||
</script>
|
||||
28
frontend/admin/src/views/TelegramView.vue
Normal file
28
frontend/admin/src/views/TelegramView.vue
Normal file
@@ -0,0 +1,28 @@
|
||||
<template>
|
||||
<ItemTgMiniAppLink label="Ссылка на Telegram Mini App"
|
||||
v-model="settings.items.telegram.mini_app_url"/>
|
||||
<ItemTgBotToken label="Telegram Bot Token" v-model="settings.items.telegram.bot_token"/>
|
||||
<ItemTgChatID label="Telegram ChatID" v-model="settings.items.telegram.chat_id"/>
|
||||
<ItemTgMessageTemplate
|
||||
label="Шаблон уведомления о новом заказе владельцу"
|
||||
v-model="settings.items.telegram.owner_notification_template"
|
||||
>
|
||||
Введите шаблон сообщения для Telegram-уведомлений о новом заказе владельцу магазина.
|
||||
</ItemTgMessageTemplate>
|
||||
<ItemTgMessageTemplate
|
||||
label="Шаблон уведомления о новом заказе покупателю"
|
||||
v-model="settings.items.telegram.customer_notification_template"
|
||||
>
|
||||
Введите шаблон сообщения для Telegram-уведомлений о новом заказе покупателю.
|
||||
</ItemTgMessageTemplate>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {useSettingsStore} from "@/stores/settings.js";
|
||||
import ItemTgMiniAppLink from "@/components/Settings/ItemTgMiniAppLink.vue";
|
||||
import ItemTgBotToken from "@/components/Settings/ItemTgBotToken.vue";
|
||||
import ItemTgChatID from "@/components/Settings/ItemTgChatID.vue";
|
||||
import ItemTgMessageTemplate from "@/components/Settings/ItemTgMessageTemplate.vue";
|
||||
|
||||
const settings = useSettingsStore();
|
||||
</script>
|
||||
21
frontend/admin/src/views/TextsView.vue
Normal file
21
frontend/admin/src/views/TextsView.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<ItemInput label="Текст в конце списка товаров" v-model="settings.items.texts.text_no_more_products">
|
||||
Текст, отображаемый в конце списка, когда больше нет доступных товаров.
|
||||
Покупатель дошел до конца списка.
|
||||
</ItemInput>
|
||||
|
||||
<ItemInput label="Текст пустой корзины" v-model="settings.items.texts.text_empty_cart">
|
||||
Текст, отображаемый на странице просмотра корзины, если в ней нет товаров.
|
||||
</ItemInput>
|
||||
|
||||
<ItemInput label="Текст для успешного заказа" v-model="settings.items.texts.text_order_created_success">
|
||||
Текст, отображаемый при успешном создании заказа.
|
||||
</ItemInput>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {useSettingsStore} from "@/stores/settings.js";
|
||||
import ItemInput from "@/components/Settings/ItemInput.vue";
|
||||
|
||||
const settings = useSettingsStore();
|
||||
</script>
|
||||
@@ -1,8 +1,12 @@
|
||||
<?php
|
||||
|
||||
use Bastion\ApplicationFactory;
|
||||
use Cart\User;
|
||||
use Openguru\OpenCartFramework\Http\Response as HttpResponse;
|
||||
use Openguru\OpenCartFramework\Logger\LoggerInterface;
|
||||
use Openguru\OpenCartFramework\Logger\OpenCartLogAdapter;
|
||||
use Openguru\OpenCartFramework\OpenCart\Decorators\OcRegistryDecorator;
|
||||
use Openguru\OpenCartFramework\Support\Arr;
|
||||
|
||||
$sysLibPath = rtrim(DIR_SYSTEM, '/') . '/library/oc_telegram_shop';
|
||||
$basePath = rtrim(DIR_APPLICATION, '/') . '/..';
|
||||
@@ -90,16 +94,11 @@ class ControllerExtensionModuleTgshop extends Controller
|
||||
|
||||
public function index(): void
|
||||
{
|
||||
$hasConfig = $this->config->get('module_tgshop_app_name') !== null;
|
||||
|
||||
if ($hasConfig) {
|
||||
$this->updateConfigFromDefaults();
|
||||
$this->cleanUpOldAssets();
|
||||
$this->injectVueJs();
|
||||
$this->config();
|
||||
} else {
|
||||
$this->init();
|
||||
}
|
||||
$this->cleanUpOldAssets();
|
||||
$this->migrateFromOldSettings();
|
||||
$this->removeLegacyFiles();
|
||||
$this->injectVueJs();
|
||||
$this->config();
|
||||
}
|
||||
|
||||
private function config(): void
|
||||
@@ -107,142 +106,78 @@ class ControllerExtensionModuleTgshop extends Controller
|
||||
$data = [];
|
||||
$this->document->setTitle($this->language->get('heading_title'));
|
||||
|
||||
if (($this->request->server['REQUEST_METHOD'] === 'POST') && $this->validate()) {
|
||||
$postData = $this->request->post;
|
||||
$postData['module_tgshop_mainpage_slider'] = [];
|
||||
if (! empty($_POST['module_tgshop_mainpage_slider'])) {
|
||||
$postData['module_tgshop_mainpage_slider'] = $_POST['module_tgshop_mainpage_slider'];
|
||||
}
|
||||
$this->model_setting_setting->editSetting('module_tgshop', $postData);
|
||||
|
||||
$this->session->data['success'] = $this->language->get('text_success');
|
||||
|
||||
$this->response->redirect(
|
||||
$this->url->link(
|
||||
'extension/module/tgshop',
|
||||
'user_token=' . $this->session->data['user_token'] . '&type=module',
|
||||
true
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$this->baseData($data);
|
||||
|
||||
$data['order_statuses'] = $this->getOrderStatuses();
|
||||
$data['customer_groups'] = $this->getCustomerGroups();
|
||||
$data['themes'] = self::$themes;
|
||||
|
||||
$data['action'] = $this->url->link(
|
||||
'extension/module/tgshop',
|
||||
'user_token=' . $this->session->data['user_token'],
|
||||
true
|
||||
);
|
||||
|
||||
$data['settings'] = $this->getSettingsConfig();
|
||||
|
||||
$data['mainpage_slider'] = [];
|
||||
$banners = $this->config->get('module_tgshop_mainpage_slider');
|
||||
if ($banners) {
|
||||
$banners = html_entity_decode($banners);
|
||||
$data['mainpage_slider'] = $banners;
|
||||
}
|
||||
|
||||
foreach ($data['settings'] as $configs) {
|
||||
foreach ($configs as $key => $config) {
|
||||
if ($config['type'] === 'image') {
|
||||
if (isset($this->request->post[$key]) && is_file(DIR_IMAGE . $this->request->post[$key])) {
|
||||
$data[$key] = $this->model_tool_image->resize($this->request->post[$key], 100, 100);
|
||||
} elseif ($this->config->get($key) && is_file(DIR_IMAGE . $this->config->get($key))) {
|
||||
$data[$key] = $this->model_tool_image->resize($this->config->get($key), 100, 100);
|
||||
} else {
|
||||
$data[$key] = $this->model_tool_image->resize('no_image.png', 100, 100);
|
||||
}
|
||||
} elseif ($config['type'] === 'products') {
|
||||
$products = $this->request->post[$key] ?? $this->config->get($key) ?? [];
|
||||
|
||||
$data[$key] = [];
|
||||
foreach ($products as $productId) {
|
||||
$productItem = $this->model_catalog_product->getProduct($productId);
|
||||
$data[$key][] = [
|
||||
'product_id' => $productId,
|
||||
'name' => $productItem['name'],
|
||||
];
|
||||
}
|
||||
} elseif ($config['type'] === 'categories') {
|
||||
$categories = $this->request->post[$key] ?? $this->config->get($key) ?? [];
|
||||
|
||||
$data[$key] = [];
|
||||
foreach ($categories as $categoryId) {
|
||||
$categoryItem = $this->model_catalog_category->getCategory($categoryId);
|
||||
$data[$key][] = [
|
||||
'category_id' => $categoryId,
|
||||
'name' => $categoryItem['name'],
|
||||
];
|
||||
}
|
||||
} elseif (isset($this->request->post[$key])) {
|
||||
$data[$key] = $this->request->post[$key];
|
||||
} else {
|
||||
$data[$key] = $this->config->get($key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->response->setOutput($this->load->view('extension/module/tgshop', $data));
|
||||
}
|
||||
|
||||
public function init(): void
|
||||
{
|
||||
$data = [];
|
||||
$this->baseData($data);
|
||||
|
||||
$data['action'] = $this->url->link(
|
||||
'extension/module/tgshop/init',
|
||||
'user_token=' . $this->session->data['user_token'],
|
||||
true
|
||||
);
|
||||
|
||||
if ($this->request->server['REQUEST_METHOD'] === 'POST') {
|
||||
$defaults = $this->getDefaultConfig();
|
||||
$this->model_setting_setting->editSetting('module_tgshop', $defaults);
|
||||
$this->session->data['success'] = 'Инициализация модуля выполнена успешно.';
|
||||
$this->response->redirect(
|
||||
$this->url->link(
|
||||
'extension/module/tgshop',
|
||||
'user_token=' . $this->session->data['user_token'],
|
||||
true
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$this->response->setOutput($this->load->view('extension/module/tgshop_init', $data));
|
||||
}
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
$app = ApplicationFactory::create([
|
||||
'base_url' => HTTPS_SERVER,
|
||||
'public_url' => HTTPS_CATALOG,
|
||||
'telegram' => [
|
||||
'bot_token' => $this->config->get('module_tgshop_bot_token'),
|
||||
'chat_id' => $this->config->get('module_tgshop_chat_id'),
|
||||
'owner_notification_template' => $this->config->get('module_tgshop_owner_notification_template'),
|
||||
'customer_notification_template' => $this->config->get('module_tgshop_customer_notification_template'),
|
||||
],
|
||||
'db' => [
|
||||
'host' => DB_HOSTNAME,
|
||||
'database' => DB_DATABASE,
|
||||
'username' => DB_USERNAME,
|
||||
'password' => DB_PASSWORD,
|
||||
'prefix' => DB_PREFIX,
|
||||
'port' => DB_PORT,
|
||||
],
|
||||
'logs' => [
|
||||
'path' => DIR_LOGS,
|
||||
],
|
||||
]);
|
||||
try {
|
||||
$json = $this->model_setting_setting->getSetting('module_telecart');
|
||||
if (! isset($json['module_telecart_settings'])) {
|
||||
$json['module_telecart_settings'] = [];
|
||||
}
|
||||
|
||||
$items = Arr::mergeArraysRecursively($json['module_telecart_settings'], [
|
||||
'app' => [
|
||||
'shop_base_url' => HTTPS_CATALOG, // for catalog: HTTPS_SERVER, for admin: HTTPS_CATALOG
|
||||
'language_id' => (int) $this->config->get('config_language_id'),
|
||||
],
|
||||
'logs' => [
|
||||
'path' => DIR_LOGS,
|
||||
],
|
||||
'database' => [
|
||||
'host' => DB_HOSTNAME,
|
||||
'database' => DB_DATABASE,
|
||||
'username' => DB_USERNAME,
|
||||
'password' => DB_PASSWORD,
|
||||
'prefix' => DB_PREFIX,
|
||||
'port' => (int) DB_PORT,
|
||||
],
|
||||
'store' => [
|
||||
'oc_store_id' => 0,
|
||||
'oc_default_currency' => $this->config->get('config_currency'),
|
||||
'oc_config_tax' => filter_var($this->config->get('config_tax'), FILTER_VALIDATE_BOOLEAN),
|
||||
],
|
||||
'orders' => [
|
||||
'oc_customer_group_id' => (int) $this->config->get('config_customer_group_id'),
|
||||
],
|
||||
'telegram' => [
|
||||
'mini_app_url' => rtrim(HTTPS_CATALOG, '/') . '/image/catalog/tgshopspa/#/',
|
||||
],
|
||||
]);
|
||||
|
||||
$app->bind(OcRegistryDecorator::class, fn() => new OcRegistryDecorator($this->registry));
|
||||
$app = ApplicationFactory::create($items);
|
||||
$app->bind(OcRegistryDecorator::class, fn() => new OcRegistryDecorator($this->registry));
|
||||
|
||||
$app
|
||||
->withLogger(fn() => new OpenCartLogAdapter($this->log, 'TeleCartAdmin'))
|
||||
->bootAndHandleRequest();
|
||||
$app
|
||||
->withLogger(fn() => new OpenCartLogAdapter(
|
||||
$this->log,
|
||||
'TeleCartAdmin',
|
||||
$app->getConfigValue('app.app_debug')
|
||||
? LoggerInterface::LEVEL_DEBUG
|
||||
: LoggerInterface::LEVEL_WARNING,
|
||||
))
|
||||
->bootAndHandleRequest();
|
||||
} catch (Exception $e) {
|
||||
$this->log->write('[TELECART] Error: ' . $e->getMessage());
|
||||
http_response_code(HttpResponse::HTTP_INTERNAL_SERVER_ERROR);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode([
|
||||
'error' => 'Ошибка сервера. Приносим свои извинения за неудобства.',
|
||||
], JSON_THROW_ON_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
protected function validate(): bool
|
||||
@@ -251,16 +186,6 @@ class ControllerExtensionModuleTgshop extends Controller
|
||||
$this->error['telecart_error_warning'] = $this->language->get('error_permission');
|
||||
}
|
||||
|
||||
foreach ($this->getSettingsConfig() as $configs) {
|
||||
foreach ($configs as $key => $config) {
|
||||
if (($config['required'] ?? false) === true && ! $this->request->post[$key]) {
|
||||
$this->error["error_$key"] = 'Поле "' . $this->language->get(
|
||||
"lbl_$key"
|
||||
) . '" обязательно для заполнения.';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ! $this->error;
|
||||
}
|
||||
|
||||
@@ -316,286 +241,6 @@ class ControllerExtensionModuleTgshop extends Controller
|
||||
$data['user_token'] = $this->session->data['user_token'];
|
||||
}
|
||||
|
||||
private function getDefaultConfig(): array
|
||||
{
|
||||
return [
|
||||
'module_tgshop_status' => 1,
|
||||
'module_tgshop_debug' => 0,
|
||||
'module_tgshop_app_name' => $this->config->get('config_meta_title'),
|
||||
'module_tgshop_app_icon' => $this->config->get('config_image') ?: $this->model_tool_image->resize(
|
||||
'no_image.png',
|
||||
100,
|
||||
100
|
||||
),
|
||||
'module_tgshop_owner_notification_template' => <<<TEXT
|
||||
*Новый заказ \#{order_id}* в магазине *{store_name}*
|
||||
|
||||
*Покупатель:* {customer}
|
||||
*Email:* {email}
|
||||
*Телефон:* {phone}
|
||||
*IP:* {ip}
|
||||
|
||||
*Адрес доставки:*
|
||||
{address}
|
||||
|
||||
*Комментарий:*
|
||||
{comment}
|
||||
|
||||
*Сумма заказа:* {total}
|
||||
*Дата оформления:* {created_at}
|
||||
TEXT,
|
||||
'module_tgshop_customer_notification_template' => <<<TEXT
|
||||
Спасибо за Ваш заказ в магазине *{store_name}*
|
||||
|
||||
*Номер заказа* \#{order_id}
|
||||
*Сумма заказа:* {total}
|
||||
*Дата оформления:* {created_at}
|
||||
|
||||
Мы свяжемся с вами при необходимости\.
|
||||
Хорошего дня\!
|
||||
TEXT,
|
||||
'module_tgshop_theme_light' => 'light',
|
||||
'module_tgshop_theme_dark' => 'dark',
|
||||
'module_tgshop_mainpage_products' => 'most_viewed',
|
||||
'module_tgshop_order_customer_group_id' => 1,
|
||||
'module_tgshop_order_default_status_id' => 1,
|
||||
'module_tgshop_mini_app_url' => rtrim(HTTPS_CATALOG, '/') . '/image/catalog/tgshopspa/#/',
|
||||
'module_tgshop_mainpage_categories' => 'latest10',
|
||||
'module_tgshop_enable_store' => 1,
|
||||
'module_tgshop_feature_coupons' => 0,
|
||||
'module_tgshop_feature_vouchers' => 0,
|
||||
'module_tgshop_text_no_more_products' => 'Это всё по текущему запросу. Попробуйте уточнить фильтры или поиск.',
|
||||
'module_tgshop_text_empty_cart' => 'Ваша корзина пуста',
|
||||
'module_tgshop_text_order_created_success' => 'Ваш заказ успешно оформлен и будет обработан в ближайшее время.',
|
||||
'module_tgshop_mainpage_slider' => json_encode([
|
||||
'is_enabled' => false,
|
||||
'effect' => 'slide',
|
||||
'pagination' => true,
|
||||
'scrollbar' => false,
|
||||
'free_mode' => false,
|
||||
'space_between' => 30,
|
||||
'autoplay' => false,
|
||||
'loop' => false,
|
||||
'slides' => [],
|
||||
], JSON_THROW_ON_ERROR),
|
||||
'module_tgshop_yandex_metrika' => '',
|
||||
'module_tgshop_chat_id' => '',
|
||||
'module_tgshop_bot_token' => '',
|
||||
];
|
||||
}
|
||||
|
||||
private function getSettingsConfig(): array
|
||||
{
|
||||
$ocCouponsLink = $this->url->link(
|
||||
'marketing/coupon',
|
||||
'user_token=' . $this->session->data['user_token'],
|
||||
true
|
||||
);
|
||||
$ocVouchersLink = $this->url->link(
|
||||
'sale/voucher',
|
||||
'user_token=' . $this->session->data['user_token'],
|
||||
true
|
||||
);
|
||||
|
||||
return [
|
||||
'general' => [
|
||||
'module_tgshop_status' => [
|
||||
'type' => 'select',
|
||||
'options' => [
|
||||
0 => 'Выключено',
|
||||
1 => 'Включено',
|
||||
],
|
||||
'help' => 'Если выключено, покупатели в Telegram увидят сообщение, что магазин временно закрыт. Заказы и просмотр товаров будут недоступны.',
|
||||
],
|
||||
|
||||
'module_tgshop_app_name' => [
|
||||
'type' => 'text',
|
||||
'placeholder' => 'Без названия',
|
||||
'help' => <<<TEXT
|
||||
Отображается в заголовке Telegram Mini App при запуске, а также используется как подпись
|
||||
под иконкой, если пользователь добавит приложение на главный экран своего устройства.
|
||||
Рекомендуется короткое и понятное название (до 20 символов).
|
||||
Если оставить пустым, то название выводиться не будет.
|
||||
TEXT,
|
||||
],
|
||||
|
||||
'module_tgshop_app_icon' => [
|
||||
'type' => 'image',
|
||||
'help' => <<<TEXT
|
||||
Изображение, которое будет отображаться в Telegram Mini App и на рабочем столе устройства,
|
||||
если пользователь добавит приложение как ярлык. Рекомендуется использовать квадратное изображение PNG или SVG,
|
||||
размером 32×32 пикселей.
|
||||
TEXT,
|
||||
],
|
||||
|
||||
'module_tgshop_theme_light' => [
|
||||
'type' => 'select',
|
||||
'options' => static::$themes,
|
||||
'help' => 'Выберите стиль, который будет использоваться при отображении вашего магазина в Telegram для дневного режима. <a href="https://daisyui.com/docs/themes/#list-of-themes" target="_blank">Посмотреть как выглядят темы</a>',
|
||||
],
|
||||
|
||||
'module_tgshop_theme_dark' => [
|
||||
'type' => 'select',
|
||||
'options' => static::$themes,
|
||||
'help' => 'Выберите стиль, который будет использоваться при отображении вашего магазина в Telegram для ночного режима. <a href="https://daisyui.com/docs/themes/#list-of-themes" target="_blank">Посмотреть как выглядят темы</a>',
|
||||
],
|
||||
|
||||
'module_tgshop_debug' => [
|
||||
'type' => 'select',
|
||||
'options' => [
|
||||
0 => 'Выключено',
|
||||
1 => 'Включено',
|
||||
],
|
||||
'help' => 'Режим разработчика. Рекомендуется включать только по необходимости. В остальных случаях, для нормальной работы магазина, должен быть выключен.',
|
||||
],
|
||||
],
|
||||
'telegram' => [
|
||||
'module_tgshop_mini_app_url' => [
|
||||
'type' => 'text_readonly',
|
||||
'help' => <<<HTML
|
||||
Это прямая ссылка на ваш Telegram Mini App. Скопируйте её в точности, как указано — и добавьте в настройки Telegram-бота.
|
||||
<p class="text-warning">⚠️ Важно: ссылка обязательно должна заканчиваться на /#/ — иначе приложение не загрузится.</p>
|
||||
HTML,
|
||||
],
|
||||
|
||||
'module_tgshop_bot_token' => [
|
||||
'type' => 'bot_token',
|
||||
'placeholder' => 'Введите токен от телеграм бота',
|
||||
'help' => <<<TEXT
|
||||
Токен, полученный при создании бота через @BotFather.
|
||||
Он используется для взаимодействия модуля с Telegram API.
|
||||
TEXT,
|
||||
],
|
||||
'module_tgshop_chat_id' => [
|
||||
'type' => 'chatid',
|
||||
'placeholder' => 'Введите Chat ID',
|
||||
'help' => <<<TEXT
|
||||
Идентификатор Telegram-чата, куда будут отправляться уведомления о новых заказах.
|
||||
Если оставить поле пустым, уведомления отправляться не будут.
|
||||
TEXT,
|
||||
],
|
||||
'module_tgshop_owner_notification_template' => [
|
||||
'type' => 'tg_message_template',
|
||||
'placeholder' => 'Введите текст уведомления',
|
||||
'rows' => 15,
|
||||
'help' => 'Введите шаблон сообщения для Telegram-уведомлений о новом заказе владельцу магазина.',
|
||||
],
|
||||
'module_tgshop_customer_notification_template' => [
|
||||
'type' => 'tg_message_template',
|
||||
'placeholder' => 'Введите текст уведомления',
|
||||
'rows' => 15,
|
||||
'help' => 'Введите шаблон сообщения для Telegram-уведомлений о новом заказе покупателю.',
|
||||
],
|
||||
],
|
||||
'statistics' => [
|
||||
'module_tgshop_yandex_metrika' => [
|
||||
'type' => 'textarea',
|
||||
'placeholder' => 'Вставьте код счётчика Яндекс Метрики.',
|
||||
'rows' => 15,
|
||||
'help' => 'Для проверки интеграции через кнопку "Проверить" в интерфейсе Яндекс Метрики, необходимо сначала включить "Режим разработчика" на вкладке "Общие".'
|
||||
],
|
||||
],
|
||||
'shop' => [
|
||||
'module_tgshop_enable_store' => [
|
||||
'type' => 'select',
|
||||
'options' => [
|
||||
0 => 'Выключено',
|
||||
1 => 'Включено',
|
||||
],
|
||||
'help' => <<<HTML
|
||||
Если опция <strong>включена</strong> — пользователи смогут оформлять заказы прямо в Telegram-магазине. <br>
|
||||
Если <strong>выключена</strong> — оформление заказов будет недоступно. Вместо кнопки «Добавить в корзину» пользователи увидят кнопку «Перейти к товару», которая откроет страницу товара на вашем сайте. В этом режиме Telecart работает как каталог.
|
||||
HTML,
|
||||
],
|
||||
'module_tgshop_mainpage_products' => [
|
||||
'type' => 'select',
|
||||
'options' => [
|
||||
'most_viewed' => 'Популярные товары',
|
||||
'latest' => 'Последние сверху',
|
||||
'featured' => 'Избранные товары (задать в поле ниже)',
|
||||
],
|
||||
'help' => 'Выберите, какие товары показывать на главной странице магазина в Telegram. Это влияет на первую видимую секцию каталога для пользователя.',
|
||||
],
|
||||
|
||||
'module_tgshop_featured_products' => [
|
||||
'type' => 'products',
|
||||
'help' => 'На главной странице будут отображаться избранные товары, если вы выберете этот вариант в настройке “Товары на главной”. Если товары не выбраны, то будут показаны популярные товары.',
|
||||
],
|
||||
|
||||
'module_tgshop_mainpage_categories' => [
|
||||
'type' => 'select',
|
||||
'options' => [
|
||||
'no_categories' => 'Отображать только кнопку "Каталог"',
|
||||
'latest10' => 'Последние 10 категорий',
|
||||
'featured' => 'Избранные категории (задать в поле ниже)',
|
||||
],
|
||||
'help' => 'Выберите, какие категории показывать на главной странице магазина в Telegram. Это влияет на первую видимую секцию каталога для пользователя.',
|
||||
],
|
||||
|
||||
'module_tgshop_featured_categories' => [
|
||||
'type' => 'categories',
|
||||
'help' => 'На главной странице будут отображаться эти категории, если вы выберете этот вариант в настройке “Категории на главной”.',
|
||||
],
|
||||
|
||||
'module_tgshop_feature_coupons' => [
|
||||
'type' => 'select',
|
||||
'options' => [
|
||||
0 => 'Выключено',
|
||||
1 => 'Включено',
|
||||
],
|
||||
'help' => <<<HTML
|
||||
Позволяет использовать стандартные <a href="{$ocCouponsLink}" target="_blank">купоны OpenCart</a> для предоставления скидок при оформлении заказа.
|
||||
HTML,
|
||||
],
|
||||
|
||||
'module_tgshop_feature_vouchers' => [
|
||||
'type' => 'select',
|
||||
'options' => [
|
||||
0 => 'Выключено',
|
||||
1 => 'Включено',
|
||||
],
|
||||
'help' => <<<HTML
|
||||
Позволяет покупателям использовать <a href="{$ocVouchersLink}" target="_blank">подарочные сертификаты OpenCart</a> при оформлении заказа.
|
||||
HTML,
|
||||
],
|
||||
],
|
||||
'orders' => [
|
||||
'module_tgshop_order_default_status_id' => [
|
||||
'type' => 'select',
|
||||
'options' => $this->getOrderStatuses(),
|
||||
'help' => 'Статус, с которым будут создаваться заказы через Telegram по умолчанию.',
|
||||
],
|
||||
|
||||
'module_tgshop_order_customer_group_id' => [
|
||||
'hidden' => true,
|
||||
'type' => 'select',
|
||||
'options' => $this->getCustomerGroups(),
|
||||
'help' => 'Группа покупателей, которая будет назначена для заказов, оформленных через Telegram-магазин.',
|
||||
],
|
||||
],
|
||||
|
||||
'texts' => [
|
||||
'module_tgshop_text_no_more_products' => [
|
||||
'type' => 'text',
|
||||
'placeholder' => 'Это всё по текущему запросу. Попробуйте уточнить фильтры или поиск.',
|
||||
'help' => 'Текст, отображаемый в конце списка, когда больше нет доступных товаров. Покупатель дошел до конца списка.',
|
||||
],
|
||||
|
||||
'module_tgshop_text_empty_cart' => [
|
||||
'type' => 'text',
|
||||
'placeholder' => 'Ваша корзина пуста',
|
||||
'help' => 'Текст, отображаемый на странице просмотра корзины, если в ней нет товаров.',
|
||||
],
|
||||
|
||||
'module_tgshop_text_order_created_success' => [
|
||||
'type' => 'text',
|
||||
'placeholder' => 'Ваш заказ успешно оформлен и будет обработан в ближайшее время.',
|
||||
'help' => 'Текст, отображаемый при успешном создании заказа.',
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
private function getCustomerGroups(): array
|
||||
{
|
||||
$map = [];
|
||||
@@ -607,7 +252,7 @@ HTML,
|
||||
return $map;
|
||||
}
|
||||
|
||||
private function getOrderStatuses()
|
||||
private function getOrderStatuses(): array
|
||||
{
|
||||
$statuses = $this->model_localisation_order_status->getOrderStatuses();
|
||||
$map = [];
|
||||
@@ -619,45 +264,6 @@ HTML,
|
||||
return $map;
|
||||
}
|
||||
|
||||
private function updateConfigFromDefaults(): void
|
||||
{
|
||||
$defaults = $this->getDefaultConfig();
|
||||
$settings = $this->model_setting_setting->getSetting('module_tgshop');
|
||||
|
||||
$diff = [];
|
||||
foreach ($defaults as $key => $value) {
|
||||
if (! array_key_exists($key, $settings)) {
|
||||
$diff[$key] = $defaults[$key];
|
||||
}
|
||||
}
|
||||
|
||||
if ($diff) {
|
||||
$settings = array_merge($settings, $diff);
|
||||
$this->model_setting_setting->editSetting('module_tgshop', $settings);
|
||||
$this->log->write('[TELECART] Выполнено обновление настроек по умолчанию для модуля.');
|
||||
$this->session->data['success'] = 'Выполнено обновление настроек по умолчанию для модуля.';
|
||||
|
||||
foreach ($diff as $key => $value) {
|
||||
$this->config->set($key, $value);
|
||||
}
|
||||
}
|
||||
|
||||
$diffToDelete = [];
|
||||
foreach ($settings as $key => $value) {
|
||||
if (! array_key_exists($key, $defaults)) {
|
||||
$diffToDelete[] = $key;
|
||||
}
|
||||
}
|
||||
|
||||
if ($diffToDelete) {
|
||||
$keys = implode(', ', array_map(function ($key) {
|
||||
return "'{$key}'";
|
||||
}, $diffToDelete));
|
||||
$this->db->query("DELETE FROM " . DB_PREFIX . "setting WHERE `key` IN ($keys)");
|
||||
$this->log->write('[TELECART] Удалены старые конфиги: ' . $keys);
|
||||
}
|
||||
}
|
||||
|
||||
private function cleanUpOldAssets(): void
|
||||
{
|
||||
$spaPath = rtrim(DIR_IMAGE, '/') . '/catalog/tgshopspa';
|
||||
@@ -733,4 +339,95 @@ HTML,
|
||||
throw new RuntimeException('Unable to load Vuejs frontend.');
|
||||
}
|
||||
}
|
||||
|
||||
private function migrateFromOldSettings(): void
|
||||
{
|
||||
$legacySettings = $this->model_setting_setting->getSetting('module_tgshop');
|
||||
if (! $legacySettings) {
|
||||
return;
|
||||
}
|
||||
|
||||
$newSettings = $this->model_setting_setting->getSetting('module_telecart');
|
||||
|
||||
static $mapLegacyToNewSettings = [
|
||||
'module_tgshop_app_icon' => 'app.app_icon',
|
||||
'module_tgshop_theme_light' => 'app.theme_light',
|
||||
'module_tgshop_bot_token' => 'telegram.bot_token',
|
||||
'module_tgshop_status' => 'app.app_enabled',
|
||||
'module_tgshop_app_name' => 'app.app_name',
|
||||
'module_tgshop_theme_dark' => 'app.theme_dark',
|
||||
'module_tgshop_debug' => 'app.app_debug',
|
||||
'module_tgshop_chat_id' => 'telegram.chat_id',
|
||||
'module_tgshop_owner_notification_template' => 'telegram.owner_notification_template',
|
||||
'module_tgshop_text_order_created_success' => 'texts.text_order_created_success',
|
||||
'module_tgshop_enable_store' => 'store.enable_store',
|
||||
'module_tgshop_mainpage_products' => 'store.mainpage_products',
|
||||
'module_tgshop_yandex_metrika' => 'metrics.yandex_metrika_counter',
|
||||
'module_tgshop_customer_notification_template' => 'telegram.customer_notification_template',
|
||||
'module_tgshop_feature_vouchers' => 'store.feature_vouchers',
|
||||
'module_tgshop_order_default_status_id' => 'orders.order_default_status_id',
|
||||
'module_tgshop_feature_coupons' => 'store.feature_coupons',
|
||||
'module_tgshop_mainpage_categories' => 'store.mainpage_categories',
|
||||
'module_tgshop_text_no_more_products' => 'texts.text_no_more_products',
|
||||
'module_tgshop_text_empty_cart' => 'texts.text_empty_cart',
|
||||
];
|
||||
|
||||
if (! $newSettings) {
|
||||
$data = [];
|
||||
Arr::set($data, 'app.app_icon', $legacySettings['module_tgshop_app_icon']);
|
||||
|
||||
foreach ($mapLegacyToNewSettings as $key => $value) {
|
||||
if (array_key_exists($key, $legacySettings)) {
|
||||
if ($key === 'module_tgshop_status') {
|
||||
$newValue = filter_var($legacySettings[$key], FILTER_VALIDATE_BOOLEAN);
|
||||
} elseif ($key === 'module_tgshop_debug') {
|
||||
$newValue = filter_var($legacySettings[$key], FILTER_VALIDATE_BOOLEAN);
|
||||
} elseif ($key === 'module_tgshop_chat_id') {
|
||||
$newValue = (int) $legacySettings[$key];
|
||||
} elseif ($key === 'module_tgshop_enable_store') {
|
||||
$newValue = filter_var($legacySettings[$key], FILTER_VALIDATE_BOOLEAN);
|
||||
} elseif ($key === 'module_tgshop_order_default_status_id') {
|
||||
$newValue = (int) $legacySettings[$key];
|
||||
} elseif ($key === 'module_tgshop_feature_vouchers') {
|
||||
$newValue = filter_var($legacySettings[$key], FILTER_VALIDATE_BOOLEAN);
|
||||
} elseif ($key === 'module_tgshop_feature_coupons') {
|
||||
$newValue = filter_var($legacySettings[$key], FILTER_VALIDATE_BOOLEAN);
|
||||
} else {
|
||||
$newValue = $legacySettings[$key];
|
||||
}
|
||||
|
||||
Arr::set($data, $value, $newValue);
|
||||
}
|
||||
}
|
||||
|
||||
Arr::set(
|
||||
$data,
|
||||
'metrics.yandex_metrika_enabled',
|
||||
! empty(trim($legacySettings['module_tgshop_yandex_metrika']))
|
||||
);
|
||||
|
||||
$this->model_setting_setting->editSetting('module_telecart', [
|
||||
'module_telecart_settings' => $data,
|
||||
]);
|
||||
|
||||
$this->log->write('[TELECART] Выполнено обновление настроек с 1й версии модуля.');
|
||||
$this->session->data['success'] = 'Выполнено обновление настроек с прошлой версии модуля.';
|
||||
}
|
||||
|
||||
$this->model_setting_setting->deleteSetting('module_tgshop');
|
||||
}
|
||||
|
||||
private function removeLegacyFiles(): void
|
||||
{
|
||||
$legacyFilesToRemove = [
|
||||
DIR_TEMPLATE . '/extension/module/tgshop_init.twig',
|
||||
];
|
||||
|
||||
foreach ($legacyFilesToRemove as $file) {
|
||||
if (file_exists($file)) {
|
||||
unlink($file);
|
||||
$this->log->write('[TELECART] Удалён старый файл: ' . $file);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,6 @@
|
||||
<div id="content">
|
||||
<div class="page-header">
|
||||
<div class="container-fluid">
|
||||
<div class="pull-right">
|
||||
<button type="submit" form="form-module" data-toggle="tooltip" title="{{ button_save }}"
|
||||
class="btn btn-primary"><i class="fa fa-save"></i></button>
|
||||
<a href="{{ cancel }}" data-toggle="tooltip" title="{{ button_cancel }}" class="btn btn-default"><i
|
||||
class="fa fa-reply"></i></a></div>
|
||||
<h1>{{ heading_title }}</h1>
|
||||
<ul class="breadcrumb">
|
||||
{% for breadcrumb in breadcrumbs %}
|
||||
@@ -27,438 +22,18 @@
|
||||
<button type="button" class="close" data-dismiss="alert">×</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title"><i class="fa fa-pencil"></i> {{ text_edit }}</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<form action="{{ action }}" method="post" enctype="multipart/form-data" id="form-module"
|
||||
class="form-horizontal">
|
||||
|
||||
<ul class="nav nav-tabs">
|
||||
{% for tabKey, tabItems in settings %}
|
||||
<li{% if tabKey == 'general' %} class="active" {% endif %}>
|
||||
<a href="#{{ tabKey }}" data-toggle="tab">
|
||||
{% if attribute(_context, 'tab_' ~ tabKey) %}
|
||||
{{ attribute(_context, 'tab_' ~ tabKey) }}
|
||||
{% else %}
|
||||
{{ 'tab_' ~ tabKey }}
|
||||
{% endif %}
|
||||
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
<li>
|
||||
<a href="#banners" data-toggle="tab">
|
||||
Баннеры
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content">
|
||||
{% for tabKey, tabItems in settings %}
|
||||
<div class="tab-pane{%if tabKey == 'general' %} active{% endif %}" id="{{ tabKey }}">
|
||||
{% for settingKey, item in tabItems %}
|
||||
<div class="form-group{%if item['required'] %} required{% endif %}{% if item['hidden'] %} hidden{% endif %}">
|
||||
<label class="col-sm-2 control-label" for="{{ settingKey }}">
|
||||
{% if attribute(_context, 'lbl_' ~ settingKey) %}
|
||||
{{ attribute(_context, 'lbl_' ~ settingKey) }}
|
||||
{% else %}
|
||||
{{ 'lbl_' ~ settingKey }}
|
||||
{% endif %}
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
{# Select #}
|
||||
{% if item['type'] == 'select' %}
|
||||
<select name="{{ settingKey }}" id="{{ settingKey }}" class="form-control">
|
||||
{% for key, value in item['options'] %}
|
||||
<option value="{{ key }}" {% if key == attribute(_context, settingKey) %}selected="selected"{% endif %}>
|
||||
{{ value }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
{# Text Input #}
|
||||
{% elseif item['type'] == 'text' %}
|
||||
<input type="text"
|
||||
name="{{ settingKey }}"
|
||||
value="{{ attribute(_context, settingKey) }}"
|
||||
placeholder="{{ item['placeholder'] }}"
|
||||
id="{{ settingKey }}"
|
||||
class="form-control"
|
||||
/>
|
||||
|
||||
{# Image #}
|
||||
{% elseif item['type'] == 'image' %}
|
||||
<a href="" id="thumb-image-{{ settingKey }}" data-toggle="image" class="img-thumbnail">
|
||||
<img src="{{ attribute(_context, settingKey) }}"
|
||||
data-placeholder="https://placehold.co/100x100?text=Удалено"
|
||||
/>
|
||||
</a>
|
||||
<input type="hidden"
|
||||
name="{{ settingKey }}"
|
||||
value="{{ attribute(_context, settingKey) }}"
|
||||
id="{{ settingKey }}"
|
||||
/>
|
||||
{# Textarea #}
|
||||
{% elseif item['type'] == 'textarea' %}
|
||||
<textarea name="{{ settingKey }}"
|
||||
rows="{{ item['rows'] }}"
|
||||
placeholder="{{ item['placeholder'] }}"
|
||||
id="{{ settingKey }}"
|
||||
class="form-control"
|
||||
>{{ attribute(_context, settingKey) }}</textarea>
|
||||
{# Products #}
|
||||
{% elseif item['type'] == 'products' %}
|
||||
<input type="text" value="" placeholder="Начните вводить название товара..." id="{{ settingKey }}-input" class="form-control"/>
|
||||
<div id="{{ settingKey }}-list" class="well well-sm" style="height: 150px; overflow: auto;">
|
||||
{% for product in attribute(_context, settingKey) %}
|
||||
<div id="{{ settingKey }}-{{ product.product_id }}">
|
||||
<i class="fa fa-minus-circle"></i> {{ product.name }}
|
||||
<input type="hidden" name="{{ settingKey }}[]" value="{{ product.product_id }}"/>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<script>
|
||||
$('#{{ settingKey }}-input').autocomplete({
|
||||
'source': function(request, response) {
|
||||
$.ajax({
|
||||
url: 'index.php?route=catalog/product/autocomplete&user_token={{ user_token }}&filter_name=' + encodeURIComponent(request),
|
||||
dataType: 'json',
|
||||
success: function(json) {
|
||||
response($.map(json, function(item) {
|
||||
return {
|
||||
label: item['name'],
|
||||
value: item['product_id']
|
||||
}
|
||||
}));
|
||||
}
|
||||
});
|
||||
},
|
||||
'select': function(item) {
|
||||
$('#{{ settingKey }}').val('');
|
||||
$('#{{ settingKey }}-' + item['value']).remove();
|
||||
$('#{{ settingKey }}-list').append('<div id="{{ settingKey }}-' + item['value'] + '"><i class="fa fa-minus-circle"></i> ' + item['label'] + '<input type="hidden" name="{{ settingKey }}[]" value="' + item['value'] + '" /></div>');
|
||||
}
|
||||
});
|
||||
|
||||
$('#{{ settingKey }}-list').delegate('.fa-minus-circle', 'click', function() {
|
||||
$(this).parent().remove();
|
||||
});
|
||||
</script>
|
||||
|
||||
{% elseif item['type'] == 'categories' %}
|
||||
<input type="text" value="" placeholder="Начните вводить название категории..." id="{{ settingKey }}-input" class="form-control"/>
|
||||
<div id="{{ settingKey }}-list" class="well well-sm" style="height: 150px; overflow: auto;">
|
||||
{% for category in attribute(_context, settingKey) %}
|
||||
<div id="{{ settingKey }}-{{ category.category_id }}">
|
||||
<i class="fa fa-minus-circle"></i> {{ category.name }}
|
||||
<input type="hidden" name="{{ settingKey }}[]" value="{{ category.category_id }}"/>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<script>
|
||||
$('#{{ settingKey }}-input').autocomplete({
|
||||
'source': function(request, response) {
|
||||
$.ajax({
|
||||
url: 'index.php?route=catalog/category/autocomplete&user_token={{ user_token }}&filter_name=' + encodeURIComponent(request),
|
||||
dataType: 'json',
|
||||
success: function(json) {
|
||||
response($.map(json, function(item) {
|
||||
return {
|
||||
label: item['name'],
|
||||
value: item['category_id']
|
||||
}
|
||||
}));
|
||||
}
|
||||
});
|
||||
},
|
||||
'select': function(item) {
|
||||
$('#{{ settingKey }}').val('');
|
||||
$('#{{ settingKey }}-' + item['value']).remove();
|
||||
$('#{{ settingKey }}-list').append('<div id="{{ settingKey }}-' + item['value'] + '"><i class="fa fa-minus-circle"></i> ' + item['label'] + '<input type="hidden" name="{{ settingKey }}[]" value="' + item['value'] + '" /></div>');
|
||||
}
|
||||
});
|
||||
|
||||
$('#{{ settingKey }}-list').delegate('.fa-minus-circle', 'click', function() {
|
||||
$(this).parent().remove();
|
||||
});
|
||||
</script>
|
||||
{# ChatID #}
|
||||
{% elseif item['type'] == 'chatid' %}
|
||||
{% if module_tgshop_bot_token %}
|
||||
<div class="input-group">
|
||||
<span class="input-group-btn">
|
||||
<button id="{{ settingKey }}-btn" class="btn btn-primary" type="button">
|
||||
<i class="fa fa-refresh"></i> Получить Chat ID
|
||||
</button>
|
||||
</span>
|
||||
<input type="text"
|
||||
name="{{ settingKey }}"
|
||||
value="{{ attribute(_context, settingKey) }}"
|
||||
placeholder="{{ item['placeholder'] }}"
|
||||
id="{{ settingKey }}"
|
||||
class="form-control"
|
||||
/>
|
||||
<script>
|
||||
$('#{{ settingKey }}-btn').click(function () {
|
||||
const $resultLabel = $('#{{ settingKey }}-result-label');
|
||||
const telegramToken = $('#module_tgshop_bot_token').val().trim(); // fetch from input
|
||||
if (! telegramToken) {
|
||||
alert('Сначала введите Telegram Bot Token!');
|
||||
return;
|
||||
}
|
||||
|
||||
fetch('/admin/index.php?route=extension/module/tgshop/handle&api_action=getChatId&user_token={{ user_token }}')
|
||||
.then(async (res) => {
|
||||
const data = await res.json().catch(() => null);
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`Ошибка ${res.status}: ${data.message || res.statusText}`);
|
||||
}
|
||||
|
||||
$('#{{ settingKey }}').val(data.data.chat_id);
|
||||
$resultLabel
|
||||
.text('✅ ChatID успешно получен и подставлен в поле. Не забудьте сохранить настройки!')
|
||||
.css('color', 'green');
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
alert(err);
|
||||
});
|
||||
});
|
||||
|
||||
</script>
|
||||
</div>
|
||||
|
||||
<div id="{{ settingKey }}-result-label"></div>
|
||||
|
||||
<button class="btn btn-link btn-xs" type="button" data-toggle="collapse" data-target="#{{ settingKey }}-collapse" aria-expanded="false" aria-controls="collapseExample">
|
||||
Инструкция как получить ChatID.
|
||||
</button>
|
||||
<div class="collapse" id="{{ settingKey }}-collapse">
|
||||
<div class="well">
|
||||
<p class="text-primary">Как получить Chat ID</p>
|
||||
<ol>
|
||||
<li>Убедитесь, что Telegram Bot Token введён выше.</li>
|
||||
<li>Откройте вашего бота в Telegram и отправьте ему кодовое слово: `opencart_get_chatid`. Важно отправить именно такое сообщение, иначе не сработает.</li>
|
||||
<li>Вернитесь сюда и нажмите кнопку «Получить Chat ID» — скрипт автоматически подставит его в поле ниже.</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-warning">
|
||||
<strong>BotToken</strong> не указан. Пожалуйста, введите корректный BotToken и сохраните настройки. После этого здесь станет доступна настройка ChatID.
|
||||
</div>
|
||||
{% endif %}
|
||||
{% elseif item['type'] == 'tg_message_template' %}
|
||||
<div style="margin-bottom: 10px;">
|
||||
<textarea name="{{ settingKey }}"
|
||||
rows="{{ item['rows'] }}"
|
||||
placeholder="{{ item['placeholder'] }}"
|
||||
id="{{ settingKey }}"
|
||||
class="form-control"
|
||||
>{{ attribute(_context, settingKey) }}</textarea>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button class="btn btn-link" type="button" data-toggle="collapse" data-target="#{{ settingKey }}-collapse">
|
||||
Документация
|
||||
</button>
|
||||
<button id="{{ settingKey }}-btn-test" type="button" class="btn btn-primary btn-sm">
|
||||
<i class="fa fa-envelope"></i>
|
||||
Отправить тестовое уведомление
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="collapse" id="{{ settingKey }}-collapse" style="margin-top: 15px">
|
||||
<div class="well">
|
||||
<p>Вы можете использовать переменные:</p>
|
||||
<ul>
|
||||
<li><code>{store_name}</code> — название магазина</li>
|
||||
<li><code>{order_id}</code> — номер заказа</li>
|
||||
<li><code>{customer}</code> — имя и фамилия покупателя</li>
|
||||
<li><code>{email}</code> — email покупателя</li>
|
||||
<li><code>{phone}</code> — телефон</li>
|
||||
<li><code>{comment}</code> — комментарий к заказу</li>
|
||||
<li><code>{address}</code> — адрес доставки</li>
|
||||
<li><code>{total}</code> — сумма заказа</li>
|
||||
<li><code>{ip}</code> — IP покупателя</li>
|
||||
<li><code>{created_at}</code> — дата и время создания заказа</li>
|
||||
</ul>
|
||||
<p>Форматирование: поддерживается <a href="https://core.telegram.org/bots/api#markdownv2-style" target="_blank">*MarkdownV2* <i class="fa fa-external-link"></i></a>.</p>
|
||||
<p>Символы, которые нужно экранировать в тексте:</p>
|
||||
<pre>_ * [ ] ( ) ~ ` > # + - = | { } . !</pre>
|
||||
<p>Каждый из них нужно экранировать обратным слэшем \, если он не используется для форматирования. Например вместо <code>Заказ #123</code> нужно писать <code>Заказ \#123</code>.</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
$('#{{ settingKey }}-btn-test').click(function () {
|
||||
const telegramToken = $('#module_tgshop_bot_token').val().trim();
|
||||
if (! telegramToken) {
|
||||
alert('Сначала введите Telegram Bot Token!');
|
||||
return;
|
||||
}
|
||||
|
||||
const chatId = $('#module_tgshop_chat_id').val().trim();
|
||||
if (! chatId) {
|
||||
alert('Сначала введите Chat ID!');
|
||||
return;
|
||||
}
|
||||
|
||||
const template = $('#{{ settingKey }}').val().trim();
|
||||
if (! template) {
|
||||
alert('Сначала задайте шаблон!');
|
||||
return;
|
||||
}
|
||||
|
||||
fetch('/index.php?route=extension/tgshop/handle&api_action=testTgMessage', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
token: telegramToken,
|
||||
chat_id: chatId,
|
||||
template: template
|
||||
})
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(response => {
|
||||
alert(response.message || 'Уведомление успешно отправлено');
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
alert('Ошибка при отправке тестового сообщения');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
{% elseif item['type'] == 'text_readonly' %}
|
||||
<input type="text"
|
||||
readonly
|
||||
name="{{ settingKey }}"
|
||||
value="{{ attribute(_context, settingKey) }}"
|
||||
id="{{ settingKey }}"
|
||||
class="form-control"
|
||||
onfocus="this.select()"
|
||||
/>
|
||||
|
||||
{# BOT TOKEN #}
|
||||
{% elseif item['type'] == 'bot_token' %}
|
||||
<div class="input-group">
|
||||
<span class="input-group-btn">
|
||||
<button id="{{ settingKey }}-btn" class="btn btn-primary" type="button" onclick="validateBotToken()">
|
||||
<i class="fa fa-refresh"></i> Проверить Bot Token
|
||||
</button>
|
||||
</span>
|
||||
<input type="text"
|
||||
name="{{ settingKey }}"
|
||||
value="{{ attribute(_context, settingKey) }}"
|
||||
placeholder="{{ item['placeholder'] }}"
|
||||
id="{{ settingKey }}"
|
||||
class="form-control"
|
||||
onfocusout="validateBotToken()"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div id="{{ settingKey }}-result-label"></div>
|
||||
|
||||
<button class="btn btn-link btn-xs" type="button" data-toggle="collapse" data-target="#{{ settingKey }}-collapse" aria-expanded="false" aria-controls="collapseExample">
|
||||
Инструкция как создать Bot Token
|
||||
</button>
|
||||
<div class="collapse" id="{{ settingKey }}-collapse">
|
||||
<div class="well">
|
||||
<p>Подробная инструкция доступна в <a href="https://nikitakiselev.github.io/telecart-docs/#telegram" target="_blank">документации <i class="fa fa-external-link"></i></a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
function validateBotToken() {
|
||||
const $input = $('#{{ settingKey }}');
|
||||
const $resultLabel = $('#{{ settingKey }}-result-label');
|
||||
const botToken = $input.val();
|
||||
const url = '/admin/index.php?route=extension/module/tgshop/handle&api_action=configureBotToken&user_token={{ user_token }}';
|
||||
|
||||
if (botToken.trim().length === 0) {
|
||||
$resultLabel
|
||||
.text(`❌ Введите Bot Token!`)
|
||||
.css('color', 'red');
|
||||
return;
|
||||
}
|
||||
|
||||
$input.attr('readonly', true);
|
||||
$resultLabel.text('Проверяю...');
|
||||
|
||||
fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ botToken }),
|
||||
})
|
||||
.then(async (res) => {
|
||||
const response = await res.json().catch(() => null);
|
||||
|
||||
if (res.status === 422) {
|
||||
console.error(res, response);
|
||||
$resultLabel
|
||||
.text(`❌ Ошибка: ${response.error}`)
|
||||
.css('color', 'red');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`Ошибка ${response.error || res.statusText}`);
|
||||
}
|
||||
|
||||
if (! response.id) {
|
||||
throw new Error(`bot token is not found in server response.`);
|
||||
}
|
||||
|
||||
$resultLabel
|
||||
.text(`✅ Бот: @${response.username} (id: ${response.id}) webhook: ${response.webhook_url}`)
|
||||
.css('color', 'green');
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
$resultLabel
|
||||
.text(`❌ Ошибка проверки BotToken.`)
|
||||
.css('color', 'red');
|
||||
})
|
||||
.finally(() => $input.attr('readonly', false))
|
||||
}
|
||||
|
||||
</script>
|
||||
{% else %}
|
||||
Unsupported {{ item|json_encode }}
|
||||
{% endif %}
|
||||
|
||||
{% if attribute(_context, 'error_' ~ settingKey) %}
|
||||
<div class="text-danger">{{ attribute(_context, 'error_' ~ settingKey) }}</div>
|
||||
{% endif %}
|
||||
|
||||
{% if item['help'] %}
|
||||
<p class="help-block">{{ item['help'] }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<div id="banners" class="tab-pane">
|
||||
<script>
|
||||
window.TeleCart = {
|
||||
user_token: '{{ user_token }}',
|
||||
mainpage_slider: '{{ mainpage_slider }}',
|
||||
};
|
||||
</script>
|
||||
<div id="app">App Loading...</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<script>
|
||||
window.TeleCart = {
|
||||
user_token: '{{ user_token }}',
|
||||
mainpage_slider: '{{ mainpage_slider }}',
|
||||
themes: '{{ themes | json_encode }}',
|
||||
order_statuses: '{{ order_statuses | json_encode }}',
|
||||
};
|
||||
</script>
|
||||
<div id="app">App Loading...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -467,7 +42,7 @@
|
||||
|
||||
<script>
|
||||
const $element = $('#thumb-image-module_tgshop_app_icon');
|
||||
$('#button-clear').on('click', function() {
|
||||
$('#button-clear').on('click', function () {
|
||||
$element.find('img').attr('src', $element.find('img').attr('data-placeholder'));
|
||||
$element.parent().find('input').val('');
|
||||
$element.popover('destroy');
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
{{ header }}{{ column_left }}
|
||||
<div id="content">
|
||||
<div class="page-header">
|
||||
<div class="container-fluid">
|
||||
<div class="pull-right">
|
||||
<a href="{{ cancel }}" data-toggle="tooltip" title="{{ button_cancel }}" class="btn btn-default"><i
|
||||
class="fa fa-reply"></i></a></div>
|
||||
<h1>{{ heading_title }}</h1>
|
||||
<ul class="breadcrumb">
|
||||
{% for breadcrumb in breadcrumbs %}
|
||||
<li><a href="{{ breadcrumb.href }}">{{ breadcrumb.text }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container-fluid">
|
||||
{% if error_warning %}
|
||||
<div class="alert alert-danger alert-dismissible"><i
|
||||
class="fa fa-exclamation-circle"></i> {{ error_warning }}
|
||||
<button type="button" class="close" data-dismiss="alert">×</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title"><i class="fa fa-pencil"></i> Инициализация модуля</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="col-md-push-3 col-md-6 text-center">
|
||||
<div style="font-size: 16px;">
|
||||
<h2 style="margin-top: 50px; margin-bottom: 30px;">🛠 Добро пожаловать в модуль Telegram-магазина</h2>
|
||||
<p>Этот модуль разработан с вниманием к деталям и заботой о стабильной работе вашего магазина в Telegram. Я старался сделать его максимально простым, понятным и гибким.</p>
|
||||
<p>Если у вас возникнут вопросы, пожелания или нужны доработки — вы всегда можете обратиться:</p>
|
||||
<ul style="list-style: none">
|
||||
<li>📬 Email: <a href="mailto:kiselev2008@gmail.com">kiselev2008@gmail.com</a></li>
|
||||
<li>💬 Telegram-группа: <a href="https://t.me/ocstore3" target="_blank">https://t.me/ocstore3 <i class="fa fa-external-link"></i></a></li>
|
||||
</ul>
|
||||
|
||||
<p>Заходите в Telegram-группу, там я анонсирую свежие версии своих модулей.</p>
|
||||
|
||||
<div class="alert alert-info">
|
||||
<p>⚠️ Перед началом работы требуется инициализация модуля.</p>
|
||||
<p>Она создаст дефолтные настройки и подготовит систему к использованию.</p>
|
||||
<p>Нажмите кнопку ниже, чтобы выполнить первичную настройку. Всё выполнится автоматически.</p>
|
||||
</div>
|
||||
<form action="{{ action }}" method="post" enctype="multipart/form-data" class="form-horizontal">
|
||||
<button type="submit"
|
||||
data-toggle="tooltip"
|
||||
title="Нажмите чтобы выполнить начальную инициализацию модуля"
|
||||
class="btn btn-primary"
|
||||
>
|
||||
Инициализация
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ footer }}
|
||||
@@ -66,6 +66,7 @@ class ControllerExtensionTgshopHandle extends Controller
|
||||
'featured_categories' => (array) $this->config->get('module_tgshop_featured_categories'),
|
||||
'store_enabled' => filter_var($this->config->get('module_tgshop_enable_store'), FILTER_VALIDATE_BOOLEAN),
|
||||
'base_url' => HTTPS_SERVER,
|
||||
'ya_metrika_counter' => trim($this->config->get('module_tgshop_yandex_metrika')),
|
||||
'ya_metrika_enabled' => ! empty(trim($this->config->get('module_tgshop_yandex_metrika'))),
|
||||
'telegram' => [
|
||||
'bot_token' => $this->config->get('module_tgshop_bot_token'),
|
||||
@@ -141,7 +142,7 @@ class ControllerExtensionTgshopHandle extends Controller
|
||||
return '';
|
||||
}
|
||||
|
||||
private function safeJsonDecode(string $input, $default = null)
|
||||
private function safeJsonDecode(?string $input = null, $default = null)
|
||||
{
|
||||
try {
|
||||
return json_decode($input, true, 512, JSON_THROW_ON_ERROR);
|
||||
|
||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace Bastion;
|
||||
|
||||
use App\ServiceProviders\AppServiceProvider;
|
||||
use App\ServiceProviders\SettingsServiceProvider;
|
||||
use Openguru\OpenCartFramework\Application;
|
||||
use Openguru\OpenCartFramework\Cache\CacheServiceProvider;
|
||||
use Openguru\OpenCartFramework\QueryBuilder\QueryBuilderServiceProvider;
|
||||
@@ -14,14 +15,17 @@ use Openguru\OpenCartFramework\Telegram\TelegramServiceProvider;
|
||||
|
||||
class ApplicationFactory
|
||||
{
|
||||
public static function create(array $config): Application
|
||||
public static function create(array $settings): Application
|
||||
{
|
||||
$defaultConfig = require __DIR__ . '/config.php';
|
||||
$routes = require __DIR__ . '/routes.php';
|
||||
|
||||
return (new Application(Arr::mergeArraysRecursively($defaultConfig, $config)))
|
||||
->withRoutes(fn () => $routes)
|
||||
$merged = Arr::mergeArraysRecursively($defaultConfig, $settings);
|
||||
|
||||
return (new Application($merged))
|
||||
->withRoutes(fn() => $routes)
|
||||
->withServiceProviders([
|
||||
SettingsServiceProvider::class,
|
||||
QueryBuilderServiceProvider::class,
|
||||
RouteServiceProvider::class,
|
||||
AppServiceProvider::class,
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace Bastion\Handlers;
|
||||
|
||||
use Openguru\OpenCartFramework\Http\JsonResponse;
|
||||
use Openguru\OpenCartFramework\Http\Request;
|
||||
use Openguru\OpenCartFramework\OpenCart\Decorators\OcRegistryDecorator;
|
||||
use Openguru\OpenCartFramework\Support\Str;
|
||||
|
||||
class AutocompleteHandler
|
||||
{
|
||||
private OcRegistryDecorator $registry;
|
||||
|
||||
public function __construct(OcRegistryDecorator $registry)
|
||||
{
|
||||
$this->registry = $registry;
|
||||
}
|
||||
|
||||
public function getProductsById(Request $request): JsonResponse
|
||||
{
|
||||
$productIds = $request->json('product_ids', []);
|
||||
$products = [];
|
||||
|
||||
if ($productIds) {
|
||||
$products = array_map(function ($productId) {
|
||||
$item = [
|
||||
'id' => (int) $productId,
|
||||
];
|
||||
$product = $this->registry->model_catalog_product->getProduct($productId);
|
||||
|
||||
$item['name'] = $product ? Str::htmlEntityEncode($product['name']) : 'No name';
|
||||
|
||||
return $item;
|
||||
}, $productIds);
|
||||
}
|
||||
|
||||
return new JsonResponse([
|
||||
'data' => $products,
|
||||
]);
|
||||
}
|
||||
|
||||
public function getCategoriesById(Request $request): JsonResponse
|
||||
{
|
||||
$ids = $request->json('category_ids', []);
|
||||
$items = [];
|
||||
|
||||
if ($ids) {
|
||||
$items = array_map(function ($id) {
|
||||
$item = [
|
||||
'id' => (int) $id,
|
||||
];
|
||||
$entity = $this->registry->model_catalog_category->getCategory($id);
|
||||
|
||||
$item['name'] = $entity ? Str::htmlEntityEncode($entity['name']) : 'No name';
|
||||
|
||||
return $item;
|
||||
}, $ids);
|
||||
}
|
||||
|
||||
return new JsonResponse([
|
||||
'data' => $items,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -4,24 +4,35 @@ namespace Bastion\Handlers;
|
||||
|
||||
use Bastion\Exceptions\BotTokenConfiguratorException;
|
||||
use Bastion\Services\BotTokenConfigurator;
|
||||
use Bastion\Services\SettingsService;
|
||||
use Exception;
|
||||
use Openguru\OpenCartFramework\Config\Settings;
|
||||
use Openguru\OpenCartFramework\Http\JsonResponse;
|
||||
use Openguru\OpenCartFramework\Http\Request;
|
||||
use Openguru\OpenCartFramework\Http\Response;
|
||||
use Openguru\OpenCartFramework\Support\Arr;
|
||||
|
||||
class SettingsHandler
|
||||
{
|
||||
private BotTokenConfigurator $botTokenConfigurator;
|
||||
private Settings $settings;
|
||||
private SettingsService $settingsUpdateService;
|
||||
|
||||
public function __construct(BotTokenConfigurator $botTokenConfigurator)
|
||||
{
|
||||
public function __construct(
|
||||
BotTokenConfigurator $botTokenConfigurator,
|
||||
Settings $settings,
|
||||
SettingsService $settingsUpdateService
|
||||
) {
|
||||
$this->botTokenConfigurator = $botTokenConfigurator;
|
||||
$this->settings = $settings;
|
||||
$this->settingsUpdateService = $settingsUpdateService;
|
||||
}
|
||||
|
||||
public function configureBotToken(Request $request): JsonResponse
|
||||
{
|
||||
try {
|
||||
$data = $this->botTokenConfigurator->configure(trim($request->json('botToken', '')));
|
||||
|
||||
return new JsonResponse($data);
|
||||
} catch (BotTokenConfiguratorException $e) {
|
||||
return new JsonResponse(['error' => $e->getMessage()], Response::HTTP_UNPROCESSABLE_ENTITY);
|
||||
@@ -29,4 +40,34 @@ class SettingsHandler
|
||||
return new JsonResponse(['error' => $e->getMessage()], Response::HTTP_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
public function getSettingsForm(): JsonResponse
|
||||
{
|
||||
$data = Arr::getWithKeys($this->settings->getAll(), [
|
||||
'app',
|
||||
'telegram',
|
||||
'metrics',
|
||||
'store',
|
||||
'orders',
|
||||
'texts',
|
||||
'sliders',
|
||||
]);
|
||||
|
||||
return new JsonResponse(compact('data'));
|
||||
}
|
||||
|
||||
public function saveSettingsForm(Request $request): JsonResponse
|
||||
{
|
||||
$this->validate($request->json());
|
||||
|
||||
$this->settingsUpdateService->update(
|
||||
$request->json(),
|
||||
);
|
||||
|
||||
return new JsonResponse([], Response::HTTP_ACCEPTED);
|
||||
}
|
||||
|
||||
private function validate(array $input): void
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace Bastion\Handlers;
|
||||
|
||||
use Openguru\OpenCartFramework\Http\JsonResponse;
|
||||
use Openguru\OpenCartFramework\QueryBuilder\Builder;
|
||||
use Openguru\OpenCartFramework\QueryBuilder\JoinClause;
|
||||
use Openguru\OpenCartFramework\QueryBuilder\RawExpression;
|
||||
use Openguru\OpenCartFramework\QueryBuilder\Table;
|
||||
|
||||
class StatsHandler
|
||||
{
|
||||
private Builder $builder;
|
||||
|
||||
public function __construct(Builder $builder)
|
||||
{
|
||||
$this->builder = $builder;
|
||||
}
|
||||
|
||||
public function getDashboardStats(): JsonResponse
|
||||
{
|
||||
$ordersTotalAmount = $this->builder->newQuery()
|
||||
->select([
|
||||
new RawExpression('COUNT(DISTINCT orders.order_id) AS orders_count'),
|
||||
new RawExpression('SUM(orders.total) AS orders_total_amount'),
|
||||
new RawExpression('COUNT(DISTINCT order_product.product_id) AS order_products_count'),
|
||||
])
|
||||
->from(db_table('order'), 'orders')
|
||||
->join(new Table(db_table('order_history'), 'order_history'), function (JoinClause $join) {
|
||||
$join->on('orders.order_id', '=', 'order_history.order_id')
|
||||
->where('order_history.comment', '=', 'Заказ оформлен через Telegram Mini App');
|
||||
})
|
||||
->join(new Table(db_table('order_product'), 'order_product'), function (JoinClause $join) {
|
||||
$join->on('orders.order_id', '=', 'order_product.order_id');
|
||||
})
|
||||
->firstOrNull();
|
||||
|
||||
if ($ordersTotalAmount) {
|
||||
$data = [
|
||||
'orders_count' => (int) $ordersTotalAmount['orders_count'],
|
||||
'orders_total_amount' => (int) $ordersTotalAmount['orders_total_amount'],
|
||||
'order_products_count' => (int) $ordersTotalAmount['order_products_count'],
|
||||
];
|
||||
}
|
||||
|
||||
return new JsonResponse(compact('data'));
|
||||
}
|
||||
}
|
||||
@@ -2,18 +2,29 @@
|
||||
|
||||
namespace Bastion\Handlers;
|
||||
|
||||
use App\Services\SettingsService;
|
||||
use Exception;
|
||||
use GuzzleHttp\Exception\ClientException;
|
||||
use GuzzleHttp\Exception\GuzzleException;
|
||||
use Openguru\OpenCartFramework\Cache\CacheInterface;
|
||||
use Openguru\OpenCartFramework\Http\JsonResponse;
|
||||
use Openguru\OpenCartFramework\Http\Request;
|
||||
use Openguru\OpenCartFramework\Http\Response;
|
||||
use Openguru\OpenCartFramework\Support\Arr;
|
||||
use Openguru\OpenCartFramework\Telegram\Exceptions\TelegramClientException;
|
||||
use Openguru\OpenCartFramework\Telegram\TelegramService;
|
||||
|
||||
class TelegramHandler
|
||||
{
|
||||
private CacheInterface $cache;
|
||||
private TelegramService $telegramService;
|
||||
private SettingsService $settings;
|
||||
|
||||
public function __construct(CacheInterface $cache)
|
||||
public function __construct(CacheInterface $cache, TelegramService $telegramService, SettingsService $settings)
|
||||
{
|
||||
$this->cache = $cache;
|
||||
$this->telegramService = $telegramService;
|
||||
$this->settings = $settings;
|
||||
}
|
||||
|
||||
public function getChatId(): JsonResponse
|
||||
@@ -46,4 +57,79 @@ class TelegramHandler
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function testTgMessage(Request $request): JsonResponse
|
||||
{
|
||||
$template = $request->json('template', 'Нет шаблона');
|
||||
$token = $request->json('token');
|
||||
$chatId = $request->json('chat_id');
|
||||
|
||||
if (! $token) {
|
||||
return new JsonResponse([
|
||||
'message' => 'Не задан Telegram BotToken',
|
||||
]);
|
||||
}
|
||||
|
||||
if (! $chatId) {
|
||||
return new JsonResponse([
|
||||
'message' => 'Не задан ChatID.',
|
||||
]);
|
||||
}
|
||||
|
||||
$variables = [
|
||||
'{store_name}' => $this->settings->config()->getApp()->getAppName(),
|
||||
'{order_id}' => 777,
|
||||
'{customer}' => 'Иван Васильевич',
|
||||
'{email}' => 'telegram@opencart.com',
|
||||
'{phone}' => '+79999999999',
|
||||
'{comment}' => 'Это тестовый заказ',
|
||||
'{address}' => 'г. Москва',
|
||||
'{total}' => 100000,
|
||||
'{ip}' => '127.0.0.1',
|
||||
'{created_at}' => date('Y-m-d H:i:s'),
|
||||
];
|
||||
|
||||
$message = $this->telegramService->prepareMessage($template, $variables);
|
||||
|
||||
try {
|
||||
$this->telegramService
|
||||
->setBotToken($token)
|
||||
->sendMessage($chatId, $message);
|
||||
|
||||
return new JsonResponse([
|
||||
'message' => 'Сообщение отправлено. Проверьте Telegram.',
|
||||
]);
|
||||
} catch (ClientException $exception) {
|
||||
$json = json_decode($exception->getResponse()->getBody(), true);
|
||||
|
||||
return new JsonResponse([
|
||||
'message' => $json['description'],
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
return new JsonResponse([
|
||||
'message' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws GuzzleException
|
||||
* @throws TelegramClientException
|
||||
* @throws \JsonException
|
||||
*/
|
||||
public function tgGetMe(): JsonResponse
|
||||
{
|
||||
if (! $this->settings->config()->getTelegram()->getBotToken()) {
|
||||
return new JsonResponse(['data' => null]);
|
||||
}
|
||||
|
||||
$data = $this->cache->get('tg_me_info');
|
||||
|
||||
if (! $data) {
|
||||
$data = $this->telegramService->exec('getMe');
|
||||
$this->cache->set('tg_me_info', $data, 60);
|
||||
}
|
||||
|
||||
return new JsonResponse(compact('data'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,10 +4,10 @@ declare(strict_types=1);
|
||||
|
||||
namespace Bastion\Services;
|
||||
|
||||
use App\Services\SettingsService;
|
||||
use Bastion\Exceptions\BotTokenConfiguratorException;
|
||||
use Exception;
|
||||
use GuzzleHttp\Exception\GuzzleException;
|
||||
use Openguru\OpenCartFramework\Config\Settings;
|
||||
use Openguru\OpenCartFramework\Logger\LoggerInterface;
|
||||
use Openguru\OpenCartFramework\Router\Router;
|
||||
use Openguru\OpenCartFramework\Support\Arr;
|
||||
@@ -17,13 +17,13 @@ use Openguru\OpenCartFramework\Telegram\TelegramService;
|
||||
class BotTokenConfigurator
|
||||
{
|
||||
private TelegramService $telegramService;
|
||||
private Settings $settings;
|
||||
private SettingsService $settings;
|
||||
private Router $router;
|
||||
private LoggerInterface $logger;
|
||||
|
||||
public function __construct(
|
||||
TelegramService $telegramService,
|
||||
Settings $settings,
|
||||
SettingsService $settings,
|
||||
Router $router,
|
||||
LoggerInterface $logger
|
||||
) {
|
||||
@@ -72,15 +72,18 @@ class BotTokenConfigurator
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BotTokenConfiguratorException
|
||||
*/
|
||||
private function getWebhookUrl(): string
|
||||
{
|
||||
$publicUrl = rtrim($this->settings->get('public_url'), '/');
|
||||
$publicUrl = rtrim($this->settings->config()->getApp()->getShopBaseUrl(), '/');
|
||||
|
||||
if (! $publicUrl) {
|
||||
throw new BotTokenConfiguratorException('Public URL is not set in configuration.');
|
||||
}
|
||||
|
||||
$webhook = $this->router->url($this->settings->get('tg_webhook_handler', 'webhook'));
|
||||
$webhook = $this->router->url('webhook');
|
||||
|
||||
return $publicUrl . $webhook;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace Bastion\Services;
|
||||
|
||||
use Openguru\OpenCartFramework\OpenCart\Decorators\OcRegistryDecorator;
|
||||
|
||||
class SettingsService
|
||||
{
|
||||
private OcRegistryDecorator $registry;
|
||||
|
||||
public function __construct(OcRegistryDecorator $registry)
|
||||
{
|
||||
$this->registry = $registry;
|
||||
}
|
||||
|
||||
public function update(array $data): void
|
||||
{
|
||||
$this->registry->model_setting_setting->editSetting('module_telecart', [
|
||||
'module_telecart_settings' => $data,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,84 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'config_timezone' => 'UTC',
|
||||
'lang' => 'en-gb',
|
||||
'language_id' => 1,
|
||||
'auth_user_id' => 0,
|
||||
'base_url' => '/',
|
||||
|
||||
'db' => [
|
||||
'host' => 'localhost',
|
||||
'database' => 'not_set',
|
||||
'username' => 'not_set',
|
||||
'password' => 'not_set',
|
||||
'app' => [
|
||||
'app_enabled' => true,
|
||||
'app_name' => 'Telecart',
|
||||
'app_icon' => null,
|
||||
"theme_light" => "light",
|
||||
"theme_dark" => "dark",
|
||||
"app_debug" => false
|
||||
],
|
||||
|
||||
'logs' => [
|
||||
'path' => 'not_set',
|
||||
'telegram' => [
|
||||
"bot_token" => "",
|
||||
"chat_id" => null,
|
||||
"owner_notification_template" => <<<TEXT
|
||||
*Новый заказ \#{order_id}* в магазине *{store_name}*
|
||||
|
||||
*Покупатель:* {customer}
|
||||
*Email:* {email}
|
||||
*Телефон:* {phone}
|
||||
*IP:* {ip}
|
||||
|
||||
*Адрес доставки:*
|
||||
{address}
|
||||
|
||||
*Комментарий:*
|
||||
{comment}
|
||||
|
||||
*Сумма заказа:* {total}
|
||||
*Дата оформления:* {created_at}
|
||||
TEXT,
|
||||
"customer_notification_template" => <<<TEXT
|
||||
Спасибо за Ваш заказ в магазине *{store_name}*
|
||||
|
||||
*Номер заказа* \#{order_id}
|
||||
*Сумма заказа:* {total}
|
||||
*Дата оформления:* {created_at}
|
||||
|
||||
Мы свяжемся с вами при необходимости\.
|
||||
Хорошего дня\!
|
||||
TEXT,
|
||||
"mini_app_url" => "",
|
||||
],
|
||||
|
||||
"metrics" => [
|
||||
"yandex_metrika_enabled" => false,
|
||||
"yandex_metrika_counter" => "",
|
||||
],
|
||||
|
||||
'store' => [
|
||||
'enable_store' => true,
|
||||
'mainpage_products' => 'most_viewed',
|
||||
'featured_products' => [],
|
||||
'mainpage_categories' => 'latest10',
|
||||
'featured_categories' => [],
|
||||
'feature_coupons' => true,
|
||||
'feature_vouchers' => true,
|
||||
],
|
||||
|
||||
'texts' => [
|
||||
'text_no_more_products' => 'Это всё по текущему запросу. Попробуйте уточнить фильтры или поиск.',
|
||||
'text_empty_cart' => 'Ваша корзина пуста.',
|
||||
'text_order_created_success' => 'Ваш заказ успешно оформлен и будет обработан в ближайшее время.'
|
||||
],
|
||||
|
||||
'orders' => [
|
||||
'order_default_status_id' => 1,
|
||||
],
|
||||
|
||||
'sliders' => [
|
||||
'mainpage_slider' => [
|
||||
'is_enabled' => false,
|
||||
'effect' => 'slide',
|
||||
'pagination' => true,
|
||||
'scrollbar' => false,
|
||||
'free_mode' => false,
|
||||
'space_between' => 30,
|
||||
'autoplay' => false,
|
||||
'loop' => false,
|
||||
'slides' => [],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
<?php
|
||||
|
||||
use Bastion\Handlers\AutocompleteHandler;
|
||||
use Bastion\Handlers\SettingsHandler;
|
||||
use Bastion\Handlers\StatsHandler;
|
||||
use Bastion\Handlers\TelegramHandler;
|
||||
|
||||
return [
|
||||
'configureBotToken' => [SettingsHandler::class, 'configureBotToken'],
|
||||
'getChatId' => [TelegramHandler::class, 'getChatId'],
|
||||
'getSettingsForm' => [SettingsHandler::class, 'getSettingsForm'],
|
||||
'saveSettingsForm' => [SettingsHandler::class, 'saveSettingsForm'],
|
||||
'testTgMessage' => [TelegramHandler::class, 'testTgMessage'],
|
||||
'getProductsById' => [AutocompleteHandler::class, 'getProductsById'],
|
||||
'getCategoriesById' => [AutocompleteHandler::class, 'getCategoriesById'],
|
||||
'getDashboardStats' => [StatsHandler::class, 'getDashboardStats'],
|
||||
'tgGetMe' => [TelegramHandler::class, 'tgGetMe'],
|
||||
];
|
||||
|
||||
@@ -56,7 +56,7 @@ class Application extends Container
|
||||
return $container;
|
||||
});
|
||||
|
||||
$this->singleton(LoggerInterface::class, fn () => $this->logger);
|
||||
$this->singleton(LoggerInterface::class, fn() => $this->logger);
|
||||
|
||||
$this->singleton(Settings::class, function (Container $container) {
|
||||
return new Settings($container->getConfigValue());
|
||||
@@ -106,11 +106,11 @@ class Application extends Container
|
||||
|
||||
$this->profiler->addCheckpoint('Handle Middlewares.');
|
||||
|
||||
$next = fn ($req) => $this->call($controller, $method);
|
||||
$next = fn($req) => $this->call($controller, $method);
|
||||
|
||||
foreach (array_reverse($this->middlewareStack) as $class) {
|
||||
$instance = $this->get($class);
|
||||
$next = static fn ($req) => $instance->handle($req, $next);
|
||||
$next = static fn($req) => $instance->handle($req, $next);
|
||||
}
|
||||
|
||||
$response = $next($request);
|
||||
|
||||
@@ -4,47 +4,32 @@ namespace Openguru\OpenCartFramework\Config;
|
||||
|
||||
use Openguru\OpenCartFramework\Support\Arr;
|
||||
|
||||
class Settings
|
||||
class Settings implements SettingsInterface
|
||||
{
|
||||
private $settings;
|
||||
private array $config;
|
||||
|
||||
public function __construct(array $initialSettings = [])
|
||||
public function __construct(array $config)
|
||||
{
|
||||
$this->settings = $initialSettings;
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
public function get(string $key, $default = null)
|
||||
{
|
||||
return Arr::get($this->settings, $key, $default);
|
||||
}
|
||||
|
||||
public function set(string $key, $value): void
|
||||
{
|
||||
Arr::set($this->settings, $key, $value);
|
||||
return Arr::get($this->getAll(), $key, $default);
|
||||
}
|
||||
|
||||
public function has(string $key): bool
|
||||
{
|
||||
return Arr::get($this->settings, $key) !== null;
|
||||
}
|
||||
|
||||
public function remove(string $key): void
|
||||
{
|
||||
Arr::unset($this->settings, $key);
|
||||
return Arr::get($this->getAll(), $key) !== null;
|
||||
}
|
||||
|
||||
public function getAll(): array
|
||||
{
|
||||
return $this->settings;
|
||||
}
|
||||
|
||||
public function setAll(array $settings): void
|
||||
{
|
||||
$this->settings = $settings;
|
||||
return $this->config;
|
||||
}
|
||||
|
||||
public function getHash(): string
|
||||
{
|
||||
return md5(serialize($this->settings));
|
||||
return md5(serialize($this->getAll()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace Openguru\OpenCartFramework\Config;
|
||||
|
||||
interface SettingsInterface
|
||||
{
|
||||
public function get(string $key, $default = null);
|
||||
|
||||
public function has(string $key): bool;
|
||||
|
||||
public function getAll(): array;
|
||||
|
||||
public function getHash(): string;
|
||||
}
|
||||
@@ -14,7 +14,7 @@ use ReflectionMethod;
|
||||
use ReflectionNamedType;
|
||||
use RuntimeException;
|
||||
|
||||
if (!defined('BP_BASE_PATH')) {
|
||||
if (! defined('BP_BASE_PATH')) {
|
||||
$phar = Phar::running(false);
|
||||
define('BP_BASE_PATH', $phar ? "phar://$phar" : dirname(__DIR__) . '/..');
|
||||
}
|
||||
@@ -24,7 +24,7 @@ class Container implements ContainerInterface
|
||||
private array $factories = [];
|
||||
private array $instances = [];
|
||||
private array $config;
|
||||
private $taggedAbstracts = [];
|
||||
private array $taggedAbstracts = [];
|
||||
|
||||
public function __construct(array $config)
|
||||
{
|
||||
@@ -70,7 +70,7 @@ class Container implements ContainerInterface
|
||||
try {
|
||||
if ($this->has($id)) {
|
||||
if ($this->factories[$id]['singleton']) {
|
||||
if (!array_key_exists($id, $this->instances)) {
|
||||
if (! array_key_exists($id, $this->instances)) {
|
||||
$this->instances[$id] = $this->factories[$id]['concrete']($this);
|
||||
}
|
||||
|
||||
@@ -120,11 +120,11 @@ class Container implements ContainerInterface
|
||||
*/
|
||||
public function call(string $abstract, string $method): JsonResponse
|
||||
{
|
||||
if (!class_exists($abstract)) {
|
||||
if (! class_exists($abstract)) {
|
||||
throw new ContainerDependencyResolutionException('Could not resolve the concrete: ' . $abstract);
|
||||
}
|
||||
|
||||
if (!method_exists($abstract, $method)) {
|
||||
if (! method_exists($abstract, $method)) {
|
||||
throw new ContainerDependencyResolutionException('Method not found: ' . $abstract . '@' . $method);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace Openguru\OpenCartFramework;
|
||||
|
||||
use ErrorException;
|
||||
use Openguru\OpenCartFramework\Contracts\ExceptionHandlerInterface;
|
||||
use Openguru\OpenCartFramework\Exceptions\ActionNotFoundException;
|
||||
use Openguru\OpenCartFramework\Exceptions\NonLoggableExceptionInterface;
|
||||
use Openguru\OpenCartFramework\Http\JsonResponse;
|
||||
use Openguru\OpenCartFramework\Http\Response;
|
||||
@@ -58,6 +59,13 @@ class ErrorHandler
|
||||
$this->logger->logException($exception);
|
||||
}
|
||||
|
||||
if ($exception instanceof ActionNotFoundException) {
|
||||
(new JsonResponse([
|
||||
'message' => sprintf('Action %s is not found.', $exception->getAction()),
|
||||
], Response::HTTP_NOT_FOUND))->send();
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if (PHP_SAPI === 'cli') {
|
||||
echo $exception->getMessage() . PHP_EOL;
|
||||
} else {
|
||||
|
||||
@@ -6,4 +6,17 @@ use Exception;
|
||||
|
||||
class ActionNotFoundException extends Exception implements NonLoggableExceptionInterface
|
||||
{
|
||||
private string $action;
|
||||
|
||||
public function __construct(string $action, $message = "", $code = 0, Throwable $previous = null)
|
||||
{
|
||||
parent::__construct($message, $code, $previous);
|
||||
|
||||
$this->action = $action;
|
||||
}
|
||||
|
||||
public function getAction(): string
|
||||
{
|
||||
return $this->action;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,11 +14,11 @@ class QueryBuilderServiceProvider extends ServiceProvider
|
||||
public function register(): void
|
||||
{
|
||||
$this->container->bind(ConnectionInterface::class, function (Container $container) {
|
||||
$host = $container->getConfigValue('db.host');
|
||||
$username = $container->getConfigValue('db.username');
|
||||
$password = $container->getConfigValue('db.password');
|
||||
$port = $container->getConfigValue('db.port');
|
||||
$dbName = $container->getConfigValue('db.database');
|
||||
$host = $container->getConfigValue('database.host');
|
||||
$username = $container->getConfigValue('database.username');
|
||||
$password = $container->getConfigValue('database.password');
|
||||
$port = (int) $container->getConfigValue('database.port');
|
||||
$dbName = $container->getConfigValue('database.database');
|
||||
|
||||
$dsn = "mysql:host=$host;port=$port;dbname=$dbName";
|
||||
|
||||
@@ -34,4 +34,4 @@ class QueryBuilderServiceProvider extends ServiceProvider
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace Openguru\OpenCartFramework\Router;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use Openguru\OpenCartFramework\Exceptions\ActionNotFoundException;
|
||||
|
||||
class Router
|
||||
@@ -26,14 +27,14 @@ class Router
|
||||
public function resolve($action): array
|
||||
{
|
||||
if (! $action) {
|
||||
throw new ActionNotFoundException('No action provided');
|
||||
throw new InvalidArgumentException('No action provided');
|
||||
}
|
||||
|
||||
if (isset($this->routes[$action])) {
|
||||
return $this->routes[$action];
|
||||
}
|
||||
|
||||
throw new ActionNotFoundException('Action "' . $action . '" not found.');
|
||||
throw new ActionNotFoundException($action, 'Action "' . $action . '" not found.');
|
||||
}
|
||||
|
||||
public function url(string $action, array $query = []): string
|
||||
|
||||
@@ -18,11 +18,12 @@ class Arr
|
||||
{
|
||||
$result = [];
|
||||
foreach ($array as $item) {
|
||||
if (!array_key_exists($keyField, $item)) {
|
||||
if (! array_key_exists($keyField, $item)) {
|
||||
throw new InvalidArgumentException("Key field '{$keyField}' is missing in one of the items.");
|
||||
}
|
||||
$result[$item[$keyField]] = $item;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
@@ -45,9 +46,31 @@ class Arr
|
||||
return $result;
|
||||
}
|
||||
|
||||
public static function has(array $items, string $key): bool
|
||||
{
|
||||
if (empty($items)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (array_key_exists($key, $items)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$segments = explode('.', $key);
|
||||
foreach ($segments as $segment) {
|
||||
if (! is_array($items) || ! array_key_exists($segment, $items)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$items = $items[$segment];
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function get(array $items, string $key, $default = null)
|
||||
{
|
||||
if (!$items) {
|
||||
if (! $items) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
@@ -57,7 +80,7 @@ class Arr
|
||||
|
||||
$segments = explode('.', $key);
|
||||
foreach ($segments as $segment) {
|
||||
if (!is_array($items) || !array_key_exists($segment, $items)) {
|
||||
if (! is_array($items) || ! array_key_exists($segment, $items)) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
@@ -71,7 +94,7 @@ class Arr
|
||||
{
|
||||
$keys = explode('.', $key);
|
||||
foreach ($keys as $k) {
|
||||
if (!isset($array[$k]) || !is_array($array[$k])) {
|
||||
if (! isset($array[$k]) || ! is_array($array[$k])) {
|
||||
$array[$k] = [];
|
||||
}
|
||||
$array = &$array[$k];
|
||||
@@ -84,7 +107,7 @@ class Arr
|
||||
$keys = explode('.', $key);
|
||||
while (count($keys) > 1) {
|
||||
$k = array_shift($keys);
|
||||
if (!isset($array[$k]) || !is_array($array[$k])) {
|
||||
if (! isset($array[$k]) || ! is_array($array[$k])) {
|
||||
return;
|
||||
}
|
||||
$array = &$array[$k];
|
||||
@@ -119,13 +142,73 @@ class Arr
|
||||
|
||||
public static function mergeArraysRecursively(array $base, array $override): array
|
||||
{
|
||||
$result = $base;
|
||||
|
||||
foreach ($override as $key => $value) {
|
||||
if (isset($base[$key]) && is_array($base[$key]) && is_array($value)) {
|
||||
$base[$key] = static::mergeArraysRecursively($base[$key], $value);
|
||||
if (isset($result[$key]) && is_array($result[$key]) && is_array($value)) {
|
||||
$result[$key] = static::mergeArraysRecursively($result[$key], $value);
|
||||
} else {
|
||||
$base[$key] = $value;
|
||||
$result[$key] = $value;
|
||||
}
|
||||
}
|
||||
return $base;
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Объединяет массивы рекурсивно, где ключи override могут быть в dot notation.
|
||||
* Преобразует dot notation ключи в вложенную структуру перед объединением.
|
||||
*
|
||||
* @param array $base Базовый массив
|
||||
* @param array $override Массив с ключами в dot notation (например, ['app.logs.path' => '/var/log'])
|
||||
* @return array Объединенный массив
|
||||
*
|
||||
* @example
|
||||
* $base = ['app' => ['name' => 'MyApp']];
|
||||
* $override = ['app.logs.path' => '/var/log'];
|
||||
* // Результат: ['app' => ['name' => 'MyApp', 'logs' => ['path' => '/var/log']]]
|
||||
*/
|
||||
public static function mergeArraysRecursivelyWithDotNotation(array $base, array $override): array
|
||||
{
|
||||
$overrideNested = [];
|
||||
foreach ($override as $key => $value) {
|
||||
if (strpos($key, '.') !== false) {
|
||||
// Используем существующий метод set для создания вложенной структуры
|
||||
static::set($overrideNested, $key, $value);
|
||||
} else {
|
||||
// Обычный ключ без dot notation
|
||||
$overrideNested[$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
// Объединяем с базовым массивом
|
||||
return static::mergeArraysRecursively($base, $overrideNested);
|
||||
}
|
||||
|
||||
/**
|
||||
* Возвращает массив только с указанными ключами в dot notation
|
||||
* Сохраняет структуру вложенности из исходного массива
|
||||
*
|
||||
* @param array $array Исходный массив
|
||||
* @param array $keys Массив ключей в dot notation (например, ['app.name', 'app.logs.path', 'telegram.bot_token'])
|
||||
* @return array Отфильтрованный массив с сохранением структуры
|
||||
*
|
||||
* @example
|
||||
* $array = ['app' => ['name' => 'MyApp', 'logs' => ['path' => '/var/log']], 'telegram' => ['bot_token' => 'token']];
|
||||
* Arr::getWithKeys($array, ['app.name', 'app.logs.path', 'telegram.bot_token'])
|
||||
* // Вернет: ['app' => ['name' => 'MyApp', 'logs' => ['path' => '/var/log']], 'telegram' => ['bot_token' => 'token']]
|
||||
*/
|
||||
public static function getWithKeys(array $array, array $keys): array
|
||||
{
|
||||
$filtered = [];
|
||||
|
||||
foreach ($keys as $key) {
|
||||
if (static::has($array, $key)) {
|
||||
$value = static::get($array, $key);
|
||||
static::set($filtered, $key, $value);
|
||||
}
|
||||
}
|
||||
|
||||
return $filtered;
|
||||
}
|
||||
}
|
||||
|
||||
37
module/oc_telegram_shop/upload/oc_telegram_shop/framework/Support/Str.php
Executable file
37
module/oc_telegram_shop/upload/oc_telegram_shop/framework/Support/Str.php
Executable file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace Openguru\OpenCartFramework\Support;
|
||||
|
||||
class Str
|
||||
{
|
||||
/**
|
||||
* Determine if a given string starts with a given substring.
|
||||
*
|
||||
* @param string $haystack The string to search in.
|
||||
* @param string|array $needles The substring(s) to search for.
|
||||
* @return bool True if the string starts with any of the given substrings, false otherwise.
|
||||
*/
|
||||
public static function startsWith(string $haystack, $needles): bool
|
||||
{
|
||||
foreach ((array) $needles as $needle) {
|
||||
if ($needle === '') {
|
||||
if ($haystack === '') {
|
||||
return true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strncmp($haystack, $needle, strlen($needle)) === 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function htmlEntityEncode(string $string): string
|
||||
{
|
||||
return html_entity_decode($string, ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ use Openguru\OpenCartFramework\Support\Utils;
|
||||
if (! function_exists('table')) {
|
||||
function db_table(string $name): string
|
||||
{
|
||||
$prefix = Application::getInstance()->getConfigValue('db.prefix');
|
||||
$prefix = Application::getInstance()->getConfigValue('database.prefix');
|
||||
|
||||
return $prefix . $name;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace Openguru\OpenCartFramework\Telegram;
|
||||
|
||||
use Openguru\OpenCartFramework\Config\Settings;
|
||||
use App\Services\SettingsService;
|
||||
use Openguru\OpenCartFramework\Http\Request;
|
||||
use Openguru\OpenCartFramework\Logger\LoggerInterface;
|
||||
use Openguru\OpenCartFramework\Telegram\Exceptions\TelegramInvalidSignatureException;
|
||||
@@ -10,10 +10,10 @@ use Openguru\OpenCartFramework\Telegram\Exceptions\TelegramInvalidSignatureExcep
|
||||
class SignatureValidator
|
||||
{
|
||||
private ?string $botToken;
|
||||
private Settings $settings;
|
||||
private SettingsService $settings;
|
||||
private LoggerInterface $logger;
|
||||
|
||||
public function __construct(Settings $settings, LoggerInterface $logger, ?string $botToken = null)
|
||||
public function __construct(SettingsService $settings, LoggerInterface $logger, ?string $botToken = null)
|
||||
{
|
||||
$this->botToken = $botToken;
|
||||
$this->settings = $settings;
|
||||
@@ -22,8 +22,9 @@ class SignatureValidator
|
||||
|
||||
public function validate(Request $request): void
|
||||
{
|
||||
if ($this->settings->get('app_debug')) {
|
||||
if ($this->settings->config()->getApp()->isAppDebug()) {
|
||||
$this->logger->warning('Dev Mode is enabled. Ignoring Signature Validation.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -39,7 +40,7 @@ class SignatureValidator
|
||||
|
||||
$data = $this->parseInitDataStringToArray($initDataString);
|
||||
|
||||
if (!isset($data['hash'])) {
|
||||
if (! isset($data['hash'])) {
|
||||
throw new TelegramInvalidSignatureException('Missing hash in init data');
|
||||
}
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
namespace Openguru\OpenCartFramework\Telegram;
|
||||
|
||||
use App\Services\SettingsService;
|
||||
use Openguru\OpenCartFramework\Application;
|
||||
use Openguru\OpenCartFramework\Config\Settings;
|
||||
use Openguru\OpenCartFramework\Container\ServiceProvider;
|
||||
use Openguru\OpenCartFramework\Logger\LoggerInterface;
|
||||
|
||||
@@ -13,12 +13,13 @@ class TelegramServiceProvider extends ServiceProvider
|
||||
{
|
||||
$this->container->singleton(TelegramService::class, function (Application $app) {
|
||||
$botToken = $app->getConfigValue('telegram.bot_token');
|
||||
|
||||
return new TelegramService($botToken);
|
||||
});
|
||||
|
||||
$this->container->singleton(SignatureValidator::class, function (Application $app) {
|
||||
return new SignatureValidator(
|
||||
$app->get(Settings::class),
|
||||
$app->get(SettingsService::class),
|
||||
$app->get(LoggerInterface::class),
|
||||
$app->getConfigValue('telegram.bot_token'),
|
||||
);
|
||||
|
||||
@@ -10,7 +10,7 @@ class TranslatorServiceProvider extends ServiceProvider
|
||||
public function register(): void
|
||||
{
|
||||
$this->container->singleton(TranslatorInterface::class, function (Container $container) {
|
||||
$language = $container->getConfigValue('lang');
|
||||
$language = $container->getConfigValue('lang', 1);
|
||||
$translator = new Translator($language);
|
||||
$translator->loadTranslationsFromFolder(resources_path('/translations'));
|
||||
return $translator;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App;
|
||||
|
||||
use App\ServiceProviders\AppServiceProvider;
|
||||
use App\ServiceProviders\SettingsServiceProvider;
|
||||
use Openguru\OpenCartFramework\Application;
|
||||
use Openguru\OpenCartFramework\Cache\CacheServiceProvider;
|
||||
use Openguru\OpenCartFramework\QueryBuilder\QueryBuilderServiceProvider;
|
||||
@@ -20,8 +21,9 @@ class ApplicationFactory
|
||||
$routes = require __DIR__ . '/routes.php';
|
||||
|
||||
return (new Application(Arr::mergeArraysRecursively($defaultConfig, $config)))
|
||||
->withRoutes(fn () => $routes)
|
||||
->withRoutes(fn() => $routes)
|
||||
->withServiceProviders([
|
||||
SettingsServiceProvider::class,
|
||||
QueryBuilderServiceProvider::class,
|
||||
CacheServiceProvider::class,
|
||||
RouteServiceProvider::class,
|
||||
|
||||
89
module/oc_telegram_shop/upload/oc_telegram_shop/src/DTO/Settings/AppDTO.php
Executable file
89
module/oc_telegram_shop/upload/oc_telegram_shop/src/DTO/Settings/AppDTO.php
Executable file
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
namespace App\DTO\Settings;
|
||||
|
||||
final class AppDTO
|
||||
{
|
||||
private bool $appEnabled;
|
||||
private string $appName;
|
||||
private ?string $appIcon;
|
||||
private string $themeLight;
|
||||
private string $themeDark;
|
||||
private bool $appDebug;
|
||||
private int $languageId;
|
||||
private string $shopBaseUrl;
|
||||
|
||||
public function __construct(
|
||||
bool $appEnabled,
|
||||
string $appName,
|
||||
?string $appIcon,
|
||||
string $themeLight,
|
||||
string $themeDark,
|
||||
bool $appDebug,
|
||||
int $languageId,
|
||||
string $shopBaseUrl
|
||||
) {
|
||||
$this->appEnabled = $appEnabled;
|
||||
$this->appName = $appName;
|
||||
$this->appIcon = $appIcon;
|
||||
$this->themeLight = $themeLight;
|
||||
$this->themeDark = $themeDark;
|
||||
$this->appDebug = $appDebug;
|
||||
$this->languageId = $languageId;
|
||||
$this->shopBaseUrl = $shopBaseUrl;
|
||||
}
|
||||
|
||||
public function isAppEnabled(): bool
|
||||
{
|
||||
return $this->appEnabled;
|
||||
}
|
||||
|
||||
public function getAppName(): string
|
||||
{
|
||||
return $this->appName;
|
||||
}
|
||||
|
||||
public function getAppIcon(): ?string
|
||||
{
|
||||
return $this->appIcon;
|
||||
}
|
||||
|
||||
public function getThemeLight(): string
|
||||
{
|
||||
return $this->themeLight;
|
||||
}
|
||||
|
||||
public function getThemeDark(): string
|
||||
{
|
||||
return $this->themeDark;
|
||||
}
|
||||
|
||||
public function isAppDebug(): bool
|
||||
{
|
||||
return $this->appDebug;
|
||||
}
|
||||
|
||||
public function getLanguageId(): int
|
||||
{
|
||||
return $this->languageId;
|
||||
}
|
||||
|
||||
public function getShopBaseUrl(): string
|
||||
{
|
||||
return $this->shopBaseUrl;
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'app_enabled' => $this->isAppEnabled(),
|
||||
'app_name' => $this->getAppName(),
|
||||
'app_icon' => $this->getAppIcon(),
|
||||
'theme_light' => $this->getThemeLight(),
|
||||
'theme_dark' => $this->getThemeDark(),
|
||||
'app_debug' => $this->isAppDebug(),
|
||||
'language_id' => $this->getLanguageId(),
|
||||
'shop_base_url' => $this->getShopBaseUrl(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
namespace App\DTO\Settings;
|
||||
|
||||
final class ConfigDTO
|
||||
{
|
||||
private AppDTO $app;
|
||||
private TelegramDTO $telegram;
|
||||
private MetricsDTO $metrics;
|
||||
private StoreDTO $store;
|
||||
private OrdersDTO $orders;
|
||||
private TextsDTO $texts;
|
||||
private SlidersDTO $sliders;
|
||||
private DatabaseDTO $database;
|
||||
private LogsDTO $logs;
|
||||
|
||||
public function __construct(
|
||||
AppDTO $app,
|
||||
TelegramDTO $telegram,
|
||||
MetricsDTO $metrics,
|
||||
StoreDTO $store,
|
||||
OrdersDTO $orders,
|
||||
TextsDTO $texts,
|
||||
SlidersDTO $sliders,
|
||||
DatabaseDTO $database,
|
||||
LogsDTO $logs
|
||||
) {
|
||||
$this->app = $app;
|
||||
$this->telegram = $telegram;
|
||||
$this->metrics = $metrics;
|
||||
$this->store = $store;
|
||||
$this->orders = $orders;
|
||||
$this->texts = $texts;
|
||||
$this->sliders = $sliders;
|
||||
$this->database = $database;
|
||||
$this->logs = $logs;
|
||||
}
|
||||
|
||||
public function getApp(): AppDTO
|
||||
{
|
||||
return $this->app;
|
||||
}
|
||||
|
||||
public function getTelegram(): TelegramDTO
|
||||
{
|
||||
return $this->telegram;
|
||||
}
|
||||
|
||||
public function getMetrics(): MetricsDTO
|
||||
{
|
||||
return $this->metrics;
|
||||
}
|
||||
|
||||
public function getStore(): StoreDTO
|
||||
{
|
||||
return $this->store;
|
||||
}
|
||||
|
||||
public function getOrders(): OrdersDTO
|
||||
{
|
||||
return $this->orders;
|
||||
}
|
||||
|
||||
public function getTexts(): TextsDTO
|
||||
{
|
||||
return $this->texts;
|
||||
}
|
||||
|
||||
public function getSliders(): SlidersDTO
|
||||
{
|
||||
return $this->sliders;
|
||||
}
|
||||
|
||||
public function getDatabase(): DatabaseDTO
|
||||
{
|
||||
return $this->database;
|
||||
}
|
||||
|
||||
public function getLogs(): LogsDTO
|
||||
{
|
||||
return $this->logs;
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'app' => $this->app->toArray(),
|
||||
'database' => $this->database->toArray(),
|
||||
'logs' => $this->logs->toArray(),
|
||||
'metrics' => $this->metrics->toArray(),
|
||||
'orders' => $this->orders->toArray(),
|
||||
'sliders' => $this->sliders->toArray(),
|
||||
'store' => $this->store->toArray(),
|
||||
'telegram' => $this->telegram->toArray(),
|
||||
'texts' => $this->texts->toArray(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace App\DTO\Settings;
|
||||
|
||||
final class DatabaseDTO
|
||||
{
|
||||
private string $host;
|
||||
private string $database;
|
||||
private string $username;
|
||||
private string $password;
|
||||
private string $prefix;
|
||||
private int $port;
|
||||
|
||||
public function __construct(
|
||||
string $host,
|
||||
string $database,
|
||||
string $username,
|
||||
string $password,
|
||||
string $prefix,
|
||||
int $port
|
||||
) {
|
||||
$this->host = $host;
|
||||
$this->database = $database;
|
||||
$this->username = $username;
|
||||
$this->password = $password;
|
||||
$this->prefix = $prefix;
|
||||
$this->port = $port;
|
||||
}
|
||||
|
||||
public function getHost(): string
|
||||
{
|
||||
return $this->host;
|
||||
}
|
||||
|
||||
public function getDatabase(): string
|
||||
{
|
||||
return $this->database;
|
||||
}
|
||||
|
||||
public function getUsername(): string
|
||||
{
|
||||
return $this->username;
|
||||
}
|
||||
|
||||
public function getPassword(): string
|
||||
{
|
||||
return $this->password;
|
||||
}
|
||||
|
||||
public function getPrefix(): string
|
||||
{
|
||||
return $this->prefix;
|
||||
}
|
||||
|
||||
public function getPort(): int
|
||||
{
|
||||
return $this->port;
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'host' => $this->host,
|
||||
'database' => $this->database,
|
||||
'username' => $this->username,
|
||||
'password' => $this->password,
|
||||
'prefix' => $this->prefix,
|
||||
'port' => $this->port,
|
||||
];
|
||||
}
|
||||
}
|
||||
25
module/oc_telegram_shop/upload/oc_telegram_shop/src/DTO/Settings/LogsDTO.php
Executable file
25
module/oc_telegram_shop/upload/oc_telegram_shop/src/DTO/Settings/LogsDTO.php
Executable file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\DTO\Settings;
|
||||
|
||||
final class LogsDTO
|
||||
{
|
||||
private string $path;
|
||||
|
||||
public function __construct(string $path)
|
||||
{
|
||||
$this->path = $path;
|
||||
}
|
||||
|
||||
public function getPath(): string
|
||||
{
|
||||
return $this->path;
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'path' => $this->path,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\DTO\Settings\MainpageSlider;
|
||||
|
||||
final class LinkDTO
|
||||
{
|
||||
private string $type;
|
||||
private ?LinkValueDTO $value;
|
||||
|
||||
public function __construct(
|
||||
string $type,
|
||||
?LinkValueDTO $value
|
||||
) {
|
||||
$this->type = $type;
|
||||
$this->value = $value;
|
||||
}
|
||||
|
||||
public function getType(): string
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
public function getValue(): ?LinkValueDTO
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
$result = [
|
||||
'type' => $this->type,
|
||||
'value' => null,
|
||||
];
|
||||
|
||||
if ($this->value !== null) {
|
||||
$result['value'] = $this->value->toArray();
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\DTO\Settings\MainpageSlider;
|
||||
|
||||
final class LinkType
|
||||
{
|
||||
public const NONE = 'none';
|
||||
public const CATEGORY = 'category';
|
||||
public const PRODUCT = 'product';
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace App\DTO\Settings\MainpageSlider;
|
||||
|
||||
final class LinkValueDTO
|
||||
{
|
||||
private ?int $categoryId;
|
||||
private ?string $name;
|
||||
private ?int $productId;
|
||||
|
||||
public function __construct(
|
||||
?int $categoryId = null,
|
||||
?string $name = null,
|
||||
?int $productId = null
|
||||
) {
|
||||
$this->categoryId = $categoryId;
|
||||
$this->name = $name;
|
||||
$this->productId = $productId;
|
||||
}
|
||||
|
||||
public function getCategoryId(): ?int
|
||||
{
|
||||
return $this->categoryId;
|
||||
}
|
||||
|
||||
public function getName(): ?string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function getProductId(): ?int
|
||||
{
|
||||
return $this->productId;
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
$result = [];
|
||||
|
||||
if ($this->categoryId !== null) {
|
||||
$result['category_id'] = $this->categoryId;
|
||||
}
|
||||
|
||||
if ($this->name !== null) {
|
||||
$result['name'] = $this->name;
|
||||
}
|
||||
|
||||
if ($this->productId !== null) {
|
||||
$result['product_id'] = $this->productId;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
namespace App\DTO\Settings\MainpageSlider;
|
||||
|
||||
final class MainpageSliderDTO
|
||||
{
|
||||
private bool $isEnabled;
|
||||
private string $effect;
|
||||
private bool $pagination;
|
||||
private bool $scrollbar;
|
||||
private bool $freeMode;
|
||||
private int $spaceBetween;
|
||||
private bool $autoplay;
|
||||
private bool $loop;
|
||||
/** @var SlideDTO[] */
|
||||
private array $slides;
|
||||
|
||||
/**
|
||||
* @param SlideDTO[] $slides
|
||||
*/
|
||||
public function __construct(
|
||||
bool $isEnabled,
|
||||
string $effect,
|
||||
bool $pagination,
|
||||
bool $scrollbar,
|
||||
bool $freeMode,
|
||||
int $spaceBetween,
|
||||
bool $autoplay,
|
||||
bool $loop,
|
||||
array $slides
|
||||
) {
|
||||
$this->isEnabled = $isEnabled;
|
||||
$this->effect = $effect;
|
||||
$this->pagination = $pagination;
|
||||
$this->scrollbar = $scrollbar;
|
||||
$this->freeMode = $freeMode;
|
||||
$this->spaceBetween = $spaceBetween;
|
||||
$this->autoplay = $autoplay;
|
||||
$this->loop = $loop;
|
||||
$this->slides = $slides;
|
||||
}
|
||||
|
||||
public function isEnabled(): bool
|
||||
{
|
||||
return $this->isEnabled;
|
||||
}
|
||||
|
||||
public function getEffect(): string
|
||||
{
|
||||
return $this->effect;
|
||||
}
|
||||
|
||||
public function isPagination(): bool
|
||||
{
|
||||
return $this->pagination;
|
||||
}
|
||||
|
||||
public function isScrollbar(): bool
|
||||
{
|
||||
return $this->scrollbar;
|
||||
}
|
||||
|
||||
public function isFreeMode(): bool
|
||||
{
|
||||
return $this->freeMode;
|
||||
}
|
||||
|
||||
public function getSpaceBetween(): int
|
||||
{
|
||||
return $this->spaceBetween;
|
||||
}
|
||||
|
||||
public function isAutoplay(): bool
|
||||
{
|
||||
return $this->autoplay;
|
||||
}
|
||||
|
||||
public function isLoop(): bool
|
||||
{
|
||||
return $this->loop;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return SlideDTO[]
|
||||
*/
|
||||
public function getSlides(): array
|
||||
{
|
||||
return $this->slides;
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
$slides = [];
|
||||
foreach ($this->slides as $slide) {
|
||||
$slides[] = $slide->toArray();
|
||||
}
|
||||
|
||||
return [
|
||||
'is_enabled' => $this->isEnabled,
|
||||
'effect' => $this->effect,
|
||||
'pagination' => $this->pagination,
|
||||
'scrollbar' => $this->scrollbar,
|
||||
'free_mode' => $this->freeMode,
|
||||
'space_between' => $this->spaceBetween,
|
||||
'autoplay' => $this->autoplay,
|
||||
'loop' => $this->loop,
|
||||
'slides' => $slides,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace App\DTO\Settings\MainpageSlider;
|
||||
|
||||
final class SlideDTO
|
||||
{
|
||||
private string $title;
|
||||
private LinkDTO $link;
|
||||
private string $image;
|
||||
|
||||
public function __construct(
|
||||
string $title,
|
||||
LinkDTO $link,
|
||||
string $image
|
||||
) {
|
||||
$this->title = $title;
|
||||
$this->link = $link;
|
||||
$this->image = $image;
|
||||
}
|
||||
|
||||
public function getTitle(): string
|
||||
{
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
public function getLink(): LinkDTO
|
||||
{
|
||||
return $this->link;
|
||||
}
|
||||
|
||||
public function getImage(): string
|
||||
{
|
||||
return $this->image;
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'title' => $this->title,
|
||||
'link' => $this->link->toArray(),
|
||||
'image' => $this->image,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\DTO\Settings;
|
||||
|
||||
final class MetricsDTO
|
||||
{
|
||||
private bool $yandexMetrikaEnabled;
|
||||
private string $yandexMetrikaCounter;
|
||||
|
||||
public function __construct(
|
||||
bool $yandexMetrikaEnabled,
|
||||
string $yandexMetrikaCounter
|
||||
) {
|
||||
$this->yandexMetrikaEnabled = $yandexMetrikaEnabled;
|
||||
$this->yandexMetrikaCounter = $yandexMetrikaCounter;
|
||||
}
|
||||
|
||||
public function isYandexMetrikaEnabled(): bool
|
||||
{
|
||||
return $this->yandexMetrikaEnabled;
|
||||
}
|
||||
|
||||
public function getYandexMetrikaCounter(): string
|
||||
{
|
||||
return $this->yandexMetrikaCounter;
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'yandex_metrika_enabled' => $this->yandexMetrikaEnabled,
|
||||
'yandex_metrika_counter' => $this->yandexMetrikaCounter,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\DTO\Settings;
|
||||
|
||||
final class OrdersDTO
|
||||
{
|
||||
private int $orderDefaultStatusId;
|
||||
private int $ocCustomerGroupId;
|
||||
|
||||
public function __construct(int $orderDefaultStatusId, int $ocCustomerGroupId)
|
||||
{
|
||||
$this->orderDefaultStatusId = $orderDefaultStatusId;
|
||||
$this->ocCustomerGroupId = $ocCustomerGroupId;
|
||||
}
|
||||
|
||||
public function getOrderDefaultStatusId(): int
|
||||
{
|
||||
return $this->orderDefaultStatusId;
|
||||
}
|
||||
|
||||
public function getOcCustomerGroupId(): int
|
||||
{
|
||||
return $this->ocCustomerGroupId;
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'order_default_status_id' => $this->orderDefaultStatusId,
|
||||
'oc_customer_group_id' => $this->ocCustomerGroupId,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\DTO\Settings;
|
||||
|
||||
use App\DTO\Settings\MainpageSlider\MainpageSliderDTO;
|
||||
|
||||
final class SlidersDTO
|
||||
{
|
||||
private MainpageSliderDTO $mainpageSlider;
|
||||
|
||||
public function __construct(MainpageSliderDTO $mainpageSlider)
|
||||
{
|
||||
$this->mainpageSlider = $mainpageSlider;
|
||||
}
|
||||
|
||||
public function getMainpageSlider(): MainpageSliderDTO
|
||||
{
|
||||
return $this->mainpageSlider;
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'mainpage_slider' => $this->mainpageSlider->toArray(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
120
module/oc_telegram_shop/upload/oc_telegram_shop/src/DTO/Settings/StoreDTO.php
Executable file
120
module/oc_telegram_shop/upload/oc_telegram_shop/src/DTO/Settings/StoreDTO.php
Executable file
@@ -0,0 +1,120 @@
|
||||
<?php
|
||||
|
||||
namespace App\DTO\Settings;
|
||||
|
||||
final class StoreDTO
|
||||
{
|
||||
private bool $enableStore;
|
||||
private string $mainpageProducts;
|
||||
/** @var int[] */
|
||||
private array $featuredProducts;
|
||||
private string $mainpageCategories;
|
||||
/** @var int[] */
|
||||
private array $featuredCategories;
|
||||
private bool $featureCoupons;
|
||||
private bool $featureVouchers;
|
||||
private string $ocDefaultCurrency;
|
||||
private bool $ocConfigTax;
|
||||
private int $ocStoreId;
|
||||
|
||||
/**
|
||||
* @param int[] $featuredProducts
|
||||
* @param int[] $featuredCategories
|
||||
*/
|
||||
public function __construct(
|
||||
bool $enableStore,
|
||||
string $mainpageProducts,
|
||||
array $featuredProducts,
|
||||
string $mainpageCategories,
|
||||
array $featuredCategories,
|
||||
bool $featureCoupons,
|
||||
bool $featureVouchers,
|
||||
string $ocDefaultCurrency,
|
||||
bool $ocConfigTax,
|
||||
int $ocStoreId
|
||||
) {
|
||||
$this->enableStore = $enableStore;
|
||||
$this->mainpageProducts = $mainpageProducts;
|
||||
$this->featuredProducts = $featuredProducts;
|
||||
$this->mainpageCategories = $mainpageCategories;
|
||||
$this->featuredCategories = $featuredCategories;
|
||||
$this->featureCoupons = $featureCoupons;
|
||||
$this->featureVouchers = $featureVouchers;
|
||||
$this->ocDefaultCurrency = $ocDefaultCurrency;
|
||||
$this->ocConfigTax = $ocConfigTax;
|
||||
$this->ocStoreId = $ocStoreId;
|
||||
}
|
||||
|
||||
public function isEnableStore(): bool
|
||||
{
|
||||
return $this->enableStore;
|
||||
}
|
||||
|
||||
public function getMainpageProducts(): string
|
||||
{
|
||||
return $this->mainpageProducts;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int[]
|
||||
*/
|
||||
public function getFeaturedProducts(): array
|
||||
{
|
||||
return $this->featuredProducts;
|
||||
}
|
||||
|
||||
public function getMainpageCategories(): string
|
||||
{
|
||||
return $this->mainpageCategories;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int[]
|
||||
*/
|
||||
public function getFeaturedCategories(): array
|
||||
{
|
||||
return $this->featuredCategories;
|
||||
}
|
||||
|
||||
public function isFeatureCoupons(): bool
|
||||
{
|
||||
return $this->featureCoupons;
|
||||
}
|
||||
|
||||
public function isFeatureVouchers(): bool
|
||||
{
|
||||
return $this->featureVouchers;
|
||||
}
|
||||
|
||||
public function getOcDefaultCurrency(): string
|
||||
{
|
||||
return $this->ocDefaultCurrency;
|
||||
}
|
||||
|
||||
public function isOcConfigTax(): bool
|
||||
{
|
||||
return $this->ocConfigTax;
|
||||
}
|
||||
|
||||
public function getOcStoreId(): int
|
||||
{
|
||||
return $this->ocStoreId;
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'enable_store' => $this->enableStore,
|
||||
'mainpage_products' => $this->mainpageProducts,
|
||||
'featured_products' => $this->featuredProducts,
|
||||
'mainpage_categories' => $this->mainpageCategories,
|
||||
'featured_categories' => $this->featuredCategories,
|
||||
'feature_coupons' => $this->featureCoupons,
|
||||
'feature_vouchers' => $this->featureVouchers,
|
||||
'oc_default_currency' => $this->ocDefaultCurrency,
|
||||
'oc_config_tax' => $this->ocConfigTax,
|
||||
'oc_store_id' => $this->ocStoreId,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace App\DTO\Settings;
|
||||
|
||||
final class TelegramDTO
|
||||
{
|
||||
private string $botToken;
|
||||
private ?int $chatId;
|
||||
private string $ownerNotificationTemplate;
|
||||
private string $customerNotificationTemplate;
|
||||
private string $miniAppUrl;
|
||||
|
||||
public function __construct(
|
||||
string $botToken,
|
||||
?int $chatId,
|
||||
string $ownerNotificationTemplate,
|
||||
string $customerNotificationTemplate,
|
||||
string $miniAppUrl
|
||||
) {
|
||||
$this->botToken = $botToken;
|
||||
$this->chatId = $chatId;
|
||||
$this->ownerNotificationTemplate = $ownerNotificationTemplate;
|
||||
$this->customerNotificationTemplate = $customerNotificationTemplate;
|
||||
$this->miniAppUrl = $miniAppUrl;
|
||||
}
|
||||
|
||||
public function getBotToken(): string
|
||||
{
|
||||
return $this->botToken;
|
||||
}
|
||||
|
||||
public function getChatId(): ?int
|
||||
{
|
||||
return $this->chatId;
|
||||
}
|
||||
|
||||
public function getOwnerNotificationTemplate(): string
|
||||
{
|
||||
return $this->ownerNotificationTemplate;
|
||||
}
|
||||
|
||||
public function getCustomerNotificationTemplate(): string
|
||||
{
|
||||
return $this->customerNotificationTemplate;
|
||||
}
|
||||
|
||||
public function getMiniAppUrl(): string
|
||||
{
|
||||
return $this->miniAppUrl;
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'bot_token' => $this->botToken,
|
||||
'chat_id' => $this->chatId,
|
||||
'owner_notification_template' => $this->ownerNotificationTemplate,
|
||||
'customer_notification_template' => $this->customerNotificationTemplate,
|
||||
'mini_app_url' => $this->miniAppUrl,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace App\DTO\Settings;
|
||||
|
||||
final class TextsDTO
|
||||
{
|
||||
private string $textNoMoreProducts;
|
||||
private string $textEmptyCart;
|
||||
private string $textOrderCreatedSuccess;
|
||||
|
||||
public function __construct(
|
||||
string $textNoMoreProducts,
|
||||
string $textEmptyCart,
|
||||
string $textOrderCreatedSuccess
|
||||
) {
|
||||
$this->textNoMoreProducts = $textNoMoreProducts;
|
||||
$this->textEmptyCart = $textEmptyCart;
|
||||
$this->textOrderCreatedSuccess = $textOrderCreatedSuccess;
|
||||
}
|
||||
|
||||
public function getTextNoMoreProducts(): string
|
||||
{
|
||||
return $this->textNoMoreProducts;
|
||||
}
|
||||
|
||||
public function getTextEmptyCart(): string
|
||||
{
|
||||
return $this->textEmptyCart;
|
||||
}
|
||||
|
||||
public function getTextOrderCreatedSuccess(): string
|
||||
{
|
||||
return $this->textOrderCreatedSuccess;
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'text_no_more_products' => $this->textNoMoreProducts,
|
||||
'text_empty_cart' => $this->textEmptyCart,
|
||||
'text_order_created_success' => $this->textOrderCreatedSuccess,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace App\Handlers;
|
||||
|
||||
use Openguru\OpenCartFramework\Config\Settings;
|
||||
use App\Services\SettingsService;
|
||||
use Openguru\OpenCartFramework\Http\JsonResponse;
|
||||
use Openguru\OpenCartFramework\ImageTool\ImageToolInterface;
|
||||
use Openguru\OpenCartFramework\OpenCart\Decorators\OcRegistryDecorator;
|
||||
@@ -11,9 +11,9 @@ class BannerHandler
|
||||
{
|
||||
private OcRegistryDecorator $registry;
|
||||
private ImageToolInterface $imageTool;
|
||||
private Settings $settings;
|
||||
private SettingsService $settings;
|
||||
|
||||
public function __construct(OcRegistryDecorator $registry, ImageToolInterface $imageTool, Settings $settings)
|
||||
public function __construct(OcRegistryDecorator $registry, ImageToolInterface $imageTool, SettingsService $settings)
|
||||
{
|
||||
$this->registry = $registry;
|
||||
$this->imageTool = $imageTool;
|
||||
@@ -24,23 +24,20 @@ class BannerHandler
|
||||
|
||||
public function show(): JsonResponse
|
||||
{
|
||||
$slider = $this->settings->get('mainpage_slider', []);
|
||||
$slider = $this->settings->config()->getSliders()->getMainpageSlider();
|
||||
$data = [];
|
||||
|
||||
if ($slider && ! empty($slider['slides']) && is_array($slider['slides'])) {
|
||||
foreach ($slider['slides'] as $index => $slide) {
|
||||
if (is_file(DIR_IMAGE . $slide['image'])) {
|
||||
$slider['slides'][$index] = [
|
||||
'id' => $index,
|
||||
'title' => $slide['title'],
|
||||
'link' => $slide['link'],
|
||||
'image' => $this->imageTool->cover($slide['image'], 1110, 600),
|
||||
];
|
||||
}
|
||||
foreach ($slider->getSlides() as $index => $slide) {
|
||||
if (is_file(DIR_IMAGE . $slide->getImage())) {
|
||||
$data['slides'][$index] = [
|
||||
'id' => $index,
|
||||
'title' => $slide->getTitle(),
|
||||
'link' => $slide->getLink(),
|
||||
'image' => $this->imageTool->cover($slide->getImage(), 1110, 600),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return new JsonResponse([
|
||||
'data' => $slider,
|
||||
]);
|
||||
return new JsonResponse(compact('data'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Handlers;
|
||||
|
||||
use App\Services\SettingsService;
|
||||
use App\Support\Utils;
|
||||
use Openguru\OpenCartFramework\Config\Settings;
|
||||
use Openguru\OpenCartFramework\Http\JsonResponse;
|
||||
use Openguru\OpenCartFramework\Http\Request;
|
||||
use Openguru\OpenCartFramework\ImageTool\ImageToolInterface;
|
||||
@@ -18,9 +18,9 @@ class CategoriesHandler
|
||||
|
||||
private Builder $queryBuilder;
|
||||
private ImageToolInterface $ocImageTool;
|
||||
private Settings $settings;
|
||||
private SettingsService $settings;
|
||||
|
||||
public function __construct(Builder $queryBuilder, ImageToolInterface $ocImageTool, Settings $settings)
|
||||
public function __construct(Builder $queryBuilder, ImageToolInterface $ocImageTool, SettingsService $settings)
|
||||
{
|
||||
$this->queryBuilder = $queryBuilder;
|
||||
$this->ocImageTool = $ocImageTool;
|
||||
@@ -29,12 +29,12 @@ class CategoriesHandler
|
||||
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$languageId = $this->settings->get('language_id');
|
||||
$languageId = $this->settings->config()->getApp()->getLanguageId();
|
||||
|
||||
$perPage = $request->get('perPage', 100);
|
||||
$forMainPage = filter_var($request->get('forMainPage', false), FILTER_VALIDATE_BOOLEAN);
|
||||
$featuredCategories = $this->settings->get('featured_categories');
|
||||
$mainpageCategories = $this->settings->get('mainpage_categories');
|
||||
$featuredCategories = $this->settings->config()->getStore()->getFeaturedCategories();
|
||||
$mainpageCategories = $this->settings->config()->getStore()->getMainpageCategories();
|
||||
|
||||
if ($forMainPage && $mainpageCategories === 'no_categories') {
|
||||
return new JsonResponse(['data' => []]);
|
||||
|
||||
@@ -5,8 +5,8 @@ declare(strict_types=1);
|
||||
namespace App\Handlers;
|
||||
|
||||
use App\Services\ProductsService;
|
||||
use App\Services\SettingsService;
|
||||
use Exception;
|
||||
use Openguru\OpenCartFramework\Config\Settings;
|
||||
use Openguru\OpenCartFramework\Exceptions\EntityNotFoundException;
|
||||
use Openguru\OpenCartFramework\Http\JsonResponse;
|
||||
use Openguru\OpenCartFramework\Http\Request;
|
||||
@@ -16,11 +16,11 @@ use RuntimeException;
|
||||
|
||||
class ProductsHandler
|
||||
{
|
||||
private Settings $settings;
|
||||
private SettingsService $settings;
|
||||
private ProductsService $productsService;
|
||||
private LoggerInterface $logger;
|
||||
|
||||
public function __construct(Settings $settings, ProductsService $productsService, LoggerInterface $logger)
|
||||
public function __construct(SettingsService $settings, ProductsService $productsService, LoggerInterface $logger)
|
||||
{
|
||||
$this->settings = $settings;
|
||||
$this->productsService = $productsService;
|
||||
@@ -33,7 +33,7 @@ class ProductsHandler
|
||||
$perPage = min((int) $request->json('perPage', 6), 15);
|
||||
$search = trim($request->get('search', ''));
|
||||
$filters = $request->json('filters');
|
||||
$languageId = $this->settings->get('language_id');
|
||||
$languageId = $this->settings->config()->getApp()->getLanguageId();
|
||||
|
||||
$response = $this->productsService->getProductsResponse(
|
||||
compact('page', 'perPage', 'search', 'filters'),
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
namespace App\Handlers;
|
||||
|
||||
use App\Services\SettingsService;
|
||||
use Exception;
|
||||
use GuzzleHttp\Exception\ClientException;
|
||||
use Openguru\OpenCartFramework\Config\Settings;
|
||||
use Openguru\OpenCartFramework\Http\JsonResponse;
|
||||
use Openguru\OpenCartFramework\Http\Request;
|
||||
use Openguru\OpenCartFramework\ImageTool\ImageToolInterface;
|
||||
@@ -13,13 +13,17 @@ use Openguru\OpenCartFramework\Telegram\TelegramService;
|
||||
|
||||
class SettingsHandler
|
||||
{
|
||||
private Settings $settings;
|
||||
private SettingsService $settings;
|
||||
private ImageToolInterface $imageTool;
|
||||
private Router $router;
|
||||
private TelegramService $telegramService;
|
||||
|
||||
public function __construct(Settings $settings, ImageToolInterface $imageTool, Router $router, TelegramService $telegramService)
|
||||
{
|
||||
public function __construct(
|
||||
SettingsService $settings,
|
||||
ImageToolInterface $imageTool,
|
||||
Router $router,
|
||||
TelegramService $telegramService
|
||||
) {
|
||||
$this->settings = $settings;
|
||||
$this->imageTool = $imageTool;
|
||||
$this->router = $router;
|
||||
@@ -28,49 +32,51 @@ class SettingsHandler
|
||||
|
||||
public function index(): JsonResponse
|
||||
{
|
||||
$appIcon = $this->settings->get('app_icon');
|
||||
$appConfig = $this->settings->config()->getApp();
|
||||
|
||||
$appIcon = $appConfig->getAppIcon();
|
||||
$hash = $this->settings->getHash();
|
||||
$icons = [];
|
||||
|
||||
if ($appIcon) {
|
||||
$icons['icon192'] = $this->imageTool->resize($appIcon, 192, 192, 'no_image.png', 'png'). '?_v=' . $hash;
|
||||
$icons['icon180'] = $this->imageTool->resize($appIcon, 180, 180, 'no_image.png', 'png'). '?_v=' . $hash;
|
||||
$icons['icon152'] = $this->imageTool->resize($appIcon, 152, 152, 'no_image.png', 'png'). '?_v=' . $hash;
|
||||
$icons['icon120'] = $this->imageTool->resize($appIcon, 120, 120, 'no_image.png', 'png'). '?_v=' . $hash;
|
||||
$appIcon = $this->imageTool->resize($appIcon, 32, 32, 'no_image.png', 'png'). '?_v=' . $hash;
|
||||
$icons['icon192'] = $this->imageTool->resize($appIcon, 192, 192, 'no_image.png', 'png') . '?_v=' . $hash;
|
||||
$icons['icon180'] = $this->imageTool->resize($appIcon, 180, 180, 'no_image.png', 'png') . '?_v=' . $hash;
|
||||
$icons['icon152'] = $this->imageTool->resize($appIcon, 152, 152, 'no_image.png', 'png') . '?_v=' . $hash;
|
||||
$icons['icon120'] = $this->imageTool->resize($appIcon, 120, 120, 'no_image.png', 'png') . '?_v=' . $hash;
|
||||
$appIcon = $this->imageTool->resize($appIcon, 32, 32, 'no_image.png', 'png') . '?_v=' . $hash;
|
||||
}
|
||||
|
||||
return new JsonResponse([
|
||||
'app_name' => $this->settings->get('app_name'),
|
||||
'app_debug' => $this->settings->get('app_debug'),
|
||||
'app_icon' => $appIcon ?? '',
|
||||
'app_name' => $appConfig->getAppName(),
|
||||
'app_debug' => $appConfig->isAppDebug(),
|
||||
'app_icon' => $appIcon,
|
||||
'app_icon192' => $icons['icon192'] ?? '',
|
||||
'app_icon180' => $icons['icon180'] ?? '',
|
||||
'app_icon152' => $icons['icon152'] ?? '',
|
||||
'app_icon120' => $icons['icon120'] ?? '',
|
||||
'manifest_url' => $this->router->url('manifest', ['_v' => $hash]),
|
||||
'theme_light' => $this->settings->get('theme_light'),
|
||||
'theme_dark' => $this->settings->get('theme_dark'),
|
||||
'ya_metrika_enabled' => $this->settings->get('ya_metrika_enabled'),
|
||||
'app_enabled' => $this->settings->get('app_enabled'),
|
||||
'store_enabled' => $this->settings->get('store_enabled'),
|
||||
'feature_coupons' => $this->settings->get('feature_coupons') ?? false,
|
||||
'feature_vouchers' => $this->settings->get('feature_vouchers') ?? false,
|
||||
'currency_code' => $this->settings->get('oc_default_currency', 'RUB'),
|
||||
'texts' => $this->settings->get('texts'),
|
||||
'mainpage_slider' => $this->settings->get('mainpage_slider'),
|
||||
'theme_light' => $appConfig->getThemeLight(),
|
||||
'theme_dark' => $appConfig->getThemeDark(),
|
||||
'ya_metrika_enabled' => $this->settings->config()->getMetrics()->isYandexMetrikaEnabled(),
|
||||
'app_enabled' => $appConfig->isAppEnabled(),
|
||||
'store_enabled' => $this->settings->config()->getStore()->isEnableStore(),
|
||||
'feature_coupons' => $this->settings->config()->getStore()->isFeatureCoupons(),
|
||||
'feature_vouchers' => $this->settings->config()->getStore()->isFeatureVouchers(),
|
||||
'currency_code' => $this->settings->config()->getStore()->getOcDefaultCurrency(),
|
||||
'texts' => $this->settings->config()->getTexts()->toArray(),
|
||||
'mainpage_slider' => $this->settings->config()->getSliders()->getMainpageSlider()->toArray(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function manifest(): JsonResponse
|
||||
{
|
||||
$appIcon = $this->settings->get('app_icon');
|
||||
$appIcon = $this->settings->config()->getApp()->getAppIcon();
|
||||
$icon192 = $this->imageTool->resize($appIcon, 192, 192, 'no_image.png', 'png');
|
||||
$icon512 = $this->imageTool->resize($appIcon, 512, 512, 'no_image.png', 'png');
|
||||
|
||||
return new JsonResponse([
|
||||
'name' => $this->settings->get('app_name'),
|
||||
'short_name' => $this->settings->get('app_name'),
|
||||
'name' => $this->settings->config()->getApp()->getAppName(),
|
||||
'short_name' => $this->settings->config()->getApp()->getAppName(),
|
||||
'start_url' => '/image/catalog/tgshopspa/',
|
||||
'display' => 'standalone',
|
||||
'background_color' => '#ffffff',
|
||||
@@ -110,7 +116,7 @@ class SettingsHandler
|
||||
}
|
||||
|
||||
$variables = [
|
||||
'{store_name}' => $this->settings->get('oc_store_name'),
|
||||
'{store_name}' => $this->settings->config()->getApp()->getAppName(),
|
||||
'{order_id}' => 777,
|
||||
'{customer}' => 'Иван Васильевич',
|
||||
'{email}' => 'telegram@opencart.com',
|
||||
|
||||
@@ -47,6 +47,8 @@ class TelegramHandler
|
||||
*/
|
||||
public function webhook(Request $request): JsonResponse
|
||||
{
|
||||
$this->logger->debug('Webhook received');
|
||||
|
||||
$update = $request->json();
|
||||
$message = $update['message'] ?? null;
|
||||
if (! $message) {
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\ServiceProviders;
|
||||
|
||||
use App\Services\SettingsSerializerService;
|
||||
use App\Services\SettingsService;
|
||||
use Openguru\OpenCartFramework\Container\Container;
|
||||
use Openguru\OpenCartFramework\Container\ServiceProvider;
|
||||
|
||||
class SettingsServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function register(): void
|
||||
{
|
||||
$this->container->singleton(SettingsService::class, function (Container $container) {
|
||||
return new SettingsService(
|
||||
$container->getConfigValue(),
|
||||
$container->get(SettingsSerializerService::class)
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -5,10 +5,8 @@ declare(strict_types=1);
|
||||
namespace App\Services;
|
||||
|
||||
use App\Exceptions\OrderValidationFailedException;
|
||||
use Cassandra\Date;
|
||||
use DateTime;
|
||||
use Exception;
|
||||
use Openguru\OpenCartFramework\Config\Settings;
|
||||
use Openguru\OpenCartFramework\Logger\LoggerInterface;
|
||||
use Openguru\OpenCartFramework\OpenCart\Decorators\OcRegistryDecorator;
|
||||
use Openguru\OpenCartFramework\QueryBuilder\Connections\ConnectionInterface;
|
||||
@@ -23,7 +21,7 @@ class OrderCreateService
|
||||
private ConnectionInterface $database;
|
||||
private CartService $cartService;
|
||||
private OcRegistryDecorator $oc;
|
||||
private Settings $settings;
|
||||
private SettingsService $settings;
|
||||
private TelegramService $telegramService;
|
||||
private LoggerInterface $logger;
|
||||
private ValidatorInterface $validator;
|
||||
@@ -32,7 +30,7 @@ class OrderCreateService
|
||||
ConnectionInterface $database,
|
||||
CartService $cartService,
|
||||
OcRegistryDecorator $registry,
|
||||
Settings $settings,
|
||||
SettingsService $settings,
|
||||
TelegramService $telegramService,
|
||||
LoggerInterface $logger,
|
||||
ValidatorInterface $validator
|
||||
@@ -56,10 +54,10 @@ class OrderCreateService
|
||||
|
||||
$now = date('Y-m-d H:i:s');
|
||||
$storeId = $this->settings->get('oc_store_id');
|
||||
$storeName = $this->settings->get('oc_store_name');
|
||||
$orderStatusId = $this->settings->get('oc_order_status_id');
|
||||
$customerGroupId = $this->settings->get('oc_customer_group_id');
|
||||
$languageId = $this->oc->config->get('config_language_id');
|
||||
$storeName = $this->settings->config()->getApp()->getAppName();
|
||||
$orderStatusId = $this->settings->config()->getOrders()->getOrderDefaultStatusId();
|
||||
$customerGroupId = $this->settings->config()->getOrders()->getOcCustomerGroupId();
|
||||
$languageId = $this->settings->config()->getApp()->getLanguageId();
|
||||
$currencyId = $this->oc->currency->getId($this->oc->session->data['currency']);
|
||||
$currencyCode = $this->oc->session->data['currency'];
|
||||
$currencyValue = $this->oc->currency->getValue($this->oc->session->data['currency']);
|
||||
@@ -233,8 +231,8 @@ class OrderCreateService
|
||||
'{created_at}' => $orderData['date_added'],
|
||||
];
|
||||
|
||||
$chatId = $this->settings->get('telegram.chat_id');
|
||||
$template = $this->settings->get('telegram.owner_notification_template');
|
||||
$chatId = $this->settings->config()->getTelegram()->getChatId();
|
||||
$template = $this->settings->config()->getTelegram()->getOwnerNotificationTemplate();
|
||||
|
||||
if ($chatId && $template) {
|
||||
$message = $this->telegramService->prepareMessage($template, $variables);
|
||||
@@ -248,7 +246,7 @@ class OrderCreateService
|
||||
|
||||
$allowsWriteToPm = Arr::get($tgInitData, 'user.allows_write_to_pm', false);
|
||||
$customerChatId = Arr::get($tgInitData, 'user.id');
|
||||
$template = $this->settings->get('telegram.customer_notification_template');
|
||||
$template = $this->settings->config()->getTelegram()->getCustomerNotificationTemplate();
|
||||
|
||||
if ($allowsWriteToPm && $customerChatId && $template) {
|
||||
$message = $this->telegramService->prepareMessage($template, $variables);
|
||||
|
||||
@@ -6,7 +6,6 @@ use App\Support\Utils;
|
||||
use Cart\Currency;
|
||||
use Cart\Tax;
|
||||
use Exception;
|
||||
use Openguru\OpenCartFramework\Config\Settings;
|
||||
use Openguru\OpenCartFramework\CriteriaBuilder\CriteriaBuilder;
|
||||
use Openguru\OpenCartFramework\Exceptions\EntityNotFoundException;
|
||||
use Openguru\OpenCartFramework\ImageTool\ImageToolInterface;
|
||||
@@ -24,7 +23,7 @@ class ProductsService
|
||||
private Builder $queryBuilder;
|
||||
private Currency $currency;
|
||||
private Tax $tax;
|
||||
private Settings $settings;
|
||||
private SettingsService $settings;
|
||||
private ImageToolInterface $ocImageTool;
|
||||
private OcRegistryDecorator $oc;
|
||||
private LoggerInterface $logger;
|
||||
@@ -34,7 +33,7 @@ class ProductsService
|
||||
Builder $queryBuilder,
|
||||
Currency $currency,
|
||||
Tax $tax,
|
||||
Settings $settings,
|
||||
SettingsService $settings,
|
||||
ImageToolInterface $ocImageTool,
|
||||
OcRegistryDecorator $registry,
|
||||
LoggerInterface $logger,
|
||||
@@ -61,8 +60,8 @@ class ProductsService
|
||||
$maxPages = $params['maxPages'] ?? 50;
|
||||
$filters = $params['filters'] ?? [];
|
||||
|
||||
$customerGroupId = (int) $this->settings->get('oc_customer_group_id');
|
||||
$currency = $this->settings->get('oc_default_currency');
|
||||
$customerGroupId = $this->settings->config()->getOrders()->getOcCustomerGroupId();
|
||||
$currency = $this->settings->config()->getStore()->getOcDefaultCurrency();
|
||||
|
||||
$specialPriceSql = "(SELECT price
|
||||
FROM oc_product_special ps
|
||||
@@ -173,7 +172,7 @@ class ProductsService
|
||||
$priceNumeric = $this->tax->calculate(
|
||||
$product['price'],
|
||||
$product['tax_class_id'],
|
||||
$this->settings->get('oc_config_tax'),
|
||||
$this->settings->config()->getStore()->isOcConfigTax(),
|
||||
);
|
||||
$price = $this->currency->format($priceNumeric, $currency);
|
||||
|
||||
@@ -183,7 +182,7 @@ class ProductsService
|
||||
$specialPriceNumeric = $this->tax->calculate(
|
||||
$product['special'],
|
||||
$product['tax_class_id'],
|
||||
$this->settings->get('oc_config_tax'),
|
||||
$this->settings->config()->getStore()->isOcConfigTax(),
|
||||
);
|
||||
$special = $this->currency->format(
|
||||
$specialPriceNumeric,
|
||||
|
||||
@@ -0,0 +1,686 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\DTO\Settings\AppDTO;
|
||||
use App\DTO\Settings\ConfigDTO;
|
||||
use App\DTO\Settings\DatabaseDTO;
|
||||
use App\DTO\Settings\LogsDTO;
|
||||
use App\DTO\Settings\MainpageSlider\LinkDTO;
|
||||
use App\DTO\Settings\MainpageSlider\LinkValueDTO;
|
||||
use App\DTO\Settings\MainpageSlider\MainpageSliderDTO;
|
||||
use App\DTO\Settings\MainpageSlider\SlideDTO;
|
||||
use App\DTO\Settings\MetricsDTO;
|
||||
use App\DTO\Settings\OrdersDTO;
|
||||
use App\DTO\Settings\SlidersDTO;
|
||||
use App\DTO\Settings\StoreDTO;
|
||||
use App\DTO\Settings\TelegramDTO;
|
||||
use App\DTO\Settings\TextsDTO;
|
||||
use InvalidArgumentException;
|
||||
|
||||
class SettingsSerializerService
|
||||
{
|
||||
public function fromArray(array $data): ConfigDTO
|
||||
{
|
||||
$keys = ['app', 'telegram', 'metrics', 'store', 'orders', 'texts', 'sliders', 'database', 'logs'];
|
||||
foreach ($keys as $key) {
|
||||
if (! array_key_exists($key, $data)) {
|
||||
throw new InvalidArgumentException("Settings key '$key' is required!");
|
||||
}
|
||||
}
|
||||
|
||||
$this->validateApp($data['app']);
|
||||
$this->validateTelegram($data['telegram']);
|
||||
$this->validateMetrics($data['metrics']);
|
||||
$this->validateStore($data['store']);
|
||||
$this->validateOrders($data['orders']);
|
||||
$this->validateTexts($data['texts']);
|
||||
$this->validateSliders($data['sliders']);
|
||||
$this->validateDatabase($data['database']);
|
||||
$this->validateLogs($data['logs']);
|
||||
|
||||
return new ConfigDTO(
|
||||
$this->deserializeApp($data['app']),
|
||||
$this->deserializeTelegram($data['telegram']),
|
||||
$this->deserializeMetrics($data['metrics']),
|
||||
$this->deserializeStore($data['store']),
|
||||
$this->deserializeOrders($data['orders']),
|
||||
$this->deserializeTexts($data['texts']),
|
||||
$this->deserializeSliders($data['sliders']),
|
||||
$this->deserializeDatabase($data['database']),
|
||||
$this->deserializeLogs($data['logs']),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \JsonException
|
||||
*/
|
||||
public function serialize(ConfigDTO $settings): string
|
||||
{
|
||||
return json_encode($settings->toArray(), JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
|
||||
private function deserializeApp(array $data): AppDTO
|
||||
{
|
||||
if (! isset($data['language_id'])) {
|
||||
throw new InvalidArgumentException('app.language_id is required');
|
||||
}
|
||||
|
||||
if (! is_int($data['language_id'])) {
|
||||
throw new InvalidArgumentException('app.language_id must be an integer');
|
||||
}
|
||||
|
||||
if (! isset($data['shop_base_url'])) {
|
||||
throw new InvalidArgumentException('app.shop_base_url is required');
|
||||
}
|
||||
|
||||
if (! is_string($data['shop_base_url'])) {
|
||||
throw new InvalidArgumentException('app.shop_base_url must be a string');
|
||||
}
|
||||
|
||||
return new AppDTO(
|
||||
$data['app_enabled'] ?? false,
|
||||
$data['app_name'] ?? '',
|
||||
$data['app_icon'] ?? null,
|
||||
$data['theme_light'] ?? 'light',
|
||||
$data['theme_dark'] ?? 'dark',
|
||||
$data['app_debug'] ?? false,
|
||||
$data['language_id'],
|
||||
$data['shop_base_url']
|
||||
);
|
||||
}
|
||||
|
||||
private function deserializeTelegram(array $data): TelegramDTO
|
||||
{
|
||||
if (! isset($data['mini_app_url'])) {
|
||||
throw new InvalidArgumentException('telegram.mini_app_url is required');
|
||||
}
|
||||
|
||||
if (! is_string($data['mini_app_url'])) {
|
||||
throw new InvalidArgumentException('telegram.mini_app_url must be a string');
|
||||
}
|
||||
|
||||
return new TelegramDTO(
|
||||
$data['bot_token'],
|
||||
$data['chat_id'],
|
||||
$data['owner_notification_template'],
|
||||
$data['customer_notification_template'],
|
||||
$data['mini_app_url']
|
||||
);
|
||||
}
|
||||
|
||||
private function deserializeMetrics(array $data): MetricsDTO
|
||||
{
|
||||
return new MetricsDTO(
|
||||
$data['yandex_metrika_enabled'] ?? false,
|
||||
$data['yandex_metrika_counter'] ?? ''
|
||||
);
|
||||
}
|
||||
|
||||
private function deserializeStore(array $data): StoreDTO
|
||||
{
|
||||
if (! isset($data['oc_default_currency'])) {
|
||||
throw new InvalidArgumentException('store.oc_default_currency is required');
|
||||
}
|
||||
|
||||
if (! is_string($data['oc_default_currency'])) {
|
||||
throw new InvalidArgumentException('store.oc_default_currency must be a string');
|
||||
}
|
||||
|
||||
if (! isset($data['oc_config_tax'])) {
|
||||
throw new InvalidArgumentException('store.oc_config_tax is required');
|
||||
}
|
||||
|
||||
if (! is_bool($data['oc_config_tax'])) {
|
||||
throw new InvalidArgumentException('store.oc_config_tax must be a boolean');
|
||||
}
|
||||
|
||||
if (! isset($data['oc_store_id'])) {
|
||||
throw new InvalidArgumentException('store.oc_store_id is required');
|
||||
}
|
||||
|
||||
if (! is_int($data['oc_store_id'])) {
|
||||
throw new InvalidArgumentException('store.oc_store_id must be an integer');
|
||||
}
|
||||
|
||||
return new StoreDTO(
|
||||
$data['enable_store'] ?? true,
|
||||
$data['mainpage_products'] ?? 'most_viewed',
|
||||
$data['featured_products'] ?? [],
|
||||
$data['mainpage_categories'] ?? 'latest10',
|
||||
$data['featured_categories'] ?? [],
|
||||
$data['feature_coupons'] ?? true,
|
||||
$data['feature_vouchers'] ?? true,
|
||||
$data['oc_default_currency'],
|
||||
$data['oc_config_tax'],
|
||||
$data['oc_store_id']
|
||||
);
|
||||
}
|
||||
|
||||
private function deserializeOrders(array $data): OrdersDTO
|
||||
{
|
||||
if (! isset($data['oc_customer_group_id'])) {
|
||||
throw new InvalidArgumentException('orders.oc_customer_group_id is required');
|
||||
}
|
||||
|
||||
if (! is_int($data['oc_customer_group_id'])) {
|
||||
throw new InvalidArgumentException('orders.oc_customer_group_id must be an integer');
|
||||
}
|
||||
|
||||
return new OrdersDTO(
|
||||
$data['order_default_status_id'] ?? 1,
|
||||
$data['oc_customer_group_id']
|
||||
);
|
||||
}
|
||||
|
||||
private function deserializeTexts(array $data): TextsDTO
|
||||
{
|
||||
return new TextsDTO(
|
||||
$data['text_no_more_products'],
|
||||
$data['text_empty_cart'],
|
||||
$data['text_order_created_success']
|
||||
);
|
||||
}
|
||||
|
||||
private function deserializeSliders(array $data): SlidersDTO
|
||||
{
|
||||
return new SlidersDTO(
|
||||
$this->deserializeMainpageSlider($data['mainpage_slider'] ?? [])
|
||||
);
|
||||
}
|
||||
|
||||
private function deserializeMainpageSlider(array $data): MainpageSliderDTO
|
||||
{
|
||||
$slides = [];
|
||||
if (isset($data['slides']) && is_array($data['slides'])) {
|
||||
foreach ($data['slides'] as $slideData) {
|
||||
$slides[] = $this->deserializeSlide($slideData);
|
||||
}
|
||||
}
|
||||
|
||||
return new MainpageSliderDTO(
|
||||
$data['is_enabled'] ?? false,
|
||||
$data['effect'] ?? 'slide',
|
||||
$data['pagination'] ?? true,
|
||||
$data['scrollbar'] ?? false,
|
||||
$data['free_mode'] ?? false,
|
||||
$data['space_between'] ?? 30,
|
||||
$data['autoplay'] ?? false,
|
||||
$data['loop'] ?? false,
|
||||
$slides
|
||||
);
|
||||
}
|
||||
|
||||
private function deserializeSlide(array $data): SlideDTO
|
||||
{
|
||||
return new SlideDTO(
|
||||
$data['title'] ?? '',
|
||||
$this->deserializeLink($data['link'] ?? []),
|
||||
$data['image'] ?? ''
|
||||
);
|
||||
}
|
||||
|
||||
private function deserializeLink(array $data): LinkDTO
|
||||
{
|
||||
$value = null;
|
||||
if (isset($data['value'])) {
|
||||
$value = $this->deserializeLinkValue($data['value']);
|
||||
}
|
||||
|
||||
return new LinkDTO(
|
||||
$data['type'] ?? 'none',
|
||||
$value
|
||||
);
|
||||
}
|
||||
|
||||
private function deserializeLinkValue(array $data): LinkValueDTO
|
||||
{
|
||||
return new LinkValueDTO(
|
||||
$data['category_id'] ?? null,
|
||||
$data['name'] ?? null,
|
||||
$data['product_id'] ?? null
|
||||
);
|
||||
}
|
||||
|
||||
// ==================== Validation Methods ====================
|
||||
|
||||
private function validateApp(array $data): void
|
||||
{
|
||||
if (! is_bool($data['app_enabled'])) {
|
||||
throw new InvalidArgumentException('app.app_enabled must be a boolean');
|
||||
}
|
||||
|
||||
if (! is_string($data['app_name'])) {
|
||||
throw new InvalidArgumentException('app.app_name must be a string');
|
||||
}
|
||||
|
||||
if (isset($data['app_icon']) && ! is_string($data['app_icon'])) {
|
||||
throw new InvalidArgumentException('app.app_icon must be a string or null');
|
||||
}
|
||||
|
||||
if (! is_string($data['theme_light'])) {
|
||||
throw new InvalidArgumentException('app.theme_light must be a string');
|
||||
}
|
||||
|
||||
if (! is_string($data['theme_dark'])) {
|
||||
throw new InvalidArgumentException('app.theme_dark must be a string');
|
||||
}
|
||||
|
||||
if (! is_bool($data['app_debug'])) {
|
||||
throw new InvalidArgumentException('app.app_debug must be a boolean');
|
||||
}
|
||||
|
||||
if (! isset($data['language_id'])) {
|
||||
throw new InvalidArgumentException('app.language_id is required');
|
||||
}
|
||||
|
||||
if (! is_int($data['language_id'])) {
|
||||
throw new InvalidArgumentException('app.language_id must be an integer');
|
||||
}
|
||||
|
||||
if ($data['language_id'] <= 0) {
|
||||
throw new InvalidArgumentException('app.language_id must be a positive integer');
|
||||
}
|
||||
|
||||
if (! isset($data['shop_base_url'])) {
|
||||
throw new InvalidArgumentException('app.shop_base_url is required');
|
||||
}
|
||||
|
||||
if (! is_string($data['shop_base_url'])) {
|
||||
throw new InvalidArgumentException('app.shop_base_url must be a string');
|
||||
}
|
||||
}
|
||||
|
||||
private function validateTelegram(array $data): void
|
||||
{
|
||||
if (isset($data['bot_token']) && ! is_string($data['bot_token'])) {
|
||||
throw new InvalidArgumentException('telegram.bot_token must be a string or null');
|
||||
}
|
||||
|
||||
if (isset($data['chat_id']) && ! is_int($data['chat_id'])) {
|
||||
throw new InvalidArgumentException('telegram.chat_id must be an integer or null');
|
||||
}
|
||||
|
||||
if (isset($data['owner_notification_template']) && ! is_string(
|
||||
$data['owner_notification_template']
|
||||
)) {
|
||||
throw new InvalidArgumentException('telegram.owner_notification_template must be a string or null');
|
||||
}
|
||||
|
||||
if (isset($data['customer_notification_template']) && ! is_string(
|
||||
$data['customer_notification_template']
|
||||
)) {
|
||||
throw new InvalidArgumentException('telegram.customer_notification_template must be a string or null');
|
||||
}
|
||||
|
||||
if (! isset($data['mini_app_url'])) {
|
||||
throw new InvalidArgumentException('telegram.mini_app_url is required');
|
||||
}
|
||||
|
||||
if (! is_string($data['mini_app_url'])) {
|
||||
throw new InvalidArgumentException('telegram.mini_app_url must be a string');
|
||||
}
|
||||
}
|
||||
|
||||
private function validateMetrics(array $data): void
|
||||
{
|
||||
if (isset($data['yandex_metrika_enabled']) && ! is_bool($data['yandex_metrika_enabled'])) {
|
||||
throw new InvalidArgumentException('metrics.yandex_metrika_enabled must be a boolean');
|
||||
}
|
||||
|
||||
if (isset($data['yandex_metrika_counter']) && ! is_string($data['yandex_metrika_counter'])) {
|
||||
throw new InvalidArgumentException('metrics.yandex_metrika_counter must be a string');
|
||||
}
|
||||
}
|
||||
|
||||
private function validateStore(array $data): void
|
||||
{
|
||||
if (isset($data['enable_store']) && ! is_bool($data['enable_store'])) {
|
||||
throw new InvalidArgumentException('store.enable_store must be a boolean');
|
||||
}
|
||||
|
||||
if (isset($data['mainpage_products']) && ! is_string($data['mainpage_products'])) {
|
||||
throw new InvalidArgumentException('store.mainpage_products must be a string');
|
||||
}
|
||||
|
||||
if (isset($data['featured_products'])) {
|
||||
if (! is_array($data['featured_products'])) {
|
||||
throw new InvalidArgumentException('store.featured_products must be an array');
|
||||
}
|
||||
foreach ($data['featured_products'] as $index => $productId) {
|
||||
if (! is_int($productId)) {
|
||||
throw new InvalidArgumentException("store.featured_products[$index] must be an integer");
|
||||
}
|
||||
if ($productId <= 0) {
|
||||
throw new InvalidArgumentException("store.featured_products[$index] must be a positive integer");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($data['mainpage_categories']) && ! is_string($data['mainpage_categories'])) {
|
||||
throw new InvalidArgumentException('store.mainpage_categories must be a string');
|
||||
}
|
||||
|
||||
if (isset($data['featured_categories'])) {
|
||||
if (! is_array($data['featured_categories'])) {
|
||||
throw new InvalidArgumentException('store.featured_categories must be an array');
|
||||
}
|
||||
foreach ($data['featured_categories'] as $index => $categoryId) {
|
||||
if (! is_int($categoryId)) {
|
||||
throw new InvalidArgumentException("store.featured_categories[$index] must be an integer");
|
||||
}
|
||||
if ($categoryId <= 0) {
|
||||
throw new InvalidArgumentException(
|
||||
"store.featured_categories[$index] must be a positive integer"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($data['feature_coupons']) && ! is_bool($data['feature_coupons'])) {
|
||||
throw new InvalidArgumentException('store.feature_coupons must be a boolean');
|
||||
}
|
||||
|
||||
if (isset($data['feature_vouchers']) && ! is_bool($data['feature_vouchers'])) {
|
||||
throw new InvalidArgumentException('store.feature_vouchers must be a boolean');
|
||||
}
|
||||
|
||||
if (! isset($data['oc_default_currency'])) {
|
||||
throw new InvalidArgumentException('store.oc_default_currency is required');
|
||||
}
|
||||
|
||||
if (! is_string($data['oc_default_currency'])) {
|
||||
throw new InvalidArgumentException('store.oc_default_currency must be a string');
|
||||
}
|
||||
|
||||
if (! isset($data['oc_config_tax'])) {
|
||||
throw new InvalidArgumentException('store.oc_config_tax is required');
|
||||
}
|
||||
|
||||
if (! is_bool($data['oc_config_tax'])) {
|
||||
throw new InvalidArgumentException('store.oc_config_tax must be a boolean');
|
||||
}
|
||||
|
||||
if (! isset($data['oc_store_id'])) {
|
||||
throw new InvalidArgumentException('store.oc_store_id is required');
|
||||
}
|
||||
|
||||
if (! is_int($data['oc_store_id'])) {
|
||||
throw new InvalidArgumentException('store.oc_store_id must be an integer');
|
||||
}
|
||||
|
||||
if ($data['oc_store_id'] < 0) {
|
||||
throw new InvalidArgumentException('store.oc_store_id must be a positive integer or equals 0');
|
||||
}
|
||||
}
|
||||
|
||||
private function validateOrders(array $data): void
|
||||
{
|
||||
if (isset($data['order_default_status_id'])) {
|
||||
if (! is_int($data['order_default_status_id'])) {
|
||||
throw new InvalidArgumentException('orders.order_default_status_id must be an integer');
|
||||
}
|
||||
if ($data['order_default_status_id'] <= 0) {
|
||||
throw new InvalidArgumentException('orders.order_default_status_id must be a positive integer');
|
||||
}
|
||||
}
|
||||
|
||||
if (! isset($data['oc_customer_group_id'])) {
|
||||
throw new InvalidArgumentException('orders.oc_customer_group_id is required');
|
||||
}
|
||||
|
||||
if (! is_int($data['oc_customer_group_id'])) {
|
||||
throw new InvalidArgumentException('orders.oc_customer_group_id must be an integer');
|
||||
}
|
||||
|
||||
if ($data['oc_customer_group_id'] <= 0) {
|
||||
throw new InvalidArgumentException('orders.oc_customer_group_id must be a positive integer');
|
||||
}
|
||||
}
|
||||
|
||||
private function validateTexts(array $data): void
|
||||
{
|
||||
if (isset($data['text_no_more_products']) && ! is_string($data['text_no_more_products'])) {
|
||||
throw new InvalidArgumentException('texts.text_no_more_products must be a string');
|
||||
}
|
||||
|
||||
if (isset($data['text_empty_cart']) && ! is_string($data['text_empty_cart'])) {
|
||||
throw new InvalidArgumentException('texts.text_empty_cart must be a string');
|
||||
}
|
||||
|
||||
if (isset($data['text_order_created_success']) && ! is_string($data['text_order_created_success'])) {
|
||||
throw new InvalidArgumentException('texts.text_order_created_success must be a string');
|
||||
}
|
||||
}
|
||||
|
||||
private function validateSliders(array $data): void
|
||||
{
|
||||
if (isset($data['mainpage_slider'])) {
|
||||
if (! is_array($data['mainpage_slider'])) {
|
||||
throw new InvalidArgumentException('sliders.mainpage_slider must be an object');
|
||||
}
|
||||
$this->validateMainpageSlider($data['mainpage_slider']);
|
||||
}
|
||||
}
|
||||
|
||||
private function validateMainpageSlider(array $data): void
|
||||
{
|
||||
if (isset($data['is_enabled']) && ! is_bool($data['is_enabled'])) {
|
||||
throw new InvalidArgumentException('sliders.mainpage_slider.is_enabled must be a boolean');
|
||||
}
|
||||
|
||||
if (isset($data['effect'])) {
|
||||
if (! is_string($data['effect'])) {
|
||||
throw new InvalidArgumentException('sliders.mainpage_slider.effect must be a string');
|
||||
}
|
||||
$allowedEffects = ['slide', 'fade', 'cube', 'coverflow', 'flip'];
|
||||
if (! in_array($data['effect'], $allowedEffects, true)) {
|
||||
throw new InvalidArgumentException(
|
||||
'sliders.mainpage_slider.effect must be one of: ' . implode(', ', $allowedEffects)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($data['pagination']) && ! is_bool($data['pagination'])) {
|
||||
throw new InvalidArgumentException('sliders.mainpage_slider.pagination must be a boolean');
|
||||
}
|
||||
|
||||
if (isset($data['scrollbar']) && ! is_bool($data['scrollbar'])) {
|
||||
throw new InvalidArgumentException('sliders.mainpage_slider.scrollbar must be a boolean');
|
||||
}
|
||||
|
||||
if (isset($data['free_mode']) && ! is_bool($data['free_mode'])) {
|
||||
throw new InvalidArgumentException('sliders.mainpage_slider.free_mode must be a boolean');
|
||||
}
|
||||
|
||||
if (isset($data['space_between'])) {
|
||||
if (! is_int($data['space_between'])) {
|
||||
throw new InvalidArgumentException('sliders.mainpage_slider.space_between must be an integer');
|
||||
}
|
||||
if ($data['space_between'] < 0) {
|
||||
throw new InvalidArgumentException(
|
||||
'sliders.mainpage_slider.space_between must be a non-negative integer'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($data['autoplay']) && ! is_bool($data['autoplay'])) {
|
||||
throw new InvalidArgumentException('sliders.mainpage_slider.autoplay must be a boolean');
|
||||
}
|
||||
|
||||
if (isset($data['loop']) && ! is_bool($data['loop'])) {
|
||||
throw new InvalidArgumentException('sliders.mainpage_slider.loop must be a boolean');
|
||||
}
|
||||
|
||||
if (isset($data['slides'])) {
|
||||
if (! is_array($data['slides'])) {
|
||||
throw new InvalidArgumentException('sliders.mainpage_slider.slides must be an array');
|
||||
}
|
||||
foreach ($data['slides'] as $index => $slideData) {
|
||||
if (! is_array($slideData)) {
|
||||
throw new InvalidArgumentException("sliders.mainpage_slider.slides[$index] must be an object");
|
||||
}
|
||||
$this->validateSlide($slideData, $index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function validateSlide(array $data, int $index): void
|
||||
{
|
||||
if (isset($data['title']) && ! is_string($data['title'])) {
|
||||
throw new InvalidArgumentException("sliders.mainpage_slider.slides[$index].title must be a string");
|
||||
}
|
||||
|
||||
if (isset($data['link'])) {
|
||||
if (! is_array($data['link'])) {
|
||||
throw new InvalidArgumentException("sliders.mainpage_slider.slides[$index].link must be an object");
|
||||
}
|
||||
$this->validateLink($data['link'], $index);
|
||||
}
|
||||
|
||||
if (isset($data['image']) && ! is_string($data['image'])) {
|
||||
throw new InvalidArgumentException("sliders.mainpage_slider.slides[$index].image must be a string");
|
||||
}
|
||||
}
|
||||
|
||||
private function validateLink(array $data, int $slideIndex): void
|
||||
{
|
||||
if (isset($data['type'])) {
|
||||
if (! is_string($data['type'])) {
|
||||
throw new InvalidArgumentException(
|
||||
"sliders.mainpage_slider.slides[$slideIndex].link.type must be a string"
|
||||
);
|
||||
}
|
||||
$allowedTypes = ['none', 'category', 'product'];
|
||||
if (! in_array($data['type'], $allowedTypes, true)) {
|
||||
throw new InvalidArgumentException(
|
||||
"sliders.mainpage_slider.slides[$slideIndex].link.type must be one of: " . implode(
|
||||
', ',
|
||||
$allowedTypes
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($data['value'])) {
|
||||
if ($data['value'] !== null) {
|
||||
if (! is_array($data['value'])) {
|
||||
throw new InvalidArgumentException(
|
||||
"sliders.mainpage_slider.slides[$slideIndex].link.value must be an object or null"
|
||||
);
|
||||
}
|
||||
$this->validateLinkValue($data['value'], $data['type'] ?? 'none', $slideIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function validateLinkValue(array $data, string $linkType, int $slideIndex): void
|
||||
{
|
||||
if ($linkType === 'category') {
|
||||
if (isset($data['category_id'])) {
|
||||
if (! is_int($data['category_id'])) {
|
||||
throw new InvalidArgumentException(
|
||||
"sliders.mainpage_slider.slides[$slideIndex].link.value.category_id must be an integer"
|
||||
);
|
||||
}
|
||||
if ($data['category_id'] <= 0) {
|
||||
throw new InvalidArgumentException(
|
||||
"sliders.mainpage_slider.slides[$slideIndex].link.value.category_id must be a positive integer"
|
||||
);
|
||||
}
|
||||
}
|
||||
if (isset($data['name']) && ! is_string($data['name'])) {
|
||||
throw new InvalidArgumentException(
|
||||
"sliders.mainpage_slider.slides[$slideIndex].link.value.name must be a string"
|
||||
);
|
||||
}
|
||||
} elseif ($linkType === 'product') {
|
||||
if (isset($data['product_id'])) {
|
||||
if (! is_int($data['product_id'])) {
|
||||
throw new InvalidArgumentException(
|
||||
"sliders.mainpage_slider.slides[$slideIndex].link.value.product_id must be an integer"
|
||||
);
|
||||
}
|
||||
if ($data['product_id'] <= 0) {
|
||||
throw new InvalidArgumentException(
|
||||
"sliders.mainpage_slider.slides[$slideIndex].link.value.product_id must be a positive integer"
|
||||
);
|
||||
}
|
||||
}
|
||||
if (isset($data['name']) && ! is_string($data['name'])) {
|
||||
throw new InvalidArgumentException(
|
||||
"sliders.mainpage_slider.slides[$slideIndex].link.value.name must be a string"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Проверяем, что не переданы лишние поля
|
||||
$allowedFields = ['category_id', 'product_id', 'name'];
|
||||
foreach (array_keys($data) as $field) {
|
||||
if (! in_array($field, $allowedFields, true)) {
|
||||
throw new InvalidArgumentException(
|
||||
"sliders.mainpage_slider.slides[$slideIndex].link.value contains unknown field: $field"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function deserializeLogs(array $logs): LogsDTO
|
||||
{
|
||||
return new LogsDTO(
|
||||
$logs['path'],
|
||||
);
|
||||
}
|
||||
|
||||
private function deserializeDatabase(array $data): DatabaseDTO
|
||||
{
|
||||
return new DatabaseDTO(
|
||||
$data['host'] ?? '',
|
||||
$data['database'] ?? '',
|
||||
$data['username'] ?? '',
|
||||
$data['password'] ?? '',
|
||||
$data['prefix'] ?? '',
|
||||
$data['port'] ?? 3306
|
||||
);
|
||||
}
|
||||
|
||||
private function validateDatabase(array $data): void
|
||||
{
|
||||
if (isset($data['host']) && ! is_string($data['host'])) {
|
||||
throw new InvalidArgumentException('database.host must be a string');
|
||||
}
|
||||
|
||||
if (isset($data['database']) && ! is_string($data['database'])) {
|
||||
throw new InvalidArgumentException('database.database must be a string');
|
||||
}
|
||||
|
||||
if (isset($data['username']) && ! is_string($data['username'])) {
|
||||
throw new InvalidArgumentException('database.username must be a string');
|
||||
}
|
||||
|
||||
if (isset($data['password']) && ! is_string($data['password'])) {
|
||||
throw new InvalidArgumentException('database.password must be a string');
|
||||
}
|
||||
|
||||
if (isset($data['prefix']) && ! is_string($data['prefix'])) {
|
||||
throw new InvalidArgumentException('database.prefix must be a string');
|
||||
}
|
||||
|
||||
if (isset($data['port'])) {
|
||||
if (! is_int($data['port'])) {
|
||||
throw new InvalidArgumentException('database.port must be an integer');
|
||||
}
|
||||
if ($data['port'] <= 0 || $data['port'] > 65535) {
|
||||
throw new InvalidArgumentException('database.port must be between 1 and 65535');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function validateLogs(array $logs): void
|
||||
{
|
||||
if (! isset($logs['path'])) {
|
||||
throw new InvalidArgumentException('Logs path must be set');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\DTO\Settings\ConfigDTO;
|
||||
use Openguru\OpenCartFramework\Config\Settings;
|
||||
|
||||
class SettingsService extends Settings
|
||||
{
|
||||
private ConfigDTO $config;
|
||||
|
||||
public function __construct(array $config, SettingsSerializerService $serializer)
|
||||
{
|
||||
parent::__construct($config);
|
||||
|
||||
$this->config = $serializer->fromArray($config);
|
||||
}
|
||||
|
||||
public function config(): ConfigDTO
|
||||
{
|
||||
return $this->config;
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,11 @@ namespace App\Support;
|
||||
|
||||
final class Utils
|
||||
{
|
||||
/**
|
||||
* @param string $string
|
||||
* @return string
|
||||
* @deprecated use Str::htmlEntityEncode instead
|
||||
*/
|
||||
public static function htmlEntityEncode(string $string): string
|
||||
{
|
||||
return html_entity_decode($string, ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
||||
|
||||
@@ -41,7 +41,7 @@ class TestCase extends BaseTestCase
|
||||
private function bootstrapApplication(): Application
|
||||
{
|
||||
$app = ApplicationFactory::create([
|
||||
'db' => [
|
||||
'database' => [
|
||||
'host' => getenv('DB_HOSTNAME') ?: 'mysql',
|
||||
'database' => getenv('DB_DATABASE') ?: 'ocstore3',
|
||||
'username' => getenv('DB_USERNAME') ?: 'root',
|
||||
@@ -59,6 +59,7 @@ class TestCase extends BaseTestCase
|
||||
'chat_id' => '123',
|
||||
'owner_notification_template' => 'Test',
|
||||
'customer_notification_template' => 'Test',
|
||||
'mini_app_url' => 'https://example.com',
|
||||
],
|
||||
]);
|
||||
|
||||
|
||||
@@ -49,6 +49,39 @@ class ArrTest extends TestCase
|
||||
$this->assertEquals('default', Arr::get($data, 'nonexistent', 'default'));
|
||||
}
|
||||
|
||||
public function testHas(): void
|
||||
{
|
||||
$data = [
|
||||
'key' => 'value',
|
||||
'nested' => [
|
||||
'key' => 'nestedValue',
|
||||
'deep' => [
|
||||
'key' => 'deepValue',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
// Простые ключи
|
||||
$this->assertTrue(Arr::has($data, 'key'));
|
||||
$this->assertFalse(Arr::has($data, 'nonexistent'));
|
||||
|
||||
// Dot notation - один уровень
|
||||
$this->assertTrue(Arr::has($data, 'nested.key'));
|
||||
$this->assertFalse(Arr::has($data, 'nested.nonexistent'));
|
||||
|
||||
// Dot notation - несколько уровней
|
||||
$this->assertTrue(Arr::has($data, 'nested.deep.key'));
|
||||
$this->assertFalse(Arr::has($data, 'nested.deep.nonexistent'));
|
||||
$this->assertFalse(Arr::has($data, 'nested.nonexistent.key'));
|
||||
|
||||
// Пустой массив
|
||||
$this->assertFalse(Arr::has([], 'key'));
|
||||
|
||||
// Ключ с точкой в корневом массиве
|
||||
$dataWithDotKey = ['key.with.dots' => 'value'];
|
||||
$this->assertTrue(Arr::has($dataWithDotKey, 'key.with.dots'));
|
||||
}
|
||||
|
||||
public function testSet(): void
|
||||
{
|
||||
$data = [];
|
||||
@@ -191,4 +224,443 @@ class ArrTest extends TestCase
|
||||
|
||||
$this->assertSame($expected, Arr::mergeArraysRecursively($base, $override));
|
||||
}
|
||||
|
||||
public function testMergeArraysRecursivelyWithDotNotationSimple(): void
|
||||
{
|
||||
$base = ['app' => ['name' => 'MyApp']];
|
||||
$override = ['app.logs.path' => '/var/log'];
|
||||
|
||||
$expected = [
|
||||
'app' => [
|
||||
'name' => 'MyApp',
|
||||
'logs' => [
|
||||
'path' => '/var/log',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$result = Arr::mergeArraysRecursivelyWithDotNotation($base, $override);
|
||||
$this->assertSame($expected, $result);
|
||||
}
|
||||
|
||||
public function testMergeArraysRecursivelyWithDotNotationOverrideExisting(): void
|
||||
{
|
||||
$base = [
|
||||
'app' => [
|
||||
'name' => 'MyApp',
|
||||
'logs' => [
|
||||
'path' => '/tmp/log',
|
||||
'level' => 'info',
|
||||
],
|
||||
],
|
||||
];
|
||||
$override = ['app.logs.path' => '/var/log'];
|
||||
|
||||
$expected = [
|
||||
'app' => [
|
||||
'name' => 'MyApp',
|
||||
'logs' => [
|
||||
'path' => '/var/log',
|
||||
'level' => 'info',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$result = Arr::mergeArraysRecursivelyWithDotNotation($base, $override);
|
||||
$this->assertSame($expected, $result);
|
||||
}
|
||||
|
||||
public function testMergeArraysRecursivelyWithDotNotationMultipleKeys(): void
|
||||
{
|
||||
$base = ['app' => ['name' => 'MyApp']];
|
||||
$override = [
|
||||
'app.logs.path' => '/var/log',
|
||||
'app.logs.level' => 'debug',
|
||||
'app.debug' => true,
|
||||
];
|
||||
|
||||
$expected = [
|
||||
'app' => [
|
||||
'name' => 'MyApp',
|
||||
'logs' => [
|
||||
'path' => '/var/log',
|
||||
'level' => 'debug',
|
||||
],
|
||||
'debug' => true,
|
||||
],
|
||||
];
|
||||
|
||||
$result = Arr::mergeArraysRecursivelyWithDotNotation($base, $override);
|
||||
$this->assertSame($expected, $result);
|
||||
}
|
||||
|
||||
public function testMergeArraysRecursivelyWithDotNotationDeepNesting(): void
|
||||
{
|
||||
$base = [
|
||||
'app' => [
|
||||
'name' => 'MyApp',
|
||||
],
|
||||
];
|
||||
$override = [
|
||||
'app.logs.path' => '/var/log',
|
||||
'app.config.database.host' => 'localhost',
|
||||
'app.config.database.port' => 3306,
|
||||
];
|
||||
|
||||
$expected = [
|
||||
'app' => [
|
||||
'name' => 'MyApp',
|
||||
'logs' => [
|
||||
'path' => '/var/log',
|
||||
],
|
||||
'config' => [
|
||||
'database' => [
|
||||
'host' => 'localhost',
|
||||
'port' => 3306,
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$result = Arr::mergeArraysRecursivelyWithDotNotation($base, $override);
|
||||
$this->assertSame($expected, $result);
|
||||
}
|
||||
|
||||
public function testMergeArraysRecursivelyWithDotNotationMixedKeys(): void
|
||||
{
|
||||
$base = ['app' => ['name' => 'MyApp']];
|
||||
$override = [
|
||||
'app.logs.path' => '/var/log',
|
||||
'telegram' => ['bot_token' => '123456'],
|
||||
];
|
||||
|
||||
$expected = [
|
||||
'app' => [
|
||||
'name' => 'MyApp',
|
||||
'logs' => [
|
||||
'path' => '/var/log',
|
||||
],
|
||||
],
|
||||
'telegram' => [
|
||||
'bot_token' => '123456',
|
||||
],
|
||||
];
|
||||
|
||||
$result = Arr::mergeArraysRecursivelyWithDotNotation($base, $override);
|
||||
$this->assertSame($expected, $result);
|
||||
}
|
||||
|
||||
public function testMergeArraysRecursivelyWithDotNotationEmptyBase(): void
|
||||
{
|
||||
$base = [];
|
||||
$override = ['app.logs.path' => '/var/log'];
|
||||
|
||||
$expected = [
|
||||
'app' => [
|
||||
'logs' => [
|
||||
'path' => '/var/log',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$result = Arr::mergeArraysRecursivelyWithDotNotation($base, $override);
|
||||
$this->assertSame($expected, $result);
|
||||
}
|
||||
|
||||
public function testMergeArraysRecursivelyWithDotNotationEmptyOverride(): void
|
||||
{
|
||||
$base = ['app' => ['name' => 'MyApp']];
|
||||
$override = [];
|
||||
|
||||
$expected = ['app' => ['name' => 'MyApp']];
|
||||
|
||||
$result = Arr::mergeArraysRecursivelyWithDotNotation($base, $override);
|
||||
$this->assertSame($expected, $result);
|
||||
}
|
||||
|
||||
public function testMergeArraysRecursivelyWithDotNotationOverrideArrayWithValue(): void
|
||||
{
|
||||
$base = [
|
||||
'app' => [
|
||||
'logs' => [
|
||||
'path' => '/tmp/log',
|
||||
'level' => 'info',
|
||||
],
|
||||
],
|
||||
];
|
||||
$override = ['app.logs' => '/var/log'];
|
||||
|
||||
$expected = [
|
||||
'app' => [
|
||||
'logs' => '/var/log',
|
||||
],
|
||||
];
|
||||
|
||||
$result = Arr::mergeArraysRecursivelyWithDotNotation($base, $override);
|
||||
$this->assertSame($expected, $result);
|
||||
}
|
||||
|
||||
public function testMergeArraysRecursivelyWithDotNotationRealWorldExample(): void
|
||||
{
|
||||
$json = [
|
||||
'module_telecart_settings' => [
|
||||
'app' => [
|
||||
'app_enabled' => true,
|
||||
'app_name' => 'Telecart',
|
||||
],
|
||||
'telegram' => [
|
||||
'bot_token' => 'old_token',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$envOverrides = [
|
||||
'app.logs.path' => '/var/log/telecart',
|
||||
'app.app_debug' => true,
|
||||
'telegram.bot_token' => 'new_token_from_env',
|
||||
];
|
||||
|
||||
$result = Arr::mergeArraysRecursivelyWithDotNotation(
|
||||
$json['module_telecart_settings'],
|
||||
$envOverrides
|
||||
);
|
||||
|
||||
$expected = [
|
||||
'app' => [
|
||||
'app_enabled' => true,
|
||||
'app_name' => 'Telecart',
|
||||
'logs' => [
|
||||
'path' => '/var/log/telecart',
|
||||
],
|
||||
'app_debug' => true,
|
||||
],
|
||||
'telegram' => [
|
||||
'bot_token' => 'new_token_from_env',
|
||||
],
|
||||
];
|
||||
|
||||
$this->assertSame($expected, $result);
|
||||
}
|
||||
|
||||
public function testGetWithKeysSimpleKeys(): void
|
||||
{
|
||||
$array = [
|
||||
'app_name' => 'MyApp',
|
||||
'debug_mode' => true,
|
||||
'default_language' => 'en',
|
||||
];
|
||||
|
||||
$keys = ['app_name', 'debug_mode'];
|
||||
$result = Arr::getWithKeys($array, $keys);
|
||||
|
||||
$expected = [
|
||||
'app_name' => 'MyApp',
|
||||
'debug_mode' => true,
|
||||
];
|
||||
|
||||
$this->assertSame($expected, $result);
|
||||
}
|
||||
|
||||
public function testGetWithKeysDotNotation(): void
|
||||
{
|
||||
$array = [
|
||||
'app' => [
|
||||
'name' => 'MyApp',
|
||||
'logs' => [
|
||||
'path' => '/var/log',
|
||||
'level' => 'debug',
|
||||
],
|
||||
],
|
||||
'telegram' => [
|
||||
'bot_token' => 'token123',
|
||||
'chat_id' => 'chat456',
|
||||
],
|
||||
];
|
||||
|
||||
$keys = ['app.name', 'app.logs.path', 'telegram.bot_token'];
|
||||
$result = Arr::getWithKeys($array, $keys);
|
||||
|
||||
$expected = [
|
||||
'app' => [
|
||||
'name' => 'MyApp',
|
||||
'logs' => [
|
||||
'path' => '/var/log',
|
||||
],
|
||||
],
|
||||
'telegram' => [
|
||||
'bot_token' => 'token123',
|
||||
],
|
||||
];
|
||||
|
||||
$this->assertSame($expected, $result);
|
||||
}
|
||||
|
||||
public function testGetWithKeysNonExistentKeys(): void
|
||||
{
|
||||
$array = [
|
||||
'app' => [
|
||||
'name' => 'MyApp',
|
||||
],
|
||||
'telegram' => [
|
||||
'bot_token' => 'token123',
|
||||
],
|
||||
];
|
||||
|
||||
$keys = ['app.name', 'app.nonexistent', 'telegram.bot_token', 'nonexistent.key'];
|
||||
$result = Arr::getWithKeys($array, $keys);
|
||||
|
||||
$expected = [
|
||||
'app' => [
|
||||
'name' => 'MyApp',
|
||||
],
|
||||
'telegram' => [
|
||||
'bot_token' => 'token123',
|
||||
],
|
||||
];
|
||||
|
||||
$this->assertSame($expected, $result);
|
||||
}
|
||||
|
||||
public function testGetWithKeysWithNullValues(): void
|
||||
{
|
||||
$array = [
|
||||
'app' => [
|
||||
'name' => 'MyApp',
|
||||
'icon' => null,
|
||||
'logs' => [
|
||||
'path' => '/var/log',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$keys = ['app.name', 'app.icon', 'app.logs.path'];
|
||||
$result = Arr::getWithKeys($array, $keys);
|
||||
|
||||
$expected = [
|
||||
'app' => [
|
||||
'name' => 'MyApp',
|
||||
'icon' => null,
|
||||
'logs' => [
|
||||
'path' => '/var/log',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$this->assertSame($expected, $result);
|
||||
}
|
||||
|
||||
public function testGetWithKeysEmptyKeysArray(): void
|
||||
{
|
||||
$array = [
|
||||
'app' => [
|
||||
'name' => 'MyApp',
|
||||
],
|
||||
];
|
||||
|
||||
$keys = [];
|
||||
$result = Arr::getWithKeys($array, $keys);
|
||||
|
||||
$this->assertSame([], $result);
|
||||
}
|
||||
|
||||
public function testGetWithKeysPreservesStructure(): void
|
||||
{
|
||||
$array = [
|
||||
'app' => [
|
||||
'name' => 'MyApp',
|
||||
'config' => [
|
||||
'database' => [
|
||||
'host' => 'localhost',
|
||||
'port' => 3306,
|
||||
'username' => 'root',
|
||||
],
|
||||
],
|
||||
],
|
||||
'telegram' => [
|
||||
'bot_token' => 'token',
|
||||
],
|
||||
];
|
||||
|
||||
$keys = ['app.config.database.host', 'app.config.database.port'];
|
||||
$result = Arr::getWithKeys($array, $keys);
|
||||
|
||||
$expected = [
|
||||
'app' => [
|
||||
'config' => [
|
||||
'database' => [
|
||||
'host' => 'localhost',
|
||||
'port' => 3306,
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$this->assertSame($expected, $result);
|
||||
}
|
||||
|
||||
public function testGetWithKeysMultipleKeysFromSameBranch(): void
|
||||
{
|
||||
$array = [
|
||||
'app' => [
|
||||
'name' => 'MyApp',
|
||||
'logs' => [
|
||||
'path' => '/var/log',
|
||||
'level' => 'debug',
|
||||
'max_files' => 10,
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$keys = ['app.logs.path', 'app.logs.level', 'app.logs.max_files'];
|
||||
$result = Arr::getWithKeys($array, $keys);
|
||||
|
||||
$expected = [
|
||||
'app' => [
|
||||
'logs' => [
|
||||
'path' => '/var/log',
|
||||
'level' => 'debug',
|
||||
'max_files' => 10,
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$this->assertSame($expected, $result);
|
||||
}
|
||||
|
||||
public function testGetWithKeysMixedTypes(): void
|
||||
{
|
||||
$array = [
|
||||
'app' => [
|
||||
'name' => 'MyApp',
|
||||
'enabled' => true,
|
||||
'count' => 42,
|
||||
'price' => 99.99,
|
||||
'tags' => ['tag1', 'tag2'],
|
||||
],
|
||||
];
|
||||
|
||||
$keys = ['app.name', 'app.enabled', 'app.count', 'app.price', 'app.tags'];
|
||||
$result = Arr::getWithKeys($array, $keys);
|
||||
|
||||
$expected = [
|
||||
'app' => [
|
||||
'name' => 'MyApp',
|
||||
'enabled' => true,
|
||||
'count' => 42,
|
||||
'price' => 99.99,
|
||||
'tags' => ['tag1', 'tag2'],
|
||||
],
|
||||
];
|
||||
|
||||
$this->assertSame($expected, $result);
|
||||
}
|
||||
|
||||
public function testGetWithKeysEmptyArray(): void
|
||||
{
|
||||
$array = [];
|
||||
$keys = ['app.name', 'telegram.bot_token'];
|
||||
$result = Arr::getWithKeys($array, $keys);
|
||||
|
||||
$this->assertSame([], $result);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ class CriteriaBuilderTest extends TestCase
|
||||
}
|
||||
|
||||
$baseSettings = [
|
||||
'db' => [
|
||||
'database' => [
|
||||
'prefix' => 'oc_',
|
||||
],
|
||||
];
|
||||
@@ -77,7 +77,7 @@ class CriteriaBuilderTest extends TestCase
|
||||
});
|
||||
|
||||
$application->singleton(Settings::class, function () {
|
||||
return new Settings();
|
||||
return new Settings([]);
|
||||
});
|
||||
|
||||
/** @var RulesRegistry $rulesRegistry */
|
||||
|
||||
@@ -37,17 +37,6 @@ class SettingsTest extends TestCase
|
||||
$this->assertEquals('default_host', $this->settings->get('database.non_existent', 'default_host'));
|
||||
}
|
||||
|
||||
public function testSet(): void
|
||||
{
|
||||
$this->settings->set('app_name', 'NewApp');
|
||||
$this->assertEquals('NewApp', $this->settings->get('app_name'));
|
||||
|
||||
$this->settings->set('new_setting', 'new_value');
|
||||
$this->assertEquals('new_value', $this->settings->get('new_setting'));
|
||||
|
||||
$this->settings->set('database.host', '127.0.0.1');
|
||||
$this->assertEquals('127.0.0.1', $this->settings->get('database.host'));
|
||||
}
|
||||
|
||||
public function testHas(): void
|
||||
{
|
||||
@@ -58,15 +47,6 @@ class SettingsTest extends TestCase
|
||||
$this->assertFalse($this->settings->has('database.non_existent'));
|
||||
}
|
||||
|
||||
public function testRemove(): void
|
||||
{
|
||||
$this->settings->remove('debug_mode');
|
||||
$this->assertFalse($this->settings->has('debug_mode'));
|
||||
|
||||
$this->settings->remove('database.host');
|
||||
$this->assertFalse($this->settings->has('database.host'));
|
||||
}
|
||||
|
||||
public function testGetAll(): void
|
||||
{
|
||||
$expected = [
|
||||
@@ -81,39 +61,4 @@ class SettingsTest extends TestCase
|
||||
|
||||
$this->assertEquals($expected, $this->settings->getAll());
|
||||
}
|
||||
|
||||
public function testSetAll(): void
|
||||
{
|
||||
$newSettings = [
|
||||
'app_name' => 'NewApp',
|
||||
'theme' => 'dark',
|
||||
];
|
||||
$this->settings->setAll($newSettings);
|
||||
$this->assertEquals($newSettings, $this->settings->getAll());
|
||||
}
|
||||
|
||||
public function testDotNotationGetAndSet(): void
|
||||
{
|
||||
$this->settings->set('database.username', 'root');
|
||||
$this->assertEquals('root', $this->settings->get('database.username'));
|
||||
|
||||
$this->settings->set('app.env', 'production');
|
||||
$this->assertEquals('production', $this->settings->get('app.env'));
|
||||
}
|
||||
|
||||
public function testDotNotationHas(): void
|
||||
{
|
||||
$this->settings->set('cache.enabled', true);
|
||||
$this->assertTrue($this->settings->has('cache.enabled'));
|
||||
$this->assertFalse($this->settings->has('cache.non_existent'));
|
||||
}
|
||||
|
||||
public function testDotNotationRemove(): void
|
||||
{
|
||||
$this->settings->set('logging.level', 'debug');
|
||||
$this->assertTrue($this->settings->has('logging.level'));
|
||||
|
||||
$this->settings->remove('logging.level');
|
||||
$this->assertFalse($this->settings->has('logging.level'));
|
||||
}
|
||||
}
|
||||
|
||||
55
module/oc_telegram_shop/upload/oc_telegram_shop/tests/Unit/StrTest.php
Executable file
55
module/oc_telegram_shop/upload/oc_telegram_shop/tests/Unit/StrTest.php
Executable file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use Openguru\OpenCartFramework\Support\Str;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class StrTest extends TestCase
|
||||
{
|
||||
public function testStartsWithSingleNeedle(): void
|
||||
{
|
||||
$this->assertTrue(Str::startsWith('hello world', 'hello'));
|
||||
$this->assertTrue(Str::startsWith('test string', 'test'));
|
||||
$this->assertFalse(Str::startsWith('hello world', 'world'));
|
||||
$this->assertFalse(Str::startsWith('hello world', 'foo'));
|
||||
}
|
||||
|
||||
public function testStartsWithEmptyHaystack(): void
|
||||
{
|
||||
$this->assertFalse(Str::startsWith('', 'hello'));
|
||||
$this->assertTrue(Str::startsWith('', ''));
|
||||
}
|
||||
|
||||
public function testStartsWithEmptyNeedle(): void
|
||||
{
|
||||
$this->assertFalse(Str::startsWith('hello', ''));
|
||||
}
|
||||
|
||||
public function testStartsWithMultipleNeedles(): void
|
||||
{
|
||||
$this->assertTrue(Str::startsWith('hello world', ['hello', 'foo']));
|
||||
$this->assertTrue(Str::startsWith('test string', ['foo', 'test']));
|
||||
$this->assertFalse(Str::startsWith('hello world', ['foo', 'bar']));
|
||||
}
|
||||
|
||||
public function testStartsWithCaseSensitive(): void
|
||||
{
|
||||
$this->assertTrue(Str::startsWith('Hello World', 'Hello'));
|
||||
$this->assertFalse(Str::startsWith('Hello World', 'hello'));
|
||||
$this->assertFalse(Str::startsWith('hello world', 'Hello'));
|
||||
}
|
||||
|
||||
public function testStartsWithExactMatch(): void
|
||||
{
|
||||
$this->assertTrue(Str::startsWith('test', 'test'));
|
||||
}
|
||||
|
||||
public function testStartsWithLongNeedle(): void
|
||||
{
|
||||
$this->assertTrue(Str::startsWith('short', 'short'));
|
||||
$this->assertFalse(Str::startsWith('short', 'short text'));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"hash": "db53d5f1",
|
||||
"configHash": "861000cb",
|
||||
"lockfileHash": "e3b0c442",
|
||||
"browserHash": "84fd2900",
|
||||
"optimized": {},
|
||||
"chunks": {}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"type": "module"
|
||||
}
|
||||
Reference in New Issue
Block a user