From cfb626b5956fa4180492f39996595ebae8d849b4 Mon Sep 17 00:00:00 2001 From: poduck Date: Tue, 16 Dec 2025 11:56:01 -0500 Subject: [PATCH] Rename plugins to automations and fix scheduler/payment bugs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Major changes: - Rename "plugins" to "automations" throughout codebase - Move automation system to dedicated app (scheduling/automations/) - Add new automation marketplace, creation, and management pages Bug fixes: - Fix payment endpoint 500 errors (use has_feature() instead of attribute) - Fix scheduler showing "0 AM" instead of "12 AM" - Fix scheduler business hours double-inversion display issue - Fix scheduler scroll-to-current-time when switching views - Fix week view centering on wrong day (use Sunday-based indexing) - Fix capacity widget overflow with many resources - Fix Recharts minWidth/minHeight console warnings šŸ¤– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- frontend/package-lock.json | 543 ++++++++++++ frontend/package.json | 9 +- frontend/playwright-report/index.html | 2 +- frontend/scripts/crawl-site.ts | 293 +++++++ frontend/src/App.tsx | 30 +- frontend/src/components/CreateTaskModal.tsx | 80 +- frontend/src/components/EditTaskModal.tsx | 12 +- frontend/src/components/EventAutomations.tsx | 96 +- .../src/components/FloatingHelpButton.tsx | 8 +- frontend/src/components/Sidebar.tsx | 10 +- .../components/dashboard/CapacityWidget.tsx | 2 +- .../src/components/dashboard/ChartWidget.tsx | 2 +- .../dashboard/CustomerBreakdownWidget.tsx | 2 +- ...ginShowcase.tsx => AutomationShowcase.tsx} | 6 +- frontend/src/i18n/locales/en.json | 512 +++++------ ...ketplace.tsx => AutomationMarketplace.tsx} | 254 +++--- ...{CreatePlugin.tsx => CreateAutomation.tsx} | 120 +-- .../{MyPlugins.tsx => MyAutomations.tsx} | 328 +++---- frontend/src/pages/OwnerScheduler.tsx | 28 +- frontend/src/pages/StaffDashboard.tsx | 2 +- frontend/src/pages/Tasks.tsx | 78 +- ...pPluginDocs.tsx => HelpAutomationDocs.tsx} | 0 .../{HelpPlugins.tsx => HelpAutomations.tsx} | 0 frontend/src/pages/marketing/HomePage.tsx | 6 +- .../src/pages/marketing/PrivacyPolicyPage.tsx | 23 +- .../marketing/__tests__/HomePage.test.tsx | 22 +- .../src/pages/platform/PlatformDashboard.tsx | 2 +- frontend/src/types.ts | 14 +- frontend/test-results/.last-run.json | 7 +- .../test-results/email-preview-footer.png | Bin 109763 -> 0 bytes frontend/tests/e2e/site-crawler.spec.ts | 232 +++++ frontend/tests/e2e/utils/crawler.ts | 568 ++++++++++++ .../config/settings/multitenancy.py | 1 + smoothschedule/config/urls.py | 2 + .../smoothschedule/commerce/payments/views.py | 18 +- .../smoothschedule/identity/core/mixins.py | 43 +- .../identity/core/tests/test_mixins.py | 52 +- .../scheduling/automations/__init__.py | 1 + .../scheduling/automations/admin.py | 3 + .../scheduling/automations/apps.py | 12 + .../builtin.py} | 48 +- .../custom_script.py} | 26 +- .../automations/migrations/__init__.py | 0 .../scheduling/automations/models.py | 36 + .../scheduling/automations/registry.py | 239 +++++ .../scheduling/automations/serializers.py | 368 ++++++++ .../scheduling/automations/signals.py | 15 + .../scheduling/automations/tests/__init__.py | 0 .../automations/tests/test_registry.py | 585 +++++++++++++ .../scheduling/automations/urls.py | 39 + .../scheduling/automations/views.py | 826 ++++++++++++++++++ .../scheduling/schedule/example_automation.py | 526 ----------- .../scheduling/schedule/models.py | 2 +- .../scheduling/schedule/plugins.py | 239 ----- .../scheduling/schedule/serializers.py | 4 +- .../scheduling/schedule/tasks.py | 4 +- .../scheduling/schedule/tests/test_plugins.py | 585 ------------- .../schedule/tests/test_serializers.py | 8 +- .../schedule/tests/test_services.py | 186 ++-- .../scheduling/schedule/tests/test_views.py | 8 +- .../scheduling/schedule/urls.py | 9 +- .../scheduling/schedule/views.py | 151 +++- 62 files changed, 4914 insertions(+), 2413 deletions(-) create mode 100644 frontend/scripts/crawl-site.ts rename frontend/src/components/marketing/{PluginShowcase.tsx => AutomationShowcase.tsx} (99%) rename frontend/src/pages/{PluginMarketplace.tsx => AutomationMarketplace.tsx} (76%) rename frontend/src/pages/{CreatePlugin.tsx => CreateAutomation.tsx} (83%) rename frontend/src/pages/{MyPlugins.tsx => MyAutomations.tsx} (72%) rename frontend/src/pages/help/{HelpPluginDocs.tsx => HelpAutomationDocs.tsx} (100%) rename frontend/src/pages/help/{HelpPlugins.tsx => HelpAutomations.tsx} (100%) delete mode 100644 frontend/test-results/email-preview-footer.png create mode 100644 frontend/tests/e2e/site-crawler.spec.ts create mode 100644 frontend/tests/e2e/utils/crawler.ts create mode 100644 smoothschedule/smoothschedule/scheduling/automations/__init__.py create mode 100644 smoothschedule/smoothschedule/scheduling/automations/admin.py create mode 100644 smoothschedule/smoothschedule/scheduling/automations/apps.py rename smoothschedule/smoothschedule/scheduling/{schedule/builtin_plugins.py => automations/builtin.py} (91%) rename smoothschedule/smoothschedule/scheduling/{schedule/custom_script_plugin.py => automations/custom_script.py} (92%) create mode 100644 smoothschedule/smoothschedule/scheduling/automations/migrations/__init__.py create mode 100644 smoothschedule/smoothschedule/scheduling/automations/models.py create mode 100644 smoothschedule/smoothschedule/scheduling/automations/registry.py create mode 100644 smoothschedule/smoothschedule/scheduling/automations/serializers.py create mode 100644 smoothschedule/smoothschedule/scheduling/automations/signals.py create mode 100644 smoothschedule/smoothschedule/scheduling/automations/tests/__init__.py create mode 100644 smoothschedule/smoothschedule/scheduling/automations/tests/test_registry.py create mode 100644 smoothschedule/smoothschedule/scheduling/automations/urls.py create mode 100644 smoothschedule/smoothschedule/scheduling/automations/views.py delete mode 100644 smoothschedule/smoothschedule/scheduling/schedule/example_automation.py delete mode 100644 smoothschedule/smoothschedule/scheduling/schedule/plugins.py delete mode 100644 smoothschedule/smoothschedule/scheduling/schedule/tests/test_plugins.py diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 21c3621..723d709 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -57,6 +57,7 @@ "jsdom": "^27.2.0", "postcss": "^8.5.6", "tailwindcss": "^4.1.17", + "tsx": "^4.21.0", "typescript": "^5.9.3", "vite": "^7.2.4", "vitest": "^4.0.15" @@ -4074,6 +4075,19 @@ "node": ">= 0.4" } }, + "node_modules/get-tsconfig": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", + "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -5970,6 +5984,16 @@ "node": ">=4" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/rollup": { "version": "4.53.3", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz", @@ -6285,6 +6309,525 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tsx/node_modules/@esbuild/aix-ppc64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.1.tgz", + "integrity": "sha512-HHB50pdsBX6k47S4u5g/CaLjqS3qwaOVE5ILsq64jyzgMhLuCuZ8rGzM9yhsAjfjkbgUPMzZEPa7DAp7yz6vuA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.1.tgz", + "integrity": "sha512-kFqa6/UcaTbGm/NncN9kzVOODjhZW8e+FRdSeypWe6j33gzclHtwlANs26JrupOntlcWmB0u8+8HZo8s7thHvg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.1.tgz", + "integrity": "sha512-45fuKmAJpxnQWixOGCrS+ro4Uvb4Re9+UTieUY2f8AEc+t7d4AaZ6eUJ3Hva7dtrxAAWHtlEFsXFMAgNnGU9uQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-x64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.1.tgz", + "integrity": "sha512-LBEpOz0BsgMEeHgenf5aqmn/lLNTFXVfoWMUox8CtWWYK9X4jmQzWjoGoNb8lmAYml/tQ/Ysvm8q7szu7BoxRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-arm64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.1.tgz", + "integrity": "sha512-veg7fL8eMSCVKL7IW4pxb54QERtedFDfY/ASrumK/SbFsXnRazxY4YykN/THYqFnFwJ0aVjiUrVG2PwcdAEqQQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-x64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.1.tgz", + "integrity": "sha512-+3ELd+nTzhfWb07Vol7EZ+5PTbJ/u74nC6iv4/lwIU99Ip5uuY6QoIf0Hn4m2HoV0qcnRivN3KSqc+FyCHjoVQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.1.tgz", + "integrity": "sha512-/8Rfgns4XD9XOSXlzUDepG8PX+AVWHliYlUkFI3K3GB6tqbdjYqdhcb4BKRd7C0BhZSoaCxhv8kTcBrcZWP+xg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-x64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.1.tgz", + "integrity": "sha512-GITpD8dK9C+r+5yRT/UKVT36h/DQLOHdwGVwwoHidlnA168oD3uxA878XloXebK4Ul3gDBBIvEdL7go9gCUFzQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.1.tgz", + "integrity": "sha512-ieMID0JRZY/ZeCrsFQ3Y3NlHNCqIhTprJfDgSB3/lv5jJZ8FX3hqPyXWhe+gvS5ARMBJ242PM+VNz/ctNj//eA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.1.tgz", + "integrity": "sha512-W9//kCrh/6in9rWIBdKaMtuTTzNj6jSeG/haWBADqLLa9P8O5YSRDzgD5y9QBok4AYlzS6ARHifAb75V6G670Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ia32": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.1.tgz", + "integrity": "sha512-VIUV4z8GD8rtSVMfAj1aXFahsi/+tcoXXNYmXgzISL+KB381vbSTNdeZHHHIYqFyXcoEhu9n5cT+05tRv13rlw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-loong64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.1.tgz", + "integrity": "sha512-l4rfiiJRN7sTNI//ff65zJ9z8U+k6zcCg0LALU5iEWzY+a1mVZ8iWC1k5EsNKThZ7XCQ6YWtsZ8EWYm7r1UEsg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-mips64el": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.1.tgz", + "integrity": "sha512-U0bEuAOLvO/DWFdygTHWY8C067FXz+UbzKgxYhXC0fDieFa0kDIra1FAhsAARRJbvEyso8aAqvPdNxzWuStBnA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ppc64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.1.tgz", + "integrity": "sha512-NzdQ/Xwu6vPSf/GkdmRNsOfIeSGnh7muundsWItmBsVpMoNPVpM61qNzAVY3pZ1glzzAxLR40UyYM23eaDDbYQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-riscv64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.1.tgz", + "integrity": "sha512-7zlw8p3IApcsN7mFw0O1Z1PyEk6PlKMu18roImfl3iQHTnr/yAfYv6s4hXPidbDoI2Q0pW+5xeoM4eTCC0UdrQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-s390x": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.1.tgz", + "integrity": "sha512-cGj5wli+G+nkVQdZo3+7FDKC25Uh4ZVwOAK6A06Hsvgr8WqBBuOy/1s+PUEd/6Je+vjfm6stX0kmib5b/O2Ykw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-x64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.1.tgz", + "integrity": "sha512-z3H/HYI9MM0HTv3hQZ81f+AKb+yEoCRlUby1F80vbQ5XdzEMyY/9iNlAmhqiBKw4MJXwfgsh7ERGEOhrM1niMA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.1.tgz", + "integrity": "sha512-wzC24DxAvk8Em01YmVXyjl96Mr+ecTPyOuADAvjGg+fyBpGmxmcr2E5ttf7Im8D0sXZihpxzO1isus8MdjMCXQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/netbsd-x64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.1.tgz", + "integrity": "sha512-1YQ8ybGi2yIXswu6eNzJsrYIGFpnlzEWRl6iR5gMgmsrR0FcNoV1m9k9sc3PuP5rUBLshOZylc9nqSgymI+TYg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.1.tgz", + "integrity": "sha512-5Z+DzLCrq5wmU7RDaMDe2DVXMRm2tTDvX2KU14JJVBN2CT/qov7XVix85QoJqHltpvAOZUAc3ndU56HSMWrv8g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openbsd-x64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.1.tgz", + "integrity": "sha512-Q73ENzIdPF5jap4wqLtsfh8YbYSZ8Q0wnxplOlZUOyZy7B4ZKW8DXGWgTCZmF8VWD7Tciwv5F4NsRf6vYlZtqg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.1.tgz", + "integrity": "sha512-ajbHrGM/XiK+sXM0JzEbJAen+0E+JMQZ2l4RR4VFwvV9JEERx+oxtgkpoKv1SevhjavK2z2ReHk32pjzktWbGg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/sunos-x64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.1.tgz", + "integrity": "sha512-IPUW+y4VIjuDVn+OMzHc5FV4GubIwPnsz6ubkvN8cuhEqH81NovB53IUlrlBkPMEPxvNnf79MGBoz8rZ2iW8HA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-arm64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.1.tgz", + "integrity": "sha512-RIVRWiljWA6CdVu8zkWcRmGP7iRRIIwvhDKem8UMBjPql2TXM5PkDVvvrzMtj1V+WFPB4K7zkIGM7VzRtFkjdg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-ia32": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.1.tgz", + "integrity": "sha512-2BR5M8CPbptC1AK5JbJT1fWrHLvejwZidKx3UMSF0ecHMa+smhi16drIrCEggkgviBwLYd5nwrFLSl5Kho96RQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-x64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.1.tgz", + "integrity": "sha512-d5X6RMYv6taIymSk8JBP+nxv8DQAMY6A51GPgusqLdK9wBz5wWIXy1KjTck6HnjE9hqJzJRdk+1p/t5soSbCtw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/esbuild": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.1.tgz", + "integrity": "sha512-yY35KZckJJuVVPXpvjgxiCuVEJT67F6zDeVTv4rizyPrfGBUpZQsvmxnN+C371c2esD/hNMjj4tpBhuueLN7aA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.1", + "@esbuild/android-arm": "0.27.1", + "@esbuild/android-arm64": "0.27.1", + "@esbuild/android-x64": "0.27.1", + "@esbuild/darwin-arm64": "0.27.1", + "@esbuild/darwin-x64": "0.27.1", + "@esbuild/freebsd-arm64": "0.27.1", + "@esbuild/freebsd-x64": "0.27.1", + "@esbuild/linux-arm": "0.27.1", + "@esbuild/linux-arm64": "0.27.1", + "@esbuild/linux-ia32": "0.27.1", + "@esbuild/linux-loong64": "0.27.1", + "@esbuild/linux-mips64el": "0.27.1", + "@esbuild/linux-ppc64": "0.27.1", + "@esbuild/linux-riscv64": "0.27.1", + "@esbuild/linux-s390x": "0.27.1", + "@esbuild/linux-x64": "0.27.1", + "@esbuild/netbsd-arm64": "0.27.1", + "@esbuild/netbsd-x64": "0.27.1", + "@esbuild/openbsd-arm64": "0.27.1", + "@esbuild/openbsd-x64": "0.27.1", + "@esbuild/openharmony-arm64": "0.27.1", + "@esbuild/sunos-x64": "0.27.1", + "@esbuild/win32-arm64": "0.27.1", + "@esbuild/win32-ia32": "0.27.1", + "@esbuild/win32-x64": "0.27.1" + } + }, + "node_modules/tsx/node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index ebe71cf..f32f0c7 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -53,6 +53,7 @@ "jsdom": "^27.2.0", "postcss": "^8.5.6", "tailwindcss": "^4.1.17", + "tsx": "^4.21.0", "typescript": "^5.9.3", "vite": "^7.2.4", "vitest": "^4.0.15" @@ -68,6 +69,12 @@ "test:watch": "vitest --watch", "test:e2e": "playwright test", "test:e2e:ui": "playwright test --ui", - "test:e2e:headed": "playwright test --headed" + "test:e2e:headed": "playwright test --headed", + "crawl": "tsx scripts/crawl-site.ts", + "crawl:public": "tsx scripts/crawl-site.ts --public", + "crawl:platform": "tsx scripts/crawl-site.ts --platform", + "crawl:all": "tsx scripts/crawl-site.ts --all", + "crawl:verbose": "tsx scripts/crawl-site.ts --all --verbose", + "test:crawl": "playwright test site-crawler.spec.ts" } } diff --git a/frontend/playwright-report/index.html b/frontend/playwright-report/index.html index 6ebf236..56e39e2 100644 --- a/frontend/playwright-report/index.html +++ b/frontend/playwright-report/index.html @@ -82,4 +82,4 @@ Error generating stack: `+n.message+`
- \ No newline at end of file + \ No newline at end of file diff --git a/frontend/scripts/crawl-site.ts b/frontend/scripts/crawl-site.ts new file mode 100644 index 0000000..5c69ce2 --- /dev/null +++ b/frontend/scripts/crawl-site.ts @@ -0,0 +1,293 @@ +#!/usr/bin/env tsx +/** + * Site Crawler Script + * + * Crawls the SmoothSchedule site looking for: + * - Broken links + * - Console errors + * - Network failures + * - Page errors + * + * Usage: + * npx ts-node scripts/crawl-site.ts [options] + * + * Options: + * --public Crawl public marketing site only + * --platform Crawl platform dashboard (requires login) + * --tenant Crawl tenant dashboard (requires login) + * --all Crawl all areas (default) + * --max-pages=N Maximum pages to crawl per area (default: 50) + * --verbose Show detailed logging + * --screenshots Save screenshots on errors + * + * Examples: + * npx ts-node scripts/crawl-site.ts --public + * npx ts-node scripts/crawl-site.ts --all --max-pages=100 + * npx ts-node scripts/crawl-site.ts --platform --verbose + */ + +import { chromium, Browser, BrowserContext, Page } from 'playwright'; +import { + SiteCrawler, + CrawlerOptions, + CrawlReport, + formatReport, + loginAsUser, + TEST_USERS, + UserCredentials, +} from '../tests/e2e/utils/crawler'; +import * as fs from 'fs'; +import * as path from 'path'; + +interface CrawlArea { + name: string; + startUrl: string; + requiresAuth: boolean; + user?: UserCredentials; +} + +const CRAWL_AREAS: Record = { + public: { + name: 'Public Site', + startUrl: 'http://lvh.me:5173', + requiresAuth: false, + }, + platform: { + name: 'Platform Dashboard', + startUrl: 'http://platform.lvh.me:5173/platform/login', + requiresAuth: true, + user: TEST_USERS.platformSuperuser, + }, + tenant: { + name: 'Tenant Dashboard (Owner)', + startUrl: 'http://demo.lvh.me:5173/login', + requiresAuth: true, + user: TEST_USERS.businessOwner, + }, + tenantStaff: { + name: 'Tenant Dashboard (Staff)', + startUrl: 'http://demo.lvh.me:5173/login', + requiresAuth: true, + user: TEST_USERS.businessStaff, + }, + customer: { + name: 'Customer Booking Portal (Public)', + startUrl: 'http://demo.lvh.me:5173/book', + requiresAuth: false, // Public booking page + }, + customerPortal: { + name: 'Customer Portal (Logged In)', + startUrl: 'http://demo.lvh.me:5173/login', + requiresAuth: true, + user: TEST_USERS.customer, + }, +}; + +function parseArgs(): { + areas: string[]; + maxPages: number; + verbose: boolean; + screenshots: boolean; +} { + const args = process.argv.slice(2); + const result = { + areas: [] as string[], + maxPages: 0, // 0 = unlimited + verbose: false, + screenshots: false, + }; + + for (const arg of args) { + if (arg === '--public') result.areas.push('public'); + else if (arg === '--platform') result.areas.push('platform'); + else if (arg === '--tenant') result.areas.push('tenant'); + else if (arg === '--tenant-staff') result.areas.push('tenantStaff'); + else if (arg === '--customer') result.areas.push('customer'); + else if (arg === '--customer-portal') result.areas.push('customerPortal'); + else if (arg === '--all') result.areas = Object.keys(CRAWL_AREAS); + else if (arg.startsWith('--max-pages=')) result.maxPages = parseInt(arg.split('=')[1], 10); + else if (arg === '--verbose') result.verbose = true; + else if (arg === '--screenshots') result.screenshots = true; + else if (arg === '--help' || arg === '-h') { + console.log(` +Site Crawler - Find broken links and errors + +Usage: npm run crawl -- [options] + +Options: + --public Crawl public marketing site only + --platform Crawl platform dashboard (requires login) + --tenant Crawl tenant dashboard as Business Owner + --tenant-staff Crawl tenant dashboard as Staff Member + --customer Crawl customer booking portal (public) + --all Crawl all areas + --max-pages=N Maximum pages to crawl per area (default: 50) + --verbose Show detailed logging + --screenshots Save screenshots on errors + +Examples: + npm run crawl -- --public + npm run crawl -- --tenant --max-pages=50 + npm run crawl -- --all --verbose + `); + process.exit(0); + } + } + + // Default to public only if no areas specified + if (result.areas.length === 0) { + result.areas = ['public']; + } + + // Filter out areas that don't exist in CRAWL_AREAS + result.areas = result.areas.filter(area => CRAWL_AREAS[area]); + + return result; +} + +async function crawlArea( + browser: Browser, + area: CrawlArea, + options: CrawlerOptions +): Promise { + console.log(`\n${'═'.repeat(60)}`); + console.log(` Crawling: ${area.name}`); + console.log(` URL: ${area.startUrl}`); + console.log(`${'═'.repeat(60)}`); + + const context = await browser.newContext({ + viewport: { width: 1280, height: 720 }, + ignoreHTTPSErrors: true, + }); + const page = await context.newPage(); + + try { + // Login if required + let crawlStartUrl = area.startUrl; + if (area.requiresAuth && area.user) { + const loggedIn = await loginAsUser(page, area.user); + if (!loggedIn) { + console.error(`Failed to login for ${area.name}. Skipping.`); + return { + startTime: new Date(), + endTime: new Date(), + totalPages: 0, + totalErrors: 1, + results: [], + summary: { + consoleErrors: 0, + networkErrors: 0, + brokenLinks: 0, + pageErrors: 1, + }, + }; + } + // After login, start from the current URL (typically dashboard) + crawlStartUrl = page.url(); + } + + // Create crawler and run + const crawler = new SiteCrawler(page, context, options); + const report = await crawler.crawl(crawlStartUrl); + + return report; + } finally { + await context.close(); + } +} + +function saveReport(reports: Map): string { + // Ensure output directory exists + const outputDir = path.join(process.cwd(), 'test-results'); + if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir, { recursive: true }); + } + + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + const filename = path.join(outputDir, `crawl-report-${timestamp}.json`); + + const reportData = { + timestamp: new Date().toISOString(), + areas: Object.fromEntries(reports), + }; + + fs.writeFileSync(filename, JSON.stringify(reportData, null, 2)); + return filename; +} + +async function main() { + const { areas, maxPages, verbose, screenshots } = parseArgs(); + + console.log('\nšŸ•·ļø SmoothSchedule Site Crawler'); + console.log('─'.repeat(40)); + console.log(`Areas to crawl: ${areas.join(', ')}`); + console.log(`Max pages per area: ${maxPages}`); + console.log(`Verbose: ${verbose}`); + console.log(`Screenshots: ${screenshots}`); + + const browser = await chromium.launch({ + headless: true, + }); + + const options: CrawlerOptions = { + maxPages, + verbose, + screenshotOnError: screenshots, + screenshotDir: 'test-results/crawler-screenshots', + timeout: 30000, + waitForNetworkIdle: true, + }; + + // Ensure screenshot directory exists if enabled + if (screenshots && !fs.existsSync(options.screenshotDir!)) { + fs.mkdirSync(options.screenshotDir!, { recursive: true }); + } + + const reports = new Map(); + let totalErrors = 0; + + try { + for (const areaKey of areas) { + const area = CRAWL_AREAS[areaKey]; + if (!area) continue; + + const report = await crawlArea(browser, area, options); + reports.set(areaKey, report); + totalErrors += report.totalErrors; + + // Print report for this area + console.log(formatReport(report)); + } + + // Save combined report + const reportFile = saveReport(reports); + console.log(`\nšŸ“„ Full report saved to: ${reportFile}`); + + // Final summary + console.log('\n' + '═'.repeat(60)); + console.log(' FINAL SUMMARY'); + console.log('═'.repeat(60)); + + let totalPages = 0; + for (const [areaKey, report] of reports) { + const area = CRAWL_AREAS[areaKey]; + const icon = report.totalErrors === 0 ? 'āœ…' : 'āŒ'; + console.log(`${icon} ${area.name}: ${report.totalPages} pages, ${report.totalErrors} errors`); + totalPages += report.totalPages; + } + + console.log('─'.repeat(60)); + console.log(` Total: ${totalPages} pages crawled, ${totalErrors} errors found`); + console.log('═'.repeat(60) + '\n'); + + // Exit with error code if errors found + process.exit(totalErrors > 0 ? 1 : 0); + } finally { + await browser.close(); + } +} + +main().catch(error => { + console.error('Crawler failed:', error); + process.exit(1); +}); diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 90cac9c..26f0e51 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -75,7 +75,7 @@ const Tickets = React.lazy(() => import('./pages/Tickets')); // Import Tickets p const HelpGuide = React.lazy(() => import('./pages/HelpGuide')); // Import Platform Guide page const HelpTicketing = React.lazy(() => import('./pages/HelpTicketing')); // Import Help page for ticketing const HelpApiDocs = React.lazy(() => import('./pages/HelpApiDocs')); // Import API documentation page -const HelpPluginDocs = React.lazy(() => import('./pages/help/HelpPluginDocs')); // Import Plugin documentation page +const HelpAutomationDocs = React.lazy(() => import('./pages/help/HelpAutomationDocs')); // Import Automation documentation page const HelpEmailSettings = React.lazy(() => import('./pages/HelpEmailSettings')); // Import Email settings help page // Import new help pages @@ -90,7 +90,7 @@ const HelpTimeBlocks = React.lazy(() => import('./pages/HelpTimeBlocks')); const HelpMessages = React.lazy(() => import('./pages/help/HelpMessages')); const HelpPayments = React.lazy(() => import('./pages/help/HelpPayments')); const HelpContracts = React.lazy(() => import('./pages/help/HelpContracts')); -const HelpPlugins = React.lazy(() => import('./pages/help/HelpPlugins')); +const HelpAutomations = React.lazy(() => import('./pages/help/HelpAutomations')); const HelpSettingsGeneral = React.lazy(() => import('./pages/help/HelpSettingsGeneral')); const HelpSettingsResourceTypes = React.lazy(() => import('./pages/help/HelpSettingsResourceTypes')); const HelpSettingsBooking = React.lazy(() => import('./pages/help/HelpSettingsBooking')); @@ -104,10 +104,10 @@ const HelpSettingsQuota = React.lazy(() => import('./pages/help/HelpSettingsQuot const HelpComprehensive = React.lazy(() => import('./pages/help/HelpComprehensive')); const StaffHelp = React.lazy(() => import('./pages/help/StaffHelp')); const PlatformSupport = React.lazy(() => import('./pages/PlatformSupport')); // Import Platform Support page (for businesses to contact SmoothSchedule) -const PluginMarketplace = React.lazy(() => import('./pages/PluginMarketplace')); // Import Plugin Marketplace page -const MyPlugins = React.lazy(() => import('./pages/MyPlugins')); // Import My Plugins page -const CreatePlugin = React.lazy(() => import('./pages/CreatePlugin')); // Import Create Plugin page -const Tasks = React.lazy(() => import('./pages/Tasks')); // Import Tasks page for scheduled plugin executions +const AutomationMarketplace = React.lazy(() => import('./pages/AutomationMarketplace')); // Import Automation Marketplace page +const MyAutomations = React.lazy(() => import('./pages/MyAutomations')); // Import My Automations page +const CreateAutomation = React.lazy(() => import('./pages/CreateAutomation')); // Import Create Automation page +const Tasks = React.lazy(() => import('./pages/Tasks')); // Import Tasks page for scheduled automation executions const SystemEmailTemplates = React.lazy(() => import('./pages/settings/SystemEmailTemplates')); // System email templates (Puck-based) const Contracts = React.lazy(() => import('./pages/Contracts')); // Import Contracts page const ContractTemplates = React.lazy(() => import('./pages/ContractTemplates')); // Import Contract Templates page @@ -518,7 +518,7 @@ const AppContent: React.FC = () => { } /> } /> } /> - } /> + } /> } /> {user.role === 'superuser' && ( <> @@ -738,7 +738,7 @@ const AppContent: React.FC = () => { } /> } /> } /> - } /> + } /> } /> {/* New help pages */} } /> @@ -752,7 +752,7 @@ const AppContent: React.FC = () => { } /> } /> } /> - } /> + } /> } /> } /> } /> @@ -764,30 +764,30 @@ const AppContent: React.FC = () => { } /> } /> + ) : ( ) } /> + ) : ( ) } /> + ) : ( ) diff --git a/frontend/src/components/CreateTaskModal.tsx b/frontend/src/components/CreateTaskModal.tsx index 9009851..64d34f1 100644 --- a/frontend/src/components/CreateTaskModal.tsx +++ b/frontend/src/components/CreateTaskModal.tsx @@ -12,7 +12,7 @@ import { } from '../constants/schedulePresets'; import { ErrorMessage } from './ui'; -interface PluginInstallation { +interface AutomationInstallation { id: string; template: number; template_name: string; @@ -42,7 +42,7 @@ type TaskType = 'scheduled' | 'event'; const CreateTaskModal: React.FC = ({ isOpen, onClose, onSuccess }) => { const queryClient = useQueryClient(); const [step, setStep] = useState(1); - const [selectedPlugin, setSelectedPlugin] = useState(null); + const [selectedAutomation, setSelectedAutomation] = useState(null); const [taskName, setTaskName] = useState(''); const [description, setDescription] = useState(''); @@ -68,20 +68,20 @@ const CreateTaskModal: React.FC = ({ isOpen, onClose, onSu const [isSubmitting, setIsSubmitting] = useState(false); const [error, setError] = useState(''); - // Fetch available plugins - const { data: plugins = [], isLoading: pluginsLoading } = useQuery({ - queryKey: ['plugin-installations'], + // Fetch available automations + const { data: automations = [], isLoading: automationsLoading } = useQuery({ + queryKey: ['automation-installations'], queryFn: async () => { - const { data } = await axios.get('/plugin-installations/'); - // Filter out plugins that already have scheduled tasks - return data.filter((p: PluginInstallation) => !p.scheduled_task); + const { data } = await axios.get('/automation-installations/'); + // Filter out automations that already have scheduled tasks + return data.filter((p: AutomationInstallation) => !p.scheduled_task); }, enabled: isOpen, }); const handleClose = () => { setStep(1); - setSelectedPlugin(null); + setSelectedAutomation(null); setTaskName(''); setDescription(''); setError(''); @@ -97,9 +97,9 @@ const CreateTaskModal: React.FC = ({ isOpen, onClose, onSu onClose(); }; - const handlePluginSelect = (plugin: PluginInstallation) => { - setSelectedPlugin(plugin); - setTaskName(`${plugin.template_name} - Scheduled Task`); + const handleAutomationSelect = (automation: AutomationInstallation) => { + setSelectedAutomation(automation); + setTaskName(`${automation.template_name} - Scheduled Task`); setStep(2); }; @@ -117,33 +117,33 @@ const CreateTaskModal: React.FC = ({ isOpen, onClose, onSu const showOffset = !['on_complete', 'on_cancel'].includes(selectedTrigger); const handleSubmit = async () => { - if (!selectedPlugin) return; + if (!selectedAutomation) return; setIsSubmitting(true); setError(''); try { if (taskType === 'event') { - // Create global event plugin (applies to all events) + // Create global event automation (applies to all events) const payload = { - plugin_installation: selectedPlugin.id, + automation_installation: selectedAutomation.id, trigger: selectedTrigger, offset_minutes: selectedOffset, is_active: true, apply_to_existing: applyToExisting, }; - await axios.post('/global-event-plugins/', payload); - queryClient.invalidateQueries({ queryKey: ['global-event-plugins'] }); - toast.success(applyToExisting ? 'Plugin attached to all events' : 'Plugin will apply to future events'); + await axios.post('/global-event-automations/', payload); + queryClient.invalidateQueries({ queryKey: ['global-event-automations'] }); + toast.success(applyToExisting ? 'Automation attached to all events' : 'Automation will apply to future events'); } else { // Create scheduled task let payload: any = { name: taskName, description, - plugin_name: selectedPlugin.template_slug, + automation_name: selectedAutomation.template_slug, status: 'ACTIVE', - plugin_config: selectedPlugin.config_values || {}, + automation_config: selectedAutomation.config_values || {}, }; if (scheduleMode === 'onetime') { @@ -202,7 +202,7 @@ const CreateTaskModal: React.FC = ({ isOpen, onClose, onSu
= 1 ? 'bg-blue-600 text-white' : 'bg-gray-200 dark:bg-gray-700 text-gray-500'}`}> 1
- Select Plugin + Select Automation
= 2 ? 'text-blue-600 dark:text-blue-400' : 'text-gray-400'}`}> @@ -216,36 +216,36 @@ const CreateTaskModal: React.FC = ({ isOpen, onClose, onSu {/* Content */}
- {/* Step 1: Select Plugin */} + {/* Step 1: Select Automation */} {step === 1 && (

- Select a plugin to schedule for automatic execution. + Select an automation to schedule for automatic execution.

- {pluginsLoading ? ( + {automationsLoading ? (
- ) : plugins.length === 0 ? ( + ) : automations.length === 0 ? (
-

No available plugins

+

No available automations

- Install plugins from the Marketplace first, or all your plugins are already scheduled. + Install automations from the Marketplace first, or all your automations are already scheduled.

) : (
- {plugins.map((plugin) => ( + {automations.map((automation) => (
{/* Existing automations */} - {eventPlugins.length > 0 && ( + {eventAutomations.length > 0 && (
- {eventPlugins.map((ep) => ( + {eventAutomations.map((ep) => (
= ({ eventId, compact =

- {ep.plugin_name} + {ep.automation_name}

@@ -229,20 +229,20 @@ const EventAutomations: React.FC = ({ eventId, compact = {/* Add automation form */} {showAddForm && (
- {/* Plugin selector */} + {/* Automation selector */}
@@ -302,7 +302,7 @@ const EventAutomations: React.FC = ({ eventId, compact = )} {/* Preview */} - {selectedPlugin && ( + {selectedAutomation && (
Will run: {selectedTrigger === 'on_complete' ? 'When completed' : @@ -328,7 +328,7 @@ const EventAutomations: React.FC = ({ eventId, compact = ) : (

- Install plugins from the Marketplace to use automations + Install automations from the Marketplace to use them

)}
diff --git a/frontend/src/components/FloatingHelpButton.tsx b/frontend/src/components/FloatingHelpButton.tsx index 6c76d13..c1a7e68 100644 --- a/frontend/src/components/FloatingHelpButton.tsx +++ b/frontend/src/components/FloatingHelpButton.tsx @@ -27,10 +27,10 @@ const routeToHelpPath: Record = { '/payments': '/help/payments', '/contracts': '/help/contracts', '/contracts/templates': '/help/contracts', - '/plugins': '/help/plugins', - '/plugins/marketplace': '/help/plugins', - '/plugins/my-plugins': '/help/plugins', - '/plugins/create': '/help/plugins/docs', + '/automations': '/help/automations', + '/automations/marketplace': '/help/automations', + '/automations/my-automations': '/help/automations', + '/automations/create': '/help/automations/docs', '/settings': '/help/settings/general', '/settings/general': '/help/settings/general', '/settings/resource-types': '/help/settings/resource-types', diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index b53fd1f..73b97e5 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -130,7 +130,7 @@ const Sidebar: React.FC = ({ business, user, isCollapsed, toggleCo icon={Clock} label={t('nav.tasks', 'Tasks')} isCollapsed={isCollapsed} - locked={!canUse('plugins') || !canUse('tasks')} + locked={!canUse('automations') || !canUse('tasks')} badgeElement={} /> )} @@ -257,15 +257,15 @@ const Sidebar: React.FC = ({ business, user, isCollapsed, toggleCo )} - {/* Extend Section - Plugins */} + {/* Extend Section - Automations */} {canViewAdminPages && ( } /> diff --git a/frontend/src/components/dashboard/CapacityWidget.tsx b/frontend/src/components/dashboard/CapacityWidget.tsx index ae1c41b..067e4c9 100644 --- a/frontend/src/components/dashboard/CapacityWidget.tsx +++ b/frontend/src/components/dashboard/CapacityWidget.tsx @@ -108,7 +108,7 @@ const CapacityWidget: React.FC = ({

{t('dashboard.noResourcesConfigured')}

) : ( -
+
{capacityData.resources.map((resource) => (
= ({
- + {type === 'bar' ? ( diff --git a/frontend/src/components/dashboard/CustomerBreakdownWidget.tsx b/frontend/src/components/dashboard/CustomerBreakdownWidget.tsx index bd7cce9..ffa1525 100644 --- a/frontend/src/components/dashboard/CustomerBreakdownWidget.tsx +++ b/frontend/src/components/dashboard/CustomerBreakdownWidget.tsx @@ -58,7 +58,7 @@ const CustomerBreakdownWidget: React.FC = ({
{/* Pie Chart */}
- + { +const AutomationShowcase: React.FC = () => { const { t } = useTranslation(); const [activeTab, setActiveTab] = useState(0); const [viewMode, setViewMode] = useState<'marketplace' | 'code'>('marketplace'); @@ -174,7 +174,7 @@ const PluginShowcase: React.FC = () => { // Code View )} @@ -194,4 +194,4 @@ const PluginShowcase: React.FC = () => { ); }; -export default PluginShowcase; +export default AutomationShowcase; diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index 545c8f2..d075431 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -122,11 +122,11 @@ "platformGuide": "Platform Guide", "ticketingHelp": "Ticketing System", "apiDocs": "API Docs", - "pluginDocs": "Plugin Docs", + "automationDocs": "Automation Docs", "contactSupport": "Contact Support", - "plugins": "Plugins", - "pluginMarketplace": "Marketplace", - "myPlugins": "My Plugins", + "automations": "Automations", + "automationMarketplace": "Marketplace", + "myAutomations": "My Automations", "expandSidebar": "Expand sidebar", "collapseSidebar": "Collapse sidebar", "smoothSchedule": "Smooth Schedule", @@ -1481,10 +1481,10 @@ "customization": "Customization", "customDomains": "Custom Domains", "whiteLabelling": "White Labelling", - "pluginsAutomation": "Plugins & Automation", - "usePlugins": "Use Plugins", + "automationsSection": "Automations", + "useAutomations": "Use Automations", "scheduledTasks": "Scheduled Tasks", - "createPlugins": "Create Plugins", + "createAutomations": "Create Automations", "advancedFeatures": "Advanced Features", "apiAccess": "API Access", "webhooks": "Webhooks", @@ -2178,14 +2178,14 @@ "plugins": { "badge": "Limitless Automation", "headline": "Choose from our Marketplace, or build your own.", - "subheadline": "Browse hundreds of pre-built plugins to automate your workflows instantly. Need something custom? Developers can write Python scripts to extend the platform endlessly.", + "subheadline": "Browse hundreds of pre-built automations to streamline your workflows instantly. Need something custom? Developers can write Python scripts to extend the platform endlessly.", "viewToggle": { "marketplace": "Marketplace", "developer": "Developer" }, "marketplaceCard": { "author": "by SmoothSchedule Team", - "installButton": "Install Plugin", + "installButton": "Install Automation", "usedBy": "Used by 1,200+ businesses" }, "cta": "Explore the Marketplace", @@ -2231,7 +2231,7 @@ }, "automationEngine": { "title": "Automation Engine", - "description": "Install plugins from our marketplace or build your own to automate tasks." + "description": "Install automations from our marketplace or build your own to automate tasks." }, "multiTenant": { "title": "Enterprise Security", @@ -2278,6 +2278,241 @@ "company": "FitNation" } } + }, + "privacyPolicy": { + "title": "Privacy Policy", + "lastUpdated": "Last updated: December 1, 2025", + "section1": { + "title": "1. Introduction", + "content": "Welcome to SmoothSchedule. We respect your privacy and are committed to protecting your personal data. This Privacy Policy explains how we collect, use, disclose, and safeguard your information when you use our scheduling platform and services." + }, + "section2": { + "title": "2. Information We Collect", + "subsection1": { + "title": "2.1 Information You Provide", + "intro": "We collect information you directly provide to us, including:", + "items": [ + "Account information (name, email, password, phone number)", + "Business information (business name, subdomain, industry)", + "Payment information (processed securely through third-party payment processors)", + "Customer data you input into the platform (appointments, resources, services)", + "Communications with our support team" + ] + }, + "subsection2": { + "title": "2.2 Automatically Collected Information", + "intro": "When you use our Service, we automatically collect:", + "items": [ + "Log data (IP address, browser type, device information, operating system)", + "Usage data (pages visited, features used, time spent on platform)", + "Cookie data (session cookies, preference cookies)", + "Performance and error data for service improvement" + ] + } + }, + "section3": { + "title": "3. How We Use Your Information", + "intro": "We use the collected information for:", + "items": [ + "Providing and maintaining the Service", + "Processing your transactions and managing subscriptions", + "Sending you service updates, security alerts, and administrative messages", + "Responding to your inquiries and providing customer support", + "Improving and optimizing our Service", + "Detecting and preventing fraud and security issues", + "Complying with legal obligations", + "Sending marketing communications (with your consent)" + ] + }, + "section4": { + "title": "4. Data Sharing and Disclosure", + "subsection1": { + "title": "4.1 We Share Data With:", + "items": [ + "Service Providers: Third-party vendors who help us provide the Service (hosting, payment processing, analytics)", + "Business Transfers: In connection with any merger, sale, or acquisition of all or part of our company", + "Legal Requirements: When required by law, court order, or legal process", + "Protection of Rights: To protect our rights, property, or safety, or that of our users" + ] + }, + "subsection2": { + "title": "4.2 We Do NOT:", + "items": [ + "Sell your personal data to third parties", + "Share your data for third-party marketing without consent", + "Access your customer data except for support or technical purposes" + ] + } + }, + "section5": { + "title": "5. Data Security", + "intro": "We implement industry-standard security measures to protect your data:", + "items": [ + "Encryption of data in transit (TLS/SSL)", + "Encryption of sensitive data at rest", + "Regular security audits and vulnerability assessments", + "Access controls and authentication mechanisms", + "Regular backups and disaster recovery procedures" + ], + "disclaimer": "However, no method of transmission over the Internet is 100% secure. While we strive to protect your data, we cannot guarantee absolute security." + }, + "section6": { + "title": "6. Data Retention", + "content": "We retain your personal data for as long as necessary to provide the Service and fulfill the purposes described in this policy. When you cancel your account, we retain your data for 30 days to allow for account reactivation. After this period, your personal data may be anonymized and aggregated for internal analytics and service improvement purposes. Anonymized data cannot be used to identify you personally and cannot be retrieved or attributed to any person or account. We may also retain certain data if required for legal or legitimate business purposes." + }, + "section7": { + "title": "7. Your Rights and Choices", + "intro": "Depending on your location, you may have the following rights:", + "items": [ + "Access: Request a copy of your personal data", + "Correction: Update or correct inaccurate data", + "Deletion: Request deletion of your personal data", + "Portability: Receive your data in a portable format", + "Objection: Object to certain data processing activities", + "Restriction: Request restriction of data processing", + "Withdraw Consent: Withdraw previously given consent" + ], + "contact": "To exercise these rights, please contact us at privacy@smoothschedule.com." + }, + "section8": { + "title": "8. Cookies and Tracking", + "intro": "We use cookies and similar tracking technologies to:", + "items": [ + "Maintain your session and keep you logged in", + "Remember your preferences and settings", + "Analyze usage patterns and improve our Service", + "Provide personalized content and features" + ], + "disclaimer": "You can control cookies through your browser settings, but disabling cookies may affect your ability to use certain features of the Service." + }, + "section9": { + "title": "9. Third-Party Services", + "content": "Our Service may contain links to third-party websites or integrate with third-party services (OAuth providers, payment processors). We are not responsible for the privacy practices of these third parties. We encourage you to review their privacy policies before providing any personal information." + }, + "section10": { + "title": "10. Children's Privacy", + "content": "Our Service is not intended for children under 13 years of age. We do not knowingly collect personal information from children under 13. If you believe we have collected data from a child under 13, please contact us immediately so we can delete it." + }, + "section11": { + "title": "11. International Data Transfers", + "content": "Your information may be transferred to and processed in countries other than your country of residence. These countries may have different data protection laws. We ensure appropriate safeguards are in place to protect your data in accordance with this Privacy Policy." + }, + "section12": { + "title": "12. California Privacy Rights", + "content": "If you are a California resident, you have additional rights under the California Consumer Privacy Act (CCPA), including the right to know what personal information we collect, the right to delete your information, and the right to opt-out of the sale of your information (which we do not do)." + }, + "section13": { + "title": "13. GDPR Compliance", + "content": "If you are in the European Economic Area (EEA), we process your personal data based on legal grounds such as consent, contract performance, legal obligations, or legitimate interests. You have rights under the General Data Protection Regulation (GDPR) including the right to lodge a complaint with a supervisory authority." + }, + "section14": { + "title": "14. Changes to This Privacy Policy", + "content": "We may update this Privacy Policy from time to time. We will notify you of material changes by posting the new policy on this page and updating the \"Last updated\" date. We encourage you to review this Privacy Policy periodically." + }, + "section15": { + "title": "15. Contact Us", + "intro": "If you have any questions about this Privacy Policy or our data practices, please contact us:", + "emailLabel": "Email:", + "email": "privacy@smoothschedule.com", + "dpoLabel": "Data Protection Officer:", + "dpo": "dpo@smoothschedule.com", + "websiteLabel": "Website:", + "website": "https://smoothschedule.com/contact" + } + }, + "termsOfService": { + "title": "Terms of Service", + "lastUpdated": "Last updated: December 1, 2025", + "sections": { + "acceptanceOfTerms": { + "title": "1. Acceptance of Terms", + "content": "By accessing and using SmoothSchedule (\"the Service\"), you accept and agree to be bound by the terms and provision of this agreement. If you do not agree to these Terms of Service, please do not use the Service." + }, + "descriptionOfService": { + "title": "2. Description of Service", + "content": "SmoothSchedule is a scheduling platform that enables businesses to manage appointments, resources, services, and customer interactions. The Service is provided on a subscription basis with various pricing tiers." + }, + "userAccounts": { + "title": "3. User Accounts", + "intro": "To use the Service, you must:", + "requirements": { + "accurate": "Create an account with accurate and complete information", + "security": "Maintain the security of your account credentials", + "notify": "Notify us immediately of any unauthorized access", + "responsible": "Be responsible for all activities under your account" + } + }, + "acceptableUse": { + "title": "4. Acceptable Use", + "intro": "You agree not to use the Service to:", + "prohibitions": { + "laws": "Violate any applicable laws or regulations", + "ip": "Infringe on intellectual property rights", + "malicious": "Transmit malicious code or interfere with the Service", + "unauthorized": "Attempt to gain unauthorized access to any part of the Service", + "fraudulent": "Use the Service for any fraudulent or illegal purpose" + } + }, + "subscriptionsAndPayments": { + "title": "5. Subscriptions and Payments", + "intro": "Subscription terms:", + "terms": { + "billing": "Subscriptions are billed in advance on a recurring basis", + "cancel": "You may cancel your subscription at any time", + "refunds": "No refunds are provided for partial subscription periods", + "pricing": "We reserve the right to change pricing with 30 days notice", + "failed": "Failed payments may result in service suspension" + } + }, + "trialPeriod": { + "title": "6. Trial Period", + "content": "We may offer a free trial period. At the end of the trial, your subscription will automatically convert to a paid plan unless you cancel. Trial terms may vary and are subject to change." + }, + "dataAndPrivacy": { + "title": "7. Data and Privacy", + "content": "Your use of the Service is also governed by our Privacy Policy. We collect, use, and protect your data as described in that policy. You retain ownership of all data you input into the Service." + }, + "serviceAvailability": { + "title": "8. Service Availability", + "content": "While we strive for 99.9% uptime, we do not guarantee uninterrupted access to the Service. We may perform maintenance, updates, or modifications that temporarily affect availability. We are not liable for any downtime or service interruptions." + }, + "intellectualProperty": { + "title": "9. Intellectual Property", + "content": "The Service, including all content, features, and functionality, is owned by SmoothSchedule and protected by copyright, trademark, and other intellectual property laws. You may not copy, modify, distribute, or create derivative works without our express written consent." + }, + "warrantyDisclaimer": { + "title": "10. Warranty Disclaimer", + "content": "THE SERVICE IS PROVIDED \"AS IS\" AND \"AS AVAILABLE\" WITHOUT WARRANTIES OF ANY KIND, EXPRESS OR IMPLIED. WE DISCLAIM ALL WARRANTIES, INCLUDING MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT." + }, + "limitationOfLiability": { + "title": "11. Limitation of Liability", + "content": "IN NO EVENT SHALL SMOOTHSCHEDULE BE LIABLE FOR ANY INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, OR PUNITIVE DAMAGES, OR ANY LOSS OF PROFITS OR REVENUES. OUR TOTAL LIABILITY SHALL NOT EXCEED THE AMOUNT YOU PAID US IN THE TWELVE MONTHS PRECEDING THE CLAIM." + }, + "indemnification": { + "title": "12. Indemnification", + "content": "You agree to indemnify and hold harmless SmoothSchedule and its officers, directors, employees, and agents from any claims, damages, losses, or expenses arising from your use of the Service or violation of these Terms." + }, + "termination": { + "title": "10. Termination", + "content": "We may suspend or terminate your access to the Service at any time for any reason, including violation of these Terms. Upon termination, your right to use the Service ceases immediately. Provisions that by their nature should survive termination will remain in effect." + }, + "governingLaw": { + "title": "14. Governing Law", + "content": "These Terms shall be governed by and construed in accordance with the laws of the State of Delaware, without regard to its conflict of law provisions. Any disputes shall be resolved in the courts of Delaware." + }, + "changesToTerms": { + "title": "15. Changes to Terms", + "content": "We may update these Terms from time to time. We will notify you of material changes by posting the new Terms on this page and updating the \"Last updated\" date. Your continued use of the Service after changes constitutes acceptance of the new Terms." + }, + "contactUs": { + "title": "16. Contact Us", + "intro": "If you have any questions about these Terms of Service, please contact us:", + "emailLabel": "Email:", + "email": "legal@smoothschedule.com", + "websiteLabel": "Website:", + "website": "https://smoothschedule.com/contact" + } + } } }, "trial": { @@ -2487,241 +2722,6 @@ "paymentProcessing": "Payment processing", "prioritySupport": "Priority support" } - }, - "privacyPolicy": { - "title": "Privacy Policy", - "lastUpdated": "Last updated: December 1, 2025", - "section1": { - "title": "1. Introduction", - "content": "Welcome to SmoothSchedule. We respect your privacy and are committed to protecting your personal data. This Privacy Policy explains how we collect, use, disclose, and safeguard your information when you use our scheduling platform and services." - }, - "section2": { - "title": "2. Information We Collect", - "subsection1": { - "title": "2.1 Information You Provide", - "intro": "We collect information you directly provide to us, including:", - "items": [ - "Account information (name, email, password, phone number)", - "Business information (business name, subdomain, industry)", - "Payment information (processed securely through third-party payment processors)", - "Customer data you input into the platform (appointments, resources, services)", - "Communications with our support team" - ] - }, - "subsection2": { - "title": "2.2 Automatically Collected Information", - "intro": "When you use our Service, we automatically collect:", - "items": [ - "Log data (IP address, browser type, device information, operating system)", - "Usage data (pages visited, features used, time spent on platform)", - "Cookie data (session cookies, preference cookies)", - "Performance and error data for service improvement" - ] - } - }, - "section3": { - "title": "3. How We Use Your Information", - "intro": "We use the collected information for:", - "items": [ - "Providing and maintaining the Service", - "Processing your transactions and managing subscriptions", - "Sending you service updates, security alerts, and administrative messages", - "Responding to your inquiries and providing customer support", - "Improving and optimizing our Service", - "Detecting and preventing fraud and security issues", - "Complying with legal obligations", - "Sending marketing communications (with your consent)" - ] - }, - "section4": { - "title": "4. Data Sharing and Disclosure", - "subsection1": { - "title": "4.1 We Share Data With:", - "items": [ - "Service Providers: Third-party vendors who help us provide the Service (hosting, payment processing, analytics)", - "Business Transfers: In connection with any merger, sale, or acquisition of all or part of our company", - "Legal Requirements: When required by law, court order, or legal process", - "Protection of Rights: To protect our rights, property, or safety, or that of our users" - ] - }, - "subsection2": { - "title": "4.2 We Do NOT:", - "items": [ - "Sell your personal data to third parties", - "Share your data for third-party marketing without consent", - "Access your customer data except for support or technical purposes" - ] - } - }, - "section5": { - "title": "5. Data Security", - "intro": "We implement industry-standard security measures to protect your data:", - "items": [ - "Encryption of data in transit (TLS/SSL)", - "Encryption of sensitive data at rest", - "Regular security audits and vulnerability assessments", - "Access controls and authentication mechanisms", - "Regular backups and disaster recovery procedures" - ], - "disclaimer": "However, no method of transmission over the Internet is 100% secure. While we strive to protect your data, we cannot guarantee absolute security." - }, - "section6": { - "title": "6. Data Retention", - "content": "We retain your personal data for as long as necessary to provide the Service and fulfill the purposes described in this policy. When you cancel your account, we retain your data for 30 days to allow for account reactivation. After this period, your personal data may be anonymized and aggregated for internal analytics and service improvement purposes. Anonymized data cannot be used to identify you personally and cannot be retrieved or attributed to any person or account. We may also retain certain data if required for legal or legitimate business purposes." - }, - "section7": { - "title": "7. Your Rights and Choices", - "intro": "Depending on your location, you may have the following rights:", - "items": [ - "Access: Request a copy of your personal data", - "Correction: Update or correct inaccurate data", - "Deletion: Request deletion of your personal data", - "Portability: Receive your data in a portable format", - "Objection: Object to certain data processing activities", - "Restriction: Request restriction of data processing", - "Withdraw Consent: Withdraw previously given consent" - ], - "contact": "To exercise these rights, please contact us at privacy@smoothschedule.com." - }, - "section8": { - "title": "8. Cookies and Tracking", - "intro": "We use cookies and similar tracking technologies to:", - "items": [ - "Maintain your session and keep you logged in", - "Remember your preferences and settings", - "Analyze usage patterns and improve our Service", - "Provide personalized content and features" - ], - "disclaimer": "You can control cookies through your browser settings, but disabling cookies may affect your ability to use certain features of the Service." - }, - "section9": { - "title": "9. Third-Party Services", - "content": "Our Service may contain links to third-party websites or integrate with third-party services (OAuth providers, payment processors). We are not responsible for the privacy practices of these third parties. We encourage you to review their privacy policies before providing any personal information." - }, - "section10": { - "title": "10. Children's Privacy", - "content": "Our Service is not intended for children under 13 years of age. We do not knowingly collect personal information from children under 13. If you believe we have collected data from a child under 13, please contact us immediately so we can delete it." - }, - "section11": { - "title": "11. International Data Transfers", - "content": "Your information may be transferred to and processed in countries other than your country of residence. These countries may have different data protection laws. We ensure appropriate safeguards are in place to protect your data in accordance with this Privacy Policy." - }, - "section12": { - "title": "12. California Privacy Rights", - "content": "If you are a California resident, you have additional rights under the California Consumer Privacy Act (CCPA), including the right to know what personal information we collect, the right to delete your information, and the right to opt-out of the sale of your information (which we do not do)." - }, - "section13": { - "title": "13. GDPR Compliance", - "content": "If you are in the European Economic Area (EEA), we process your personal data based on legal grounds such as consent, contract performance, legal obligations, or legitimate interests. You have rights under the General Data Protection Regulation (GDPR) including the right to lodge a complaint with a supervisory authority." - }, - "section14": { - "title": "14. Changes to This Privacy Policy", - "content": "We may update this Privacy Policy from time to time. We will notify you of material changes by posting the new policy on this page and updating the \"Last updated\" date. We encourage you to review this Privacy Policy periodically." - }, - "section15": { - "title": "15. Contact Us", - "intro": "If you have any questions about this Privacy Policy or our data practices, please contact us:", - "emailLabel": "Email:", - "email": "privacy@smoothschedule.com", - "dpoLabel": "Data Protection Officer:", - "dpo": "dpo@smoothschedule.com", - "websiteLabel": "Website:", - "website": "https://smoothschedule.com/contact" - } - }, - "termsOfService": { - "title": "Terms of Service", - "lastUpdated": "Last updated: December 1, 2025", - "sections": { - "acceptanceOfTerms": { - "title": "1. Acceptance of Terms", - "content": "By accessing and using SmoothSchedule (\"the Service\"), you accept and agree to be bound by the terms and provision of this agreement. If you do not agree to these Terms of Service, please do not use the Service." - }, - "descriptionOfService": { - "title": "2. Description of Service", - "content": "SmoothSchedule is a scheduling platform that enables businesses to manage appointments, resources, services, and customer interactions. The Service is provided on a subscription basis with various pricing tiers." - }, - "userAccounts": { - "title": "3. User Accounts", - "intro": "To use the Service, you must:", - "requirements": { - "accurate": "Create an account with accurate and complete information", - "security": "Maintain the security of your account credentials", - "notify": "Notify us immediately of any unauthorized access", - "responsible": "Be responsible for all activities under your account" - } - }, - "acceptableUse": { - "title": "4. Acceptable Use", - "intro": "You agree not to use the Service to:", - "prohibitions": { - "laws": "Violate any applicable laws or regulations", - "ip": "Infringe on intellectual property rights", - "malicious": "Transmit malicious code or interfere with the Service", - "unauthorized": "Attempt to gain unauthorized access to any part of the Service", - "fraudulent": "Use the Service for any fraudulent or illegal purpose" - } - }, - "subscriptionsAndPayments": { - "title": "5. Subscriptions and Payments", - "intro": "Subscription terms:", - "terms": { - "billing": "Subscriptions are billed in advance on a recurring basis", - "cancel": "You may cancel your subscription at any time", - "refunds": "No refunds are provided for partial subscription periods", - "pricing": "We reserve the right to change pricing with 30 days notice", - "failed": "Failed payments may result in service suspension" - } - }, - "trialPeriod": { - "title": "6. Trial Period", - "content": "We may offer a free trial period. At the end of the trial, your subscription will automatically convert to a paid plan unless you cancel. Trial terms may vary and are subject to change." - }, - "dataAndPrivacy": { - "title": "7. Data and Privacy", - "content": "Your use of the Service is also governed by our Privacy Policy. We collect, use, and protect your data as described in that policy. You retain ownership of all data you input into the Service." - }, - "serviceAvailability": { - "title": "8. Service Availability", - "content": "While we strive for 99.9% uptime, we do not guarantee uninterrupted access to the Service. We may perform maintenance, updates, or modifications that temporarily affect availability. We are not liable for any downtime or service interruptions." - }, - "intellectualProperty": { - "title": "9. Intellectual Property", - "content": "The Service, including all software, designs, text, graphics, and other content, is owned by SmoothSchedule and protected by copyright, trademark, and other intellectual property laws. You may not copy, modify, distribute, or create derivative works without our express written permission." - }, - "termination": { - "title": "10. Termination", - "content": "We may terminate or suspend your account and access to the Service at any time, with or without cause, with or without notice. Upon termination, your right to use the Service will immediately cease. We will retain your data for 30 days after termination, after which it may be permanently deleted." - }, - "limitationOfLiability": { - "title": "11. Limitation of Liability", - "content": "To the maximum extent permitted by law, SmoothSchedule shall not be liable for any indirect, incidental, special, consequential, or punitive damages, or any loss of profits or revenues, whether incurred directly or indirectly, or any loss of data, use, goodwill, or other intangible losses resulting from your use of the Service." - }, - "warrantyDisclaimer": { - "title": "12. Warranty Disclaimer", - "content": "The Service is provided \"as is\" and \"as available\" without warranties of any kind, either express or implied, including but not limited to implied warranties of merchantability, fitness for a particular purpose, or non-infringement." - }, - "indemnification": { - "title": "13. Indemnification", - "content": "You agree to indemnify and hold harmless SmoothSchedule, its officers, directors, employees, and agents from any claims, damages, losses, liabilities, and expenses (including legal fees) arising from your use of the Service or violation of these Terms." - }, - "changesToTerms": { - "title": "14. Changes to Terms", - "content": "We reserve the right to modify these Terms at any time. We will notify you of material changes via email or through the Service. Your continued use of the Service after such changes constitutes acceptance of the new Terms." - }, - "governingLaw": { - "title": "15. Governing Law", - "content": "These Terms shall be governed by and construed in accordance with the laws of the jurisdiction in which SmoothSchedule is registered, without regard to its conflict of law provisions." - }, - "contactUs": { - "title": "16. Contact Us", - "intro": "If you have any questions about these Terms of Service, please contact us at:", - "email": "Email:", - "emailAddress": "legal@smoothschedule.com", - "website": "Website:", - "websiteUrl": "https://smoothschedule.com/contact" - } - } } }, "timeBlocks": { @@ -3121,9 +3121,9 @@ "timeBlocksDocumentationDesc": "Complete guide to creating, managing, and visualizing time blocks" }, "plugins": { - "title": "Plugins", - "description": "Plugins extend SmoothSchedule with custom automation and integrations. Browse the marketplace for pre-built plugins or create your own using our scripting language.", - "whatPluginsCanDo": "What Plugins Can Do", + "title": "Automations", + "description": "Automations extend SmoothSchedule with custom workflows and integrations. Browse the marketplace for pre-built automations or create your own using our scripting language.", + "whatPluginsCanDo": "What Automations Can Do", "sendEmailsCapability": "Send Emails:", "sendEmailsDesc": "Automated reminders, confirmations, and follow-ups", "webhooksCapability": "Webhooks:", @@ -3132,20 +3132,20 @@ "reportsDesc": "Generate and email business reports on a schedule", "cleanupCapability": "Cleanup:", "cleanupDesc": "Automatically archive old data or manage records", - "pluginTypes": "Plugin Types", - "marketplacePlugins": "Marketplace Plugins", - "marketplacePluginsDesc": "Pre-built plugins available to install immediately. Browse, install, and configure with a few clicks.", - "customPlugins": "Custom Plugins", - "customPluginsDesc": "Create your own plugins using our scripting language. Full control over logic and triggers.", + "pluginTypes": "Automation Types", + "marketplacePlugins": "Marketplace Automations", + "marketplacePluginsDesc": "Pre-built automations available to install immediately. Browse, install, and configure with a few clicks.", + "customPlugins": "Custom Automations", + "customPluginsDesc": "Create your own automations using our scripting language. Full control over logic and triggers.", "triggers": "Triggers", - "triggersDesc": "Plugins can be triggered in various ways:", + "triggersDesc": "Automations can be triggered in various ways:", "beforeEventTrigger": "Before Event", "atStartTrigger": "At Start", "afterEndTrigger": "After End", "onStatusChangeTrigger": "On Status Change", "learnMore": "Learn More", - "pluginDocumentation": "Plugin Documentation", - "pluginDocumentationDesc": "Complete guide to creating and using plugins, including API reference and examples" + "pluginDocumentation": "Automation Documentation", + "pluginDocumentationDesc": "Complete guide to creating and using automations, including API reference and examples" }, "contracts": { "title": "Contracts", diff --git a/frontend/src/pages/PluginMarketplace.tsx b/frontend/src/pages/AutomationMarketplace.tsx similarity index 76% rename from frontend/src/pages/PluginMarketplace.tsx rename to frontend/src/pages/AutomationMarketplace.tsx index 695f716..6b85b71 100644 --- a/frontend/src/pages/PluginMarketplace.tsx +++ b/frontend/src/pages/AutomationMarketplace.tsx @@ -28,12 +28,12 @@ import { import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism'; import api from '../api/client'; -import { PluginTemplate, PluginCategory } from '../types'; +import { AutomationTemplate, AutomationCategory } from '../types'; import { usePlanFeatures } from '../hooks/usePlanFeatures'; import { LockedSection } from '../components/UpgradePrompt'; // Category icon mapping -const categoryIcons: Record = { +const categoryIcons: Record = { EMAIL: , REPORTS: , CUSTOMER: , @@ -44,7 +44,7 @@ const categoryIcons: Record = { }; // Category colors -const categoryColors: Record = { +const categoryColors: Record = { EMAIL: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300', REPORTS: 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-300', CUSTOMER: 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300', @@ -80,29 +80,29 @@ const detectPlatformOnlyCode = (code: string): { hasPlatformCode: boolean; warni }; }; -const PluginMarketplace: React.FC = () => { +const AutomationMarketplace: React.FC = () => { const { t } = useTranslation(); const navigate = useNavigate(); const queryClient = useQueryClient(); const [searchQuery, setSearchQuery] = useState(''); - const [selectedCategory, setSelectedCategory] = useState('ALL'); - const [selectedPlugin, setSelectedPlugin] = useState(null); + const [selectedCategory, setSelectedCategory] = useState('ALL'); + const [selectedAutomation, setSelectedAutomation] = useState(null); const [showDetailsModal, setShowDetailsModal] = useState(false); const [showCode, setShowCode] = useState(false); const [isLoadingDetails, setIsLoadingDetails] = useState(false); const [showWhatsNextModal, setShowWhatsNextModal] = useState(false); - const [installedPluginId, setInstalledPluginId] = useState(null); + const [installedAutomationId, setInstalledAutomationId] = useState(null); // Check plan permissions const { canUse, isLoading: permissionsLoading } = usePlanFeatures(); - const hasPluginsFeature = canUse('plugins'); - const isLocked = !hasPluginsFeature; + const hasAutomationsFeature = canUse('automations'); + const isLocked = !hasAutomationsFeature; - // Fetch marketplace plugins - only when user has the feature - const { data: plugins = [], isLoading, error } = useQuery({ - queryKey: ['plugin-templates', 'marketplace'], + // Fetch marketplace automations - only when user has the feature + const { data: automations = [], isLoading, error } = useQuery({ + queryKey: ['automation-templates', 'marketplace'], queryFn: async () => { - const { data } = await api.get('/plugin-templates/?view=marketplace'); + const { data } = await api.get('/automation-templates/?view=marketplace'); return data.map((p: any) => ({ id: String(p.id), name: p.name, @@ -118,49 +118,49 @@ const PluginMarketplace: React.FC = () => { createdAt: p.created_at, updatedAt: p.updated_at, logoUrl: p.logo_url, - pluginCode: p.plugin_code, + automationCode: p.automation_code, })); }, - // Don't fetch if user doesn't have the plugins feature - enabled: hasPluginsFeature && !permissionsLoading, + // Don't fetch if user doesn't have the automations feature + enabled: hasAutomationsFeature && !permissionsLoading, }); - // Fetch installed plugins to check which are already installed - const { data: installedPlugins = [] } = useQuery<{ template: number }[]>({ - queryKey: ['plugin-installations'], + // Fetch installed automations to check which are already installed + const { data: installedAutomations = [] } = useQuery<{ template: number }[]>({ + queryKey: ['automation-installations'], queryFn: async () => { - const { data } = await api.get('/plugin-installations/'); + const { data } = await api.get('/automation-installations/'); return data; }, - // Don't fetch if user doesn't have the plugins feature - enabled: hasPluginsFeature && !permissionsLoading, + // Don't fetch if user doesn't have the automations feature + enabled: hasAutomationsFeature && !permissionsLoading, }); // Create a set of installed template IDs for quick lookup const installedTemplateIds = useMemo(() => { - return new Set(installedPlugins.map(p => String(p.template))); - }, [installedPlugins]); + return new Set(installedAutomations.map(p => String(p.template))); + }, [installedAutomations]); - // Install plugin mutation + // Install automation mutation const installMutation = useMutation({ mutationFn: async (templateId: string) => { - const { data } = await api.post('/plugin-installations/', { + const { data } = await api.post('/automation-installations/', { template: templateId, }); return data; }, onSuccess: (data) => { - queryClient.invalidateQueries({ queryKey: ['plugin-installations'] }); + queryClient.invalidateQueries({ queryKey: ['automation-installations'] }); setShowDetailsModal(false); // Show What's Next modal - setInstalledPluginId(data.id); + setInstalledAutomationId(data.id); setShowWhatsNextModal(true); }, }); - // Filter plugins - const filteredPlugins = useMemo(() => { - let result = plugins; + // Filter automations + const filteredAutomations = useMemo(() => { + let result = automations; // Filter by category if (selectedCategory !== 'ALL') { @@ -178,42 +178,42 @@ const PluginMarketplace: React.FC = () => { } return result; - }, [plugins, selectedCategory, searchQuery]); + }, [automations, selectedCategory, searchQuery]); // Sort: Featured first, then by rating, then by install count - const sortedPlugins = useMemo(() => { - return [...filteredPlugins].sort((a, b) => { + const sortedAutomations = useMemo(() => { + return [...filteredAutomations].sort((a, b) => { if (a.isFeatured && !b.isFeatured) return -1; if (!a.isFeatured && b.isFeatured) return 1; if (a.rating !== b.rating) return b.rating - a.rating; return b.installCount - a.installCount; }); - }, [filteredPlugins]); + }, [filteredAutomations]); - const handleInstall = async (plugin: PluginTemplate) => { - setSelectedPlugin(plugin); + const handleInstall = async (automation: AutomationTemplate) => { + setSelectedAutomation(automation); setShowDetailsModal(true); setShowCode(false); - // Fetch full plugin details including plugin_code + // Fetch full automation details including automation_code setIsLoadingDetails(true); try { - const { data } = await api.get(`/plugin-templates/${plugin.id}/`); - setSelectedPlugin({ - ...plugin, - pluginCode: data.plugin_code, + const { data } = await api.get(`/automation-templates/${automation.id}/`); + setSelectedAutomation({ + ...automation, + automationCode: data.automation_code, logoUrl: data.logo_url, }); } catch (error) { - console.error('Failed to fetch plugin details:', error); + console.error('Failed to fetch automation details:', error); } finally { setIsLoadingDetails(false); } }; const confirmInstall = () => { - if (selectedPlugin) { - installMutation.mutate(selectedPlugin.id); + if (selectedAutomation) { + installMutation.mutate(selectedAutomation.id); } }; @@ -246,17 +246,17 @@ const PluginMarketplace: React.FC = () => { const effectivelyLocked = isLocked || is403Error; return ( - +
{/* Header */}

- {t('plugins.marketplace', 'Plugin Marketplace')} + {t('automations.marketplace', 'Automation Marketplace')}

- {t('plugins.marketplaceDescription', 'Extend your business with powerful plugins')} + {t('automations.marketplaceDescription', 'Extend your business with powerful automations')}

@@ -271,7 +271,7 @@ const PluginMarketplace: React.FC = () => { type="text" value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} - placeholder={t('plugins.searchPlugins', 'Search plugins...')} + placeholder={t('automations.searchAutomations', 'Search automations...')} className="w-full pl-10 pr-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-brand-500 focus:border-brand-500" />
@@ -281,17 +281,17 @@ const PluginMarketplace: React.FC = () => {
@@ -300,7 +300,7 @@ const PluginMarketplace: React.FC = () => { {(searchQuery || selectedCategory !== 'ALL') && (
- {t('plugins.showingResults', 'Showing')} {sortedPlugins.length} {t('plugins.results', 'results')} + {t('automations.showingResults', 'Showing')} {sortedAutomations.length} {t('automations.results', 'results')} {(searchQuery || selectedCategory !== 'ALL') && (
- {/* Plugin Grid */} - {sortedPlugins.length === 0 ? ( + {/* Automation Grid */} + {sortedAutomations.length === 0 ? (

{searchQuery || selectedCategory !== 'ALL' - ? t('plugins.noPluginsFound', 'No plugins found matching your criteria') - : t('plugins.noPluginsAvailable', 'No plugins available yet')} + ? t('automations.noAutomationsFound', 'No automations found matching your criteria') + : t('automations.noAutomationsAvailable', 'No automations available yet')}

) : (
- {sortedPlugins.map((plugin) => ( + {sortedAutomations.map((automation) => (
- {/* Plugin Card Header */} + {/* Automation Card Header */}
{/* Logo */} - {plugin.logoUrl ? ( + {automation.logoUrl ? ( {`${plugin.name} ) : ( @@ -353,24 +353,24 @@ const PluginMarketplace: React.FC = () => {
- - {categoryIcons[plugin.category]} - {plugin.category} + + {categoryIcons[automation.category]} + {automation.category} - {plugin.isFeatured && ( + {automation.isFeatured && ( Featured )} - {installedTemplateIds.has(plugin.id) && ( + {installedTemplateIds.has(automation.id) && ( Installed )}
- {plugin.isVerified && ( + {automation.isVerified && ( )}
@@ -378,39 +378,39 @@ const PluginMarketplace: React.FC = () => {

- {plugin.name} + {automation.name}

- {plugin.description} + {automation.description}

{/* Stats */}
- {plugin.rating.toFixed(1)} - ({plugin.ratingCount}) + {automation.rating.toFixed(1)} + ({automation.ratingCount})
- {plugin.installCount.toLocaleString()} + {automation.installCount.toLocaleString()}
- by {plugin.author} • v{plugin.version} + by {automation.author} • v{automation.version}
- {/* Plugin Card Actions */} + {/* Automation Card Actions */}
@@ -418,18 +418,18 @@ const PluginMarketplace: React.FC = () => {
)} - {/* Plugin Details Modal */} - {showDetailsModal && selectedPlugin && ( + {/* Automation Details Modal */} + {showDetailsModal && selectedAutomation && (
{/* Modal Header */}
{/* Logo in modal header */} - {selectedPlugin.logoUrl ? ( + {selectedAutomation.logoUrl ? ( {`${selectedPlugin.name} ) : ( @@ -439,9 +439,9 @@ const PluginMarketplace: React.FC = () => { )}

- {selectedPlugin.name} + {selectedAutomation.name}

- {selectedPlugin.isVerified && ( + {selectedAutomation.isVerified && ( )}
@@ -449,7 +449,7 @@ const PluginMarketplace: React.FC = () => { - {installedTemplateIds.has(selectedPlugin.id) ? ( + {installedTemplateIds.has(selectedAutomation.id) ? ( ) : ( @@ -637,7 +637,7 @@ const PluginMarketplace: React.FC = () => { )} {/* What's Next Modal - shown after successful install */} - {showWhatsNextModal && selectedPlugin && ( + {showWhatsNextModal && selectedAutomation && (
{/* Success Header */} @@ -646,10 +646,10 @@ const PluginMarketplace: React.FC = () => {

- Plugin Installed! + Automation Installed!

- {selectedPlugin.name} is ready to use + {selectedAutomation.name} is ready to use

@@ -663,7 +663,7 @@ const PluginMarketplace: React.FC = () => {
@@ -709,7 +709,7 @@ const PluginMarketplace: React.FC = () => {
@@ -221,7 +221,7 @@ const CreatePlugin: React.FC = () => { {/* Header */}
@@ -252,16 +252,16 @@ const CreatePlugin: React.FC = () => {
{/* Left Column - Basic Info */}
- {/* Plugin Name */} + {/* Automation Name */}

- {t('plugins.basicInfo', 'Basic Information')} + {t('automations.basicInfo', 'Basic Information')}

{
{