diff --git a/frontend/admin/bun.lock b/frontend/admin/bun.lock index 93f894e..ed406be 100644 --- a/frontend/admin/bun.lock +++ b/frontend/admin/bun.lock @@ -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=="], diff --git a/frontend/admin/package.json b/frontend/admin/package.json index 631bc46..f953149 100644 --- a/frontend/admin/package.json +++ b/frontend/admin/package.json @@ -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" diff --git a/frontend/admin/src/App.vue b/frontend/admin/src/App.vue index 65662c0..c02445b 100644 --- a/frontend/admin/src/App.vue +++ b/frontend/admin/src/App.vue @@ -1,10 +1,77 @@ + + - diff --git a/frontend/admin/src/components/Settings/ItemCategoriesSelect.vue b/frontend/admin/src/components/Settings/ItemCategoriesSelect.vue new file mode 100644 index 0000000..f456b6f --- /dev/null +++ b/frontend/admin/src/components/Settings/ItemCategoriesSelect.vue @@ -0,0 +1,116 @@ + + + diff --git a/frontend/admin/src/components/Settings/ItemImage.vue b/frontend/admin/src/components/Settings/ItemImage.vue new file mode 100644 index 0000000..c7478d9 --- /dev/null +++ b/frontend/admin/src/components/Settings/ItemImage.vue @@ -0,0 +1,26 @@ + + + + + diff --git a/frontend/admin/src/components/Settings/ItemInput.vue b/frontend/admin/src/components/Settings/ItemInput.vue new file mode 100644 index 0000000..f342fa0 --- /dev/null +++ b/frontend/admin/src/components/Settings/ItemInput.vue @@ -0,0 +1,45 @@ + + + + + diff --git a/frontend/admin/src/components/Settings/ItemProductsSelect.vue b/frontend/admin/src/components/Settings/ItemProductsSelect.vue new file mode 100644 index 0000000..ef172ec --- /dev/null +++ b/frontend/admin/src/components/Settings/ItemProductsSelect.vue @@ -0,0 +1,116 @@ + + + diff --git a/frontend/admin/src/components/Settings/ItemSelect.vue b/frontend/admin/src/components/Settings/ItemSelect.vue new file mode 100644 index 0000000..ee20698 --- /dev/null +++ b/frontend/admin/src/components/Settings/ItemSelect.vue @@ -0,0 +1,34 @@ + + + + + diff --git a/frontend/admin/src/components/Settings/ItemTextarea.vue b/frontend/admin/src/components/Settings/ItemTextarea.vue new file mode 100644 index 0000000..72b7dc9 --- /dev/null +++ b/frontend/admin/src/components/Settings/ItemTextarea.vue @@ -0,0 +1,41 @@ + + + + + + diff --git a/frontend/admin/src/components/Settings/ItemTgMiniAppLink.vue b/frontend/admin/src/components/Settings/ItemTgMiniAppLink.vue new file mode 100644 index 0000000..f239fe7 --- /dev/null +++ b/frontend/admin/src/components/Settings/ItemTgMiniAppLink.vue @@ -0,0 +1,31 @@ + + + + + diff --git a/frontend/admin/src/components/Slider/Slider.vue b/frontend/admin/src/components/Slider/Slider.vue index 7c382e7..6b178a8 100644 --- a/frontend/admin/src/components/Slider/Slider.vue +++ b/frontend/admin/src/components/Slider/Slider.vue @@ -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); -}); diff --git a/frontend/admin/src/main.js b/frontend/admin/src/main.js index c83f111..4097131 100644 --- a/frontend/admin/src/main.js +++ b/frontend/admin/src/main.js @@ -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(); }); diff --git a/frontend/admin/src/router/index.js b/frontend/admin/src/router/index.js index 4569e2f..5bbba6a 100644 --- a/frontend/admin/src/router/index.js +++ b/frontend/admin/src/router/index.js @@ -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; diff --git a/frontend/admin/src/stores/settings.js b/frontend/admin/src/stores/settings.js new file mode 100644 index 0000000..1c77814 --- /dev/null +++ b/frontend/admin/src/stores/settings.js @@ -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; + }, + }, +}); diff --git a/frontend/admin/src/stores/stats.js b/frontend/admin/src/stores/stats.js new file mode 100644 index 0000000..2afac9a --- /dev/null +++ b/frontend/admin/src/stores/stats.js @@ -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; + } + }, + +}); diff --git a/frontend/admin/src/utils/http.js b/frontend/admin/src/utils/http.js new file mode 100644 index 0000000..d68a0e1 --- /dev/null +++ b/frontend/admin/src/utils/http.js @@ -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, +}; + diff --git a/frontend/admin/src/utils/toastHelper.js b/frontend/admin/src/utils/toastHelper.js new file mode 100644 index 0000000..96ca792 --- /dev/null +++ b/frontend/admin/src/utils/toastHelper.js @@ -0,0 +1,2 @@ +import mitt from 'mitt'; +export const toastBus = mitt(); diff --git a/frontend/admin/src/views/GeneralView.vue b/frontend/admin/src/views/GeneralView.vue new file mode 100644 index 0000000..93af1c3 --- /dev/null +++ b/frontend/admin/src/views/GeneralView.vue @@ -0,0 +1,56 @@ + + + + + diff --git a/frontend/admin/src/views/HomeView.vue b/frontend/admin/src/views/HomeView.vue deleted file mode 100644 index 9000062..0000000 --- a/frontend/admin/src/views/HomeView.vue +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/frontend/admin/src/views/MetricsView.vue b/frontend/admin/src/views/MetricsView.vue new file mode 100644 index 0000000..85dcc60 --- /dev/null +++ b/frontend/admin/src/views/MetricsView.vue @@ -0,0 +1,29 @@ + + + diff --git a/frontend/admin/src/views/OrdersView.vue b/frontend/admin/src/views/OrdersView.vue new file mode 100644 index 0000000..b778039 --- /dev/null +++ b/frontend/admin/src/views/OrdersView.vue @@ -0,0 +1,17 @@ + + + diff --git a/frontend/admin/src/views/SliderView.vue b/frontend/admin/src/views/SliderView.vue new file mode 100644 index 0000000..152fce9 --- /dev/null +++ b/frontend/admin/src/views/SliderView.vue @@ -0,0 +1,10 @@ + + + diff --git a/frontend/admin/src/views/StoreView.vue b/frontend/admin/src/views/StoreView.vue new file mode 100644 index 0000000..6e07e75 --- /dev/null +++ b/frontend/admin/src/views/StoreView.vue @@ -0,0 +1,80 @@ + + + diff --git a/frontend/admin/src/views/TelegramView.vue b/frontend/admin/src/views/TelegramView.vue new file mode 100644 index 0000000..a35e8bd --- /dev/null +++ b/frontend/admin/src/views/TelegramView.vue @@ -0,0 +1,28 @@ + + + diff --git a/frontend/admin/src/views/TextsView.vue b/frontend/admin/src/views/TextsView.vue new file mode 100644 index 0000000..0150260 --- /dev/null +++ b/frontend/admin/src/views/TextsView.vue @@ -0,0 +1,21 @@ + + + diff --git a/module/oc_telegram_shop/upload/admin/controller/extension/module/tgshop.php b/module/oc_telegram_shop/upload/admin/controller/extension/module/tgshop.php index 86f1fe3..a57f9fd 100755 --- a/module/oc_telegram_shop/upload/admin/controller/extension/module/tgshop.php +++ b/module/oc_telegram_shop/upload/admin/controller/extension/module/tgshop.php @@ -1,8 +1,12 @@ 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' => << << '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' => << [ - 'type' => 'image', - 'help' => << [ - 'type' => 'select', - 'options' => static::$themes, - 'help' => 'Выберите стиль, который будет использоваться при отображении вашего магазина в Telegram для дневного режима. Посмотреть как выглядят темы', - ], - - 'module_tgshop_theme_dark' => [ - 'type' => 'select', - 'options' => static::$themes, - 'help' => 'Выберите стиль, который будет использоваться при отображении вашего магазина в Telegram для ночного режима. Посмотреть как выглядят темы', - ], - - 'module_tgshop_debug' => [ - 'type' => 'select', - 'options' => [ - 0 => 'Выключено', - 1 => 'Включено', - ], - 'help' => 'Режим разработчика. Рекомендуется включать только по необходимости. В остальных случаях, для нормальной работы магазина, должен быть выключен.', - ], - ], - 'telegram' => [ - 'module_tgshop_mini_app_url' => [ - 'type' => 'text_readonly', - 'help' => <<⚠️ Важно: ссылка обязательно должна заканчиваться на /#/ — иначе приложение не загрузится.

-HTML, - ], - - 'module_tgshop_bot_token' => [ - 'type' => 'bot_token', - 'placeholder' => 'Введите токен от телеграм бота', - 'help' => << [ - 'type' => 'chatid', - 'placeholder' => 'Введите Chat ID', - 'help' => << [ - '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' => <<включена — пользователи смогут оформлять заказы прямо в Telegram-магазине.
-Если выключена — оформление заказов будет недоступно. Вместо кнопки «Добавить в корзину» пользователи увидят кнопку «Перейти к товару», которая откроет страницу товара на вашем сайте. В этом режиме 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' => <<купоны OpenCart для предоставления скидок при оформлении заказа. -HTML, - ], - - 'module_tgshop_feature_vouchers' => [ - 'type' => 'select', - 'options' => [ - 0 => 'Выключено', - 1 => 'Включено', - ], - 'help' => <<подарочные сертификаты OpenCart при оформлении заказа. -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); + } + } + } } diff --git a/module/oc_telegram_shop/upload/admin/view/template/extension/module/tgshop.twig b/module/oc_telegram_shop/upload/admin/view/template/extension/module/tgshop.twig index d45027b..e264e63 100755 --- a/module/oc_telegram_shop/upload/admin/view/template/extension/module/tgshop.twig +++ b/module/oc_telegram_shop/upload/admin/view/template/extension/module/tgshop.twig @@ -2,11 +2,6 @@
@@ -467,7 +42,7 @@