diff --git a/dashboard/index.html b/dashboard/index.html index da91bd428..9bb71bdf7 100644 --- a/dashboard/index.html +++ b/dashboard/index.html @@ -4,7 +4,7 @@ - OpenWA + AgencyOS
diff --git a/dashboard/package-lock.json b/dashboard/package-lock.json index 3ccb62363..d180d50c8 100644 --- a/dashboard/package-lock.json +++ b/dashboard/package-lock.json @@ -8,12 +8,20 @@ "name": "dashboard", "version": "0.1.0", "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", + "@supabase/supabase-js": "^2.106.1", "@tanstack/react-query": "^5.100.10", "@tanstack/react-table": "^8.21.3", + "date-fns": "^4.4.0", "i18next": "^26.2.0", "i18next-browser-languagedetector": "^8.2.1", "lucide-react": "^1.16.0", + "openai": "^6.44.0", + "qrcode": "^1.5.4", "react": "^19.2.6", + "react-big-calendar": "^1.20.0", "react-dom": "^19.2.6", "react-i18next": "^17.0.8", "react-router-dom": "^7.15.1", @@ -22,7 +30,9 @@ "devDependencies": { "@eslint/js": "^10.0.1", "@types/node": "^25.9.0", + "@types/qrcode": "^1.5.6", "@types/react": "^19.2.14", + "@types/react-big-calendar": "^1.16.3", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^5.1.4", "eslint": "^10.4.0", @@ -269,9 +279,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", - "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -325,6 +335,59 @@ "node": ">=6.9.0" } }, + "node_modules/@dnd-kit/accessibility": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", + "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/core": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", + "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", + "license": "MIT", + "dependencies": { + "@dnd-kit/accessibility": "^3.1.1", + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/sortable": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz", + "integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==", + "license": "MIT", + "dependencies": { + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@dnd-kit/core": "^6.3.0", + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/utilities": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz", + "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/@emnapi/core": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", @@ -618,6 +681,28 @@ "url": "https://github.com/sponsors/Boshen" } }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@restart/hooks": { + "version": "0.4.16", + "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.4.16.tgz", + "integrity": "sha512-f7aCv7c+nU/3mF7NWLtVVr0Ra80RqsO89hO72r+Y/nvQr5+q0UFGkocElTH6MJApvReVh6JHUFYn2cw1WdHF3w==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/@rolldown/binding-android-arm64": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.1.tgz", @@ -888,6 +973,90 @@ "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", "license": "MIT" }, + "node_modules/@supabase/auth-js": { + "version": "2.106.1", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.106.1.tgz", + "integrity": "sha512-7eyheXfAGwkB9bZewJPs+N3UYt6kra2JG6mIxNEgbkvcO15PLD1e75PTIUEYYl3zrifm3GrpShVl7QZxKrXO/w==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/functions-js": { + "version": "2.106.1", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.106.1.tgz", + "integrity": "sha512-XbOPnR2mW7jp/EcW447xmGwCa+/Wc00Hkw8t4tUIJjRsHQ4xAESsLKcyLRhRJjJoUnJVXUlC+w0wUxUCM7CG2A==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/phoenix": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@supabase/phoenix/-/phoenix-0.4.2.tgz", + "integrity": "sha512-YSAGnmDAfuleFCVt3CeurQZAhxRfXWeZIIkwp7NhYzQ1UwW6ePSnzsFAiUm/mbCkfoCf70QQHKW/K6RKh52a4A==", + "license": "MIT" + }, + "node_modules/@supabase/postgrest-js": { + "version": "2.106.1", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.106.1.tgz", + "integrity": "sha512-Qbn6d2lqiqeaBX1Uko0e/hL90dtQGRN6CG2wMVQtJpRFstlVW45qmUTyTOsiB8dYUWu1fWYo4YzJuDbokGv3tQ==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/realtime-js": { + "version": "2.106.1", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.106.1.tgz", + "integrity": "sha512-eQCYri5E8KsjpDgC7g28cOOS2britjUWdNSJluFMainqrMRepzjOnaxqXc3RoAz7H0dxmBrfLUNF6NGP8C+YaA==", + "license": "MIT", + "dependencies": { + "@supabase/phoenix": "^0.4.2", + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/storage-js": { + "version": "2.106.1", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.106.1.tgz", + "integrity": "sha512-HWcLIhqinhWKpOQ3WzglR2unjW0eh9J7yOu3IZrZNIEkraK4La/HDvTqndljGsNw0itPtyHhuKBxRoPG1VUARw==", + "license": "MIT", + "dependencies": { + "iceberg-js": "^0.8.1", + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/supabase-js": { + "version": "2.106.1", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.106.1.tgz", + "integrity": "sha512-gP4HurGkGu7Z3xoOCjtAI17BKKp7jpsmwY0Ssbsks9XQRzJ7ZhK7LxfLdBSYgUdgZCQgjRK+Mr7+cl4Gxrk0Rw==", + "license": "MIT", + "dependencies": { + "@supabase/auth-js": "2.106.1", + "@supabase/functions-js": "2.106.1", + "@supabase/postgrest-js": "2.106.1", + "@supabase/realtime-js": "2.106.1", + "@supabase/storage-js": "2.106.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@tanstack/query-core": { "version": "5.100.10", "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.100.10.tgz", @@ -1003,6 +1172,13 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/date-arithmetic": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@types/date-arithmetic/-/date-arithmetic-4.1.4.tgz", + "integrity": "sha512-p9eZ2X9B80iKiTW4ukVj8B4K6q9/+xFtQ5MGYA5HWToY9nL4EkhV9+6ftT2VHpVMEZb5Tv00Iel516bVdO+yRw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/esrecurse": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", @@ -1034,16 +1210,44 @@ "undici-types": ">=7.24.0 <7.24.7" } }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/qrcode": { + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.6.tgz", + "integrity": "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/react": { "version": "19.2.14", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", - "dev": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" } }, + "node_modules/@types/react-big-calendar": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/@types/react-big-calendar/-/react-big-calendar-1.16.3.tgz", + "integrity": "sha512-CR+5BKMhlr/wPgsp+sXOeNKNkoU1h/+6H1XoWuL7xnurvzGRQv/EnM8jPS9yxxBvXI8pjQBaJcI7RTSGiewG/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/date-arithmetic": "*", + "@types/prop-types": "*", + "@types/react": "*" + } + }, "node_modules/@types/react-dom": { "version": "19.2.3", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", @@ -1054,6 +1258,12 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/warning": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.4.tgz", + "integrity": "sha512-CqN8MnISMwQbLJXO3doBAV4Yw9hx9/Pyr2rZ78+NfaCnhyRA/nKrpyk6E7mKw17ZOaQdLpK9GiUjrqLzBlN3sg==", + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.59.3", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.3.tgz", @@ -1358,6 +1568,30 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/balanced-match": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", @@ -1428,6 +1662,15 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001774", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001774.tgz", @@ -1449,6 +1692,49 @@ ], "license": "CC-BY-4.0" }, + "node_modules/cldrjs": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/cldrjs/-/cldrjs-0.5.5.tgz", + "integrity": "sha512-KDwzwbmLIPfCgd8JERVDpQKrUUM1U4KpFJJg2IROv89rF172lLufoJnqJ/Wea6fXL5bO6WjuLMzY8V52UWPvkA==" + }, + "node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -1488,7 +1774,28 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, + "license": "MIT" + }, + "node_modules/date-arithmetic": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-arithmetic/-/date-arithmetic-4.1.0.tgz", + "integrity": "sha512-QWxYLR5P/6GStZcdem+V1xoto6DMadYWpMXU82ES3/RfR3Wdwr3D0+be7mgOJ+Ov0G9D5Dmb9T17sNLQYj9XOg==", + "license": "MIT" + }, + "node_modules/date-fns": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.4.0.tgz", + "integrity": "sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/dayjs": { + "version": "1.11.21", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.21.tgz", + "integrity": "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==", "license": "MIT" }, "node_modules/debug": { @@ -1508,6 +1815,15 @@ } } }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -1515,6 +1831,15 @@ "dev": true, "license": "MIT" }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -1525,6 +1850,22 @@ "node": ">=8" } }, + "node_modules/dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", + "license": "MIT" + }, + "node_modules/dom-helpers": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-6.0.1.tgz", + "integrity": "sha512-IKySryuFwseGkrCA/pIqlwUPOD50w1Lj/B2Yief3vBOP18k5y4t+hTqKh55gULDVeJMRitcozve+g/wVFf4sFQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.27.1", + "csstype": "^3.1.3" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.302", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz", @@ -1532,6 +1873,12 @@ "dev": true, "license": "ISC" }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, "node_modules/engine.io-client": { "version": "6.6.4", "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.4.tgz", @@ -1874,6 +2221,15 @@ "node": ">=6.9.0" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -1887,6 +2243,15 @@ "node": ">=10.13.0" } }, + "node_modules/globalize": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/globalize/-/globalize-1.7.1.tgz", + "integrity": "sha512-PFymRL0PtitFOlSniuwwwNfkooi3cLQJo9Uke1+j1DsGfUkkHkwneImqVtGcqKI0TuzhAlHt7hAcgK324902HA==", + "license": "MIT", + "dependencies": { + "cldrjs": "^0.5.4" + } + }, "node_modules/globals": { "version": "17.6.0", "resolved": "https://registry.npmjs.org/globals/-/globals-17.6.0.tgz", @@ -1963,6 +2328,15 @@ "@babel/runtime": "^7.23.2" } }, + "node_modules/iceberg-js": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/iceberg-js/-/iceberg-js-0.8.1.tgz", + "integrity": "sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -1983,6 +2357,15 @@ "node": ">=0.8.19" } }, + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -1993,6 +2376,15 @@ "node": ">=0.10.0" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -2017,7 +2409,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, "license": "MIT" }, "node_modules/jsesc": { @@ -2368,6 +2759,30 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, + "node_modules/lodash-es": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -2387,6 +2802,21 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/luxon": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz", + "integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", + "license": "MIT" + }, "node_modules/minimatch": { "version": "10.2.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", @@ -2403,6 +2833,27 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/moment-timezone": { + "version": "0.5.48", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.48.tgz", + "integrity": "sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==", + "license": "MIT", + "dependencies": { + "moment": "^2.29.4" + }, + "engines": { + "node": "*" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -2442,6 +2893,33 @@ "dev": true, "license": "MIT" }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/openai": { + "version": "6.44.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-6.44.0.tgz", + "integrity": "sha512-09/gH+8jH0RgUwsgWHAaxsKGRT5zVZ95IaJUnqAWj6XejIBmnFRwq2WUIF37VtDEsmGrtPmvCs5+yBSeZGWvkA==", + "license": "Apache-2.0", + "peerDependencies": { + "ws": "^8.18.0", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -2492,11 +2970,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -2532,6 +3018,15 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/postcss": { "version": "8.5.14", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", @@ -2571,6 +3066,17 @@ "node": ">= 0.8.0" } }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -2581,6 +3087,23 @@ "node": ">=6" } }, + "node_modules/qrcode": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", + "license": "MIT", + "dependencies": { + "dijkstrajs": "^1.0.1", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + }, + "bin": { + "qrcode": "bin/qrcode" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/react": { "version": "19.2.6", "resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz", @@ -2590,6 +3113,34 @@ "node": ">=0.10.0" } }, + "node_modules/react-big-calendar": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/react-big-calendar/-/react-big-calendar-1.20.0.tgz", + "integrity": "sha512-Lp1mvG34l/9xtb/2LsBb4UAF3iPcUkmDqbmpsvDHzp/n8GA5gtn3+nf8BsULWw08opKDgv38nQi75dQlOOqzkg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7", + "clsx": "^2.1.1", + "date-arithmetic": "^4.1.0", + "dayjs": "^1.11.21", + "dom-helpers": "^6.0.1", + "globalize": "^1.7.1", + "invariant": "^2.2.4", + "lodash": "^4.18.1", + "lodash-es": "^4.18.1", + "luxon": "^3.7.2", + "memoize-one": "^6.0.0", + "moment": "^2.29.4", + "moment-timezone": "^0.5.48", + "prop-types": "^15.8.1", + "react-overlays": "^5.2.1", + "uncontrollable": "^7.2.1" + }, + "peerDependencies": { + "react": "^16.14.0 || ^17 || ^18 || ^19", + "react-dom": "^16.14.0 || ^17 || ^18 || ^19" + } + }, "node_modules/react-dom": { "version": "19.2.6", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.6.tgz", @@ -2629,6 +3180,48 @@ } } }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/react-lifecycles-compat": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==", + "license": "MIT" + }, + "node_modules/react-overlays": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/react-overlays/-/react-overlays-5.2.1.tgz", + "integrity": "sha512-GLLSOLWr21CqtJn8geSwQfoJufdt3mfdsnIiQswouuQ2MMPns+ihZklxvsTDKD3cR2tF8ELbi5xUsvqVhR6WvA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.8", + "@popperjs/core": "^2.11.6", + "@restart/hooks": "^0.4.7", + "@types/warning": "^3.0.0", + "dom-helpers": "^5.2.0", + "prop-types": "^15.7.2", + "uncontrollable": "^7.2.1", + "warning": "^4.0.3" + }, + "peerDependencies": { + "react": ">=16.3.0", + "react-dom": ">=16.3.0" + } + }, + "node_modules/react-overlays/node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, "node_modules/react-refresh": { "version": "0.18.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", @@ -2677,6 +3270,21 @@ "react-dom": ">=18" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "license": "ISC" + }, "node_modules/rolldown": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.1.tgz", @@ -2734,6 +3342,12 @@ "semver": "bin/semver.js" } }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, "node_modules/set-cookie-parser": { "version": "2.7.2", "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", @@ -2801,6 +3415,32 @@ "node": ">=0.10.0" } }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/tinyglobby": { "version": "0.2.16", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", @@ -2835,9 +3475,7 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD", - "optional": true + "license": "0BSD" }, "node_modules/type-check": { "version": "0.4.0", @@ -2890,6 +3528,21 @@ "typescript": ">=4.8.4 <6.1.0" } }, + "node_modules/uncontrollable": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-7.2.1.tgz", + "integrity": "sha512-svtcfoTADIB0nT9nltgjujTi7BzVmwjZClOmskKu/E8FW9BXzg9os8OLr4f8Dlnk0rYWJIWr4wv9eKUXiQvQwQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.6.3", + "@types/react": ">=16.9.11", + "invariant": "^2.2.4", + "react-lifecycles-compat": "^3.0.4" + }, + "peerDependencies": { + "react": ">=15.0.0" + } + }, "node_modules/undici-types": { "version": "7.24.6", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", @@ -3034,6 +3687,15 @@ "node": ">=0.10.0" } }, + "node_modules/warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -3050,6 +3712,12 @@ "node": ">= 8" } }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "license": "ISC" + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -3060,6 +3728,20 @@ "node": ">=0.10.0" } }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/ws": { "version": "8.18.3", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", @@ -3089,6 +3771,12 @@ "node": ">=0.4.0" } }, + "node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "license": "ISC" + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", @@ -3096,6 +3784,93 @@ "dev": true, "license": "ISC" }, + "node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "license": "MIT", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/yargs/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yargs/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/dashboard/package.json b/dashboard/package.json index ff9fe9cbb..6490f6cfe 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -10,12 +10,20 @@ "preview": "vite preview" }, "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", + "@supabase/supabase-js": "^2.106.1", "@tanstack/react-query": "^5.100.10", "@tanstack/react-table": "^8.21.3", + "date-fns": "^4.4.0", "i18next": "^26.2.0", "i18next-browser-languagedetector": "^8.2.1", "lucide-react": "^1.16.0", + "openai": "^6.44.0", + "qrcode": "^1.5.4", "react": "^19.2.6", + "react-big-calendar": "^1.20.0", "react-dom": "^19.2.6", "react-i18next": "^17.0.8", "react-router-dom": "^7.15.1", @@ -24,7 +32,9 @@ "devDependencies": { "@eslint/js": "^10.0.1", "@types/node": "^25.9.0", + "@types/qrcode": "^1.5.6", "@types/react": "^19.2.14", + "@types/react-big-calendar": "^1.16.3", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^5.1.4", "eslint": "^10.4.0", diff --git a/dashboard/public/_redirects b/dashboard/public/_redirects new file mode 100644 index 000000000..7797f7c6a --- /dev/null +++ b/dashboard/public/_redirects @@ -0,0 +1 @@ +/* /index.html 200 diff --git a/dashboard/public/image copy.png b/dashboard/public/image copy.png new file mode 100644 index 000000000..64b4dd77b Binary files /dev/null and b/dashboard/public/image copy.png differ diff --git a/dashboard/public/image.png b/dashboard/public/image.png new file mode 100644 index 000000000..bd8a030af Binary files /dev/null and b/dashboard/public/image.png differ diff --git a/dashboard/src/App.tsx b/dashboard/src/App.tsx index 21c674922..94a803420 100644 --- a/dashboard/src/App.tsx +++ b/dashboard/src/App.tsx @@ -1,13 +1,35 @@ import { useState, useEffect, lazy, Suspense } from 'react'; import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { Loader2 } from 'lucide-react'; +import { Loader as Loader2 } from 'lucide-react'; import { Layout } from './components/Layout'; import { ToastProvider } from './components/Toast'; import { RoleProvider, useRole, type UserRole } from './hooks/useRole'; import { ErrorBoundary } from './components/ErrorBoundary'; +import { CustomerAuthProvider, useCustomerAuth } from './hooks/useCustomerAuth'; +import { AgencyAuthProvider, useAgencyAuth } from './agency/hooks/useAgencyAuth'; import './App.css'; +// ── AgencyOS pages ──────────────────────────────────────────────────────────── +const AgencyLogin = lazy(() => import('./agency/pages/AgencyLogin').then(m => ({ default: m.AgencyLogin }))); +const AgencyRegister = lazy(() => import('./agency/pages/AgencyRegister').then(m => ({ default: m.AgencyRegister }))); +const AgencyLayout = lazy(() => import('./agency/components/AgencyLayout').then(m => ({ default: m.AgencyLayout }))); +const AgencyDashboard = lazy(() => import('./agency/pages/AgencyDashboard').then(m => ({ default: m.AgencyDashboard }))); +const SubAccountsPage = lazy(() => import('./agency/pages/SubAccounts').then(m => ({ default: m.SubAccounts }))); +const AgencySettings = lazy(() => import('./agency/pages/AgencySettings').then(m => ({ default: m.AgencySettings }))); +const SubAccountLayout = lazy(() => import('./agency/components/SubAccountLayout').then(m => ({ default: m.SubAccountLayout }))); +const SubAccountDashboard = lazy(() => import('./agency/pages/SubAccountDashboard')); +const AgencyConversations = lazy(() => import('./agency/pages/Conversations')); +const AgencyContacts = lazy(() => import('./agency/pages/Contacts').then(m => ({ default: m.Contacts }))); +const AgencyOpportunities = lazy(() => import('./agency/pages/Opportunities').then(m => ({ default: m.Opportunities }))); +const AgencyCalendar = lazy(() => import('./agency/pages/CalendarPage').then(m => ({ default: m.CalendarPage }))); +const AgencyAutomations = lazy(() => import('./agency/pages/Automations').then(m => ({ default: m.Automations }))); +const AgencyMedia = lazy(() => import('./agency/pages/MediaPage').then(m => ({ default: m.MediaPage }))); +const AgencyAiAgents = lazy(() => import('./agency/pages/AiAgents').then(m => ({ default: m.AiAgents }))); +const AgencyQrCodes = lazy(() => import('./agency/pages/QrCodes').then(m => ({ default: m.QrCodes }))); +const SubAccountSettings = lazy(() => import('./agency/pages/SubAccountSettings').then(m => ({ default: m.SubAccountSettings }))); + +// ── Admin dashboard pages (OpenWA gateway) ──────────────────────────────── const Login = lazy(() => import('./pages/Login').then(m => ({ default: m.Login }))); const Dashboard = lazy(() => import('./pages/Dashboard').then(m => ({ default: m.Dashboard }))); const Sessions = lazy(() => import('./pages/Sessions').then(m => ({ default: m.Sessions }))); @@ -18,6 +40,15 @@ const MessageTester = lazy(() => import('./pages/MessageTester').then(m => ({ de const Infrastructure = lazy(() => import('./pages/Infrastructure').then(m => ({ default: m.Infrastructure }))); const Plugins = lazy(() => import('./pages/Plugins')); +// ── Customer SaaS pages ─────────────────────────────────────────────────── +const CustomerLogin = lazy(() => import('./pages/customer/CustomerLogin').then(m => ({ default: m.CustomerLogin }))); +const CustomerRegister = lazy(() => import('./pages/customer/CustomerRegister').then(m => ({ default: m.CustomerRegister }))); +const CustomerLayout = lazy(() => import('./pages/customer/CustomerLayout').then(m => ({ default: m.CustomerLayout }))); +const CustomerDashboard = lazy(() => import('./pages/customer/CustomerDashboard').then(m => ({ default: m.CustomerDashboard }))); +const ConnectWhatsApp = lazy(() => import('./pages/customer/ConnectWhatsApp').then(m => ({ default: m.ConnectWhatsApp }))); +const Conversations = lazy(() => import('./pages/customer/Conversations').then(m => ({ default: m.Conversations }))); +const CustomerSettings = lazy(() => import('./pages/customer/CustomerSettings').then(m => ({ default: m.CustomerSettings }))); + const queryClient = new QueryClient({ defaultOptions: { queries: { @@ -28,8 +59,14 @@ const queryClient = new QueryClient({ }, }); -function AppContent() { - // Initialize from sessionStorage to avoid setState in effect +const loadingFallback = ( +
+ +
+); + +// ── Admin app section ────────────────────────────────────────────────────── +function AdminApp() { const savedKey = sessionStorage.getItem('openwa_api_key'); const [isAuthenticated, setIsAuthenticated] = useState(!!savedKey); const [, setApiKey] = useState(savedKey || ''); @@ -38,8 +75,6 @@ function AppContent() { const handleLogin = async (key: string) => { setApiKey(key); sessionStorage.setItem('openwa_api_key', key); - - // Fetch the role from API try { const response = await fetch('/api/auth/validate', { method: 'POST', @@ -50,10 +85,8 @@ function AppContent() { setRole(data.role as UserRole); } } catch { - // Default to viewer if we can't fetch role setRole('viewer'); } - setIsAuthenticated(true); }; @@ -64,39 +97,26 @@ function AppContent() { sessionStorage.removeItem('openwa_api_key'); }; - // Re-validate and get role on mount if already authenticated useEffect(() => { if (!savedKey) return; - fetch('/api/auth/validate', { method: 'POST', headers: { 'X-API-Key': savedKey }, }) .then(res => res.json()) .then(data => { - if (data.valid && data.role) { - setRole(data.role as UserRole); - } + if (data.valid && data.role) setRole(data.role as UserRole); }) - .catch(() => { - // Keep existing role from localStorage if validation fails - }); + .catch(() => {}); }, [savedKey, setRole]); - const loadingFallback = ( -
- -
- ); - if (!isAuthenticated) { return ; } return ( - - + }> } /> @@ -110,19 +130,125 @@ function AppContent() { } /> - - + ); } +// ── Customer app section ─────────────────────────────────────────────────── +function CustomerApp() { + const { isAuthenticated } = useCustomerAuth(); + + return ( + + + : } + /> + : } + /> + {isAuthenticated ? ( + }> + } /> + } /> + } /> + } /> + } /> + + ) : ( + } /> + )} + + + ); +} + +// ── AgencyOS section ────────────────────────────────────────────────────── +function AgencyApp() { + const { user, loading } = useAgencyAuth(); + + if (loading) return loadingFallback; + + return ( + + + : } + /> + : } + /> + {user ? ( + }> + } /> + } /> + } /> + } /> + + ) : ( + } /> + )} + + + ); +} + +// ── Root ─────────────────────────────────────────────────────────────────── +function AppRoutes() { + return ( + + + } /> + + + + } + > + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + } /> + } /> + + + + } + /> + + + ); +} + function App() { return ( - - - + + + + + ); diff --git a/dashboard/src/agency/components/AgencyLayout.tsx b/dashboard/src/agency/components/AgencyLayout.tsx new file mode 100644 index 000000000..e62ecaa16 --- /dev/null +++ b/dashboard/src/agency/components/AgencyLayout.tsx @@ -0,0 +1,158 @@ +import { useEffect, useState } from 'react'; +import { NavLink, Outlet, useOutletContext } from 'react-router-dom'; +import { useAgencyAuth } from '../hooks/useAgencyAuth'; +import { supabase } from '../../lib/supabase'; +import type { Agency } from '../types'; + +export function AgencyLayout() { + const { user, signOut } = useAgencyAuth(); + const [agency, setAgency] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + if (!user) return; + + const loadAgency = async () => { + try { + const { data, error } = await supabase + .from('agencies') + .select('*') + .eq('owner_id', user.id) + .single(); + + if (error && error.code !== 'PGRST116') { + console.error('Error loading agency:', error); + } else if (data) { + setAgency(data as Agency); + } + } catch (err) { + console.error('Error loading agency:', err); + } finally { + setLoading(false); + } + }; + + loadAgency(); + }, [user]); + + const handleLogout = async () => { + await signOut(); + }; + + const navLinkStyle = ({ isActive }: { isActive: boolean }) => ({ + display: 'flex', + alignItems: 'center', + padding: '12px 16px', + textDecoration: 'none', + color: isActive ? 'white' : '#e5e5e5', + backgroundColor: isActive ? '#7c3aed' : 'transparent', + borderRadius: '8px', + cursor: 'pointer', + fontSize: '14px', + fontWeight: isActive ? '600' : '500', + transition: 'all 0.2s ease', + } as React.CSSProperties); + + return ( +
+ {/* Sidebar */} +
+ {/* Logo */} +
+
+ AgencyOS +
+ {agency && ( +
+ {agency.name} +
+ )} + {loading &&
Loading...
} +
+ + {/* Navigation */} + + + {/* User Info & Logout */} +
+
+ {user?.email} +
+ +
+
+ + {/* Main Content */} +
+
+ +
+
+
+ ); +} + +export function useAgencyOutlet() { + return useOutletContext<{ agencyId: string }>(); +} diff --git a/dashboard/src/agency/components/SubAccountLayout.tsx b/dashboard/src/agency/components/SubAccountLayout.tsx new file mode 100644 index 000000000..17ec93e7e --- /dev/null +++ b/dashboard/src/agency/components/SubAccountLayout.tsx @@ -0,0 +1,359 @@ +import React, { useEffect, useState } from 'react'; +import { Outlet, useParams, NavLink, useNavigate } from 'react-router-dom'; +import { + LayoutDashboard, + MessageSquare, + Users, + TrendingUp, + Calendar, + Zap, + Image, + Bot, + QrCode, + Settings, + Menu, + X, + LogOut, + ChevronLeft, +} from 'lucide-react'; +import { useAgencyAuth } from '../hooks/useAgencyAuth'; +import { supabase } from '../../lib/supabase'; + +interface SubAccount { + id: string; + name: string; + agency_id: string; + logo?: string; +} + +interface Agency { + id: string; + name: string; +} + +const SubAccountLayout: React.FC = () => { + const { subAccountId } = useParams<{ subAccountId: string }>(); + const { user, signOut } = useAgencyAuth(); + const navigate = useNavigate(); + + const [subAccount, setSubAccount] = useState(null); + const [agency, setAgency] = useState(null); + const [loading, setLoading] = useState(true); + const [sidebarOpen, setSidebarOpen] = useState(true); + + useEffect(() => { + const loadData = async () => { + if (!subAccountId) { + setLoading(false); + return; + } + + try { + // Load sub-account + const { data: subAccountData, error: subAccountError } = await supabase + .from('sub_accounts') + .select('*') + .eq('id', subAccountId) + .single(); + + if (subAccountError) throw subAccountError; + + setSubAccount(subAccountData); + + // Load agency + if (subAccountData.agency_id) { + const { data: agencyData, error: agencyError } = await supabase + .from('agencies') + .select('*') + .eq('id', subAccountData.agency_id) + .single(); + + if (agencyError) throw agencyError; + setAgency(agencyData); + } + } catch (error) { + console.error('Error loading sub-account data:', error); + } finally { + setLoading(false); + } + }; + + loadData(); + }, [subAccountId]); + + const handleLogout = async () => { + await signOut(); + navigate('/agency/login'); + }; + + const navItems = [ + { label: 'Dashboard', icon: LayoutDashboard, path: 'dashboard' }, + { label: 'Conversations', icon: MessageSquare, path: 'conversations' }, + { label: 'Contacts', icon: Users, path: 'contacts' }, + { label: 'Opportunities', icon: TrendingUp, path: 'opportunities' }, + { label: 'Calendar', icon: Calendar, path: 'calendar' }, + { label: 'Automations', icon: Zap, path: 'automations' }, + { label: 'Media', icon: Image, path: 'media' }, + { label: 'AI Agents', icon: Bot, path: 'ai-agents' }, + { label: 'QR Codes', icon: QrCode, path: 'qr-codes' }, + { label: 'Settings', icon: Settings, path: 'settings' }, + ]; + + const getInitial = (name: string): string => { + return name?.charAt(0).toUpperCase() || 'S'; + }; + + if (loading) { + return ( +
+
Loading...
+
+ ); + } + + return ( +
+ {/* Mobile Hamburger */} + + + {/* Sidebar */} +
+ {/* Sidebar Top - Logo and Name */} +
+
+
+ {getInitial(subAccount?.name || '')} +
+
+
+ {subAccount?.name || 'Sub-Account'} +
+ {agency && ( +
+ {agency.name} +
+ )} +
+
+
+ + {/* Navigation Items */} + + + {/* Sidebar Bottom - User Info and Actions */} +
+ {user && ( +
+
Logged in as
+
+ {user.email} +
+
+ )} + + + + ({ + display: 'flex', + alignItems: 'center', + gap: '8px', + padding: '10px 12px', + backgroundColor: 'transparent', + border: '1px solid #333', + borderRadius: '6px', + color: '#e5e5e5', + textDecoration: 'none', + fontSize: '13px', + fontWeight: '500', + transition: 'background-color 0.2s ease, border-color 0.2s ease', + cursor: 'pointer', + width: '100%', + boxSizing: 'border-box', + })} + onMouseEnter={(e) => { + e.currentTarget.style.backgroundColor = '#1a1a1a'; + e.currentTarget.style.borderColor = '#444'; + }} + onMouseLeave={(e) => { + e.currentTarget.style.backgroundColor = 'transparent'; + e.currentTarget.style.borderColor = '#333'; + }} + > + + Back to Agency + +
+
+ + {/* Main Content */} +
+
+ +
+
+ + {/* Mobile Overlay */} + {sidebarOpen && ( +
setSidebarOpen(false)} + style={{ + display: 'block', + position: 'fixed', + inset: '0', + backgroundColor: 'rgba(0, 0, 0, 0.5)', + zIndex: 20, + }} + className="mobile-overlay" + /> + )} + + +
+ ); +}; + +export { SubAccountLayout }; +export default SubAccountLayout; diff --git a/dashboard/src/agency/hooks/useAgencyAuth.tsx b/dashboard/src/agency/hooks/useAgencyAuth.tsx new file mode 100644 index 000000000..b18cb8a2c --- /dev/null +++ b/dashboard/src/agency/hooks/useAgencyAuth.tsx @@ -0,0 +1,89 @@ +import { createContext, useContext, useEffect, useState, type ReactNode } from 'react'; +import { supabase } from '../../lib/supabase'; +import type { User, Session } from '@supabase/supabase-js'; + +interface AgencyAuthCtx { + user: User | null; + session: Session | null; + loading: boolean; + signIn: (email: string, password: string) => Promise<{ error: string | null; redirectTo?: string }>; + signUp: (email: string, password: string, agencyName: string) => Promise<{ error: string | null }>; + signOut: () => Promise; +} + +const Ctx = createContext(null); + +export function AgencyAuthProvider({ children }: { children: ReactNode }) { + const [user, setUser] = useState(null); + const [session, setSession] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + supabase.auth.getSession().then(({ data }) => { + setSession(data.session); + setUser(data.session?.user ?? null); + setLoading(false); + }); + + const { data: { subscription } } = supabase.auth.onAuthStateChange((_event, sess) => { + setSession(sess); + setUser(sess?.user ?? null); + }); + return () => subscription.unsubscribe(); + }, []); + + const signIn = async (email: string, password: string) => { + const { data, error } = await supabase.auth.signInWithPassword({ email, password }); + if (error) return { error: error.message }; + + const userId = data.user?.id; + if (userId) { + const { data: agency } = await supabase + .from('agencies') + .select('id') + .eq('owner_id', userId) + .maybeSingle(); + + if (!agency) { + // No agency — route to first sub-account they belong to + const { data: membership } = await supabase + .from('user_sub_accounts') + .select('sub_account_id') + .eq('user_id', userId) + .limit(1) + .maybeSingle(); + + if (membership?.sub_account_id) { + return { error: null, redirectTo: `/app/${membership.sub_account_id}/dashboard` }; + } + } + } + + return { error: null, redirectTo: '/agency' }; + }; + + const signUp = async (email: string, password: string, agencyName: string) => { + const { data, error } = await supabase.auth.signUp({ email, password }); + if (error) return { error: error.message }; + if (data.user) { + const { error: agencyError } = await supabase.from('agencies').insert({ + name: agencyName, + owner_id: data.user.id, + }); + if (agencyError) return { error: agencyError.message }; + } + return { error: null }; + }; + + const signOut = async () => { + await supabase.auth.signOut(); + }; + + return {children}; +} + +export function useAgencyAuth() { + const ctx = useContext(Ctx); + if (!ctx) throw new Error('useAgencyAuth must be used inside AgencyAuthProvider'); + return ctx; +} diff --git a/dashboard/src/agency/lib/agencyApi.ts b/dashboard/src/agency/lib/agencyApi.ts new file mode 100644 index 000000000..58b4bca08 --- /dev/null +++ b/dashboard/src/agency/lib/agencyApi.ts @@ -0,0 +1,58 @@ +const SUPABASE_URL = import.meta.env.VITE_SUPABASE_URL as string; +const SUPABASE_ANON_KEY = import.meta.env.VITE_SUPABASE_ANON_KEY as string; + +function fnUrl(name: string) { + return `${SUPABASE_URL}/functions/v1/${name}`; +} + +function headers() { + return { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${SUPABASE_ANON_KEY}`, + }; +} + +export const agencyOpenWA = { + createSession: (subAccountId: string) => + fetch(`${fnUrl('agency-openwa')}?action=create-session`, { + method: 'POST', + headers: headers(), + body: JSON.stringify({ subAccountId }), + }).then(r => r.json()), + + getQr: (subAccountId: string) => + fetch(`${fnUrl('agency-openwa')}?action=qr&subAccountId=${subAccountId}`, { + headers: headers(), + }).then(r => r.json()), + + getStatus: (subAccountId: string) => + fetch(`${fnUrl('agency-openwa')}?action=status&subAccountId=${subAccountId}`, { + headers: headers(), + }).then(r => r.json()), + + sendMessage: (subAccountId: string, chatId: string, text: string) => + fetch(`${fnUrl('agency-openwa')}?action=send`, { + method: 'POST', + headers: headers(), + body: JSON.stringify({ subAccountId, chatId, text }), + }).then(r => r.json()), + + disconnect: (subAccountId: string) => + fetch(`${fnUrl('agency-openwa')}?action=disconnect&subAccountId=${subAccountId}`, { + method: 'DELETE', + headers: headers(), + }).then(r => r.json()), +}; + +export const agencyAiChat = { + chat: (payload: { messages: {role: string; content: string}[]; provider: string; model: string; apiKey: string; systemPrompt: string }) => + fetch(fnUrl('agency-ai-chat'), { + method: 'POST', + headers: headers(), + body: JSON.stringify(payload), + }).then(r => r.json()), +}; + +export function webhookUrl(subAccountId: string) { + return `${SUPABASE_URL}/functions/v1/agency-webhook/${subAccountId}`; +} diff --git a/dashboard/src/agency/pages/AgencyDashboard.tsx b/dashboard/src/agency/pages/AgencyDashboard.tsx new file mode 100644 index 000000000..edb83901a --- /dev/null +++ b/dashboard/src/agency/pages/AgencyDashboard.tsx @@ -0,0 +1,372 @@ +import { useEffect, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { supabase } from '../../lib/supabase'; +import { useAgencyOutlet } from '../components/AgencyLayout'; +import type { SubAccount } from '../types'; + +interface CreateSubAccountModalProps { + isOpen: boolean; + onClose: () => void; + onSubmit: (name: string) => void; + isLoading: boolean; +} + +function CreateSubAccountModal({ isOpen, onClose, onSubmit, isLoading }: CreateSubAccountModalProps) { + const [name, setName] = useState(''); + + const handleSubmit = () => { + if (name.trim()) { + onSubmit(name); + setName(''); + } + }; + + if (!isOpen) return null; + + return ( +
+
e.stopPropagation()} + > +

+ Create New Sub-Account +

+ setName(e.target.value)} + onKeyPress={(e) => e.key === 'Enter' && handleSubmit()} + disabled={isLoading} + style={{ + width: '100%', + padding: '10px 12px', + border: '1px solid #e5e5e5', + borderRadius: '8px', + fontSize: '14px', + marginBottom: '16px', + boxSizing: 'border-box', + fontFamily: 'inherit', + }} + /> +
+ + +
+
+
+ ); +} + +export function AgencyDashboard() { + const { agencyId } = useAgencyOutlet(); + const navigate = useNavigate(); + const [stats, setStats] = useState>({ + subAccounts: 0, + contacts: 0, + conversations: 0, + }); + const [subAccounts, setSubAccounts] = useState([]); + const [loading, setLoading] = useState(true); + const [modalOpen, setModalOpen] = useState(false); + const [isCreating, setIsCreating] = useState(false); + + useEffect(() => { + if (!agencyId) return; + loadData(); + }, [agencyId]); + + const loadData = async () => { + try { + setLoading(true); + + // Load sub-accounts + const { data: subAcctsData, error: subAcctsError } = await supabase + .from('sub_accounts') + .select('*') + .eq('agency_id', agencyId); + + if (!subAcctsError && subAcctsData) { + setSubAccounts(subAcctsData as SubAccount[]); + } + + // Load stats + const subAccountIds = (subAcctsData || []).map((sa) => sa.id); + + // Count contacts + const { count: contactsCount } = await supabase + .from('agency_contacts') + .select('*', { count: 'exact', head: true }) + .in('sub_account_id', subAccountIds.length > 0 ? subAccountIds : ['']); + + // Count conversations + const { count: conversationsCount } = await supabase + .from('agency_conversations') + .select('*', { count: 'exact', head: true }) + .in('sub_account_id', subAccountIds.length > 0 ? subAccountIds : ['']) + .eq('status', 'open'); + + setStats({ + subAccounts: subAcctsData?.length || 0, + contacts: contactsCount || 0, + conversations: conversationsCount || 0, + }); + } catch (err) { + console.error('Error loading data:', err); + } finally { + setLoading(false); + } + }; + + const handleCreateSubAccount = async (name: string) => { + if (!agencyId) return; + try { + setIsCreating(true); + const { error } = await supabase.from('sub_accounts').insert({ + agency_id: agencyId, + name, + openwa_session_status: 'disconnected', + }); + + if (error) { + console.error('Error creating sub-account:', error); + alert('Failed to create sub-account'); + } else { + setModalOpen(false); + loadData(); + } + } catch (err) { + console.error('Error creating sub-account:', err); + alert('Failed to create sub-account'); + } finally { + setIsCreating(false); + } + }; + + const getWhatsAppStatus = (status: string) => { + const isConnected = status === 'connected'; + return ( + + ); + }; + + return ( +
+
+

+ Dashboard +

+

Welcome to your agency admin panel

+
+ + {/* Stats */} +
+ {[ + { label: 'Total Sub-Accounts', value: stats.subAccounts }, + { label: 'Total Contacts', value: stats.contacts }, + { label: 'Open Conversations', value: stats.conversations }, + ].map((stat) => ( +
+
+ {stat.label} +
+
+ {loading ? '...' : stat.value} +
+
+ ))} +
+ + {/* Sub-Accounts Section */} +
+
+

Sub-Accounts

+ +
+ + {/* Sub-Account Cards */} +
+ {subAccounts.map((subAccount) => ( +
+
+
+ {subAccount.name} +
+
+ {getWhatsAppStatus(subAccount.openwa_session_status)} + + {subAccount.openwa_session_status === 'connected' ? 'Connected' : 'Disconnected'} + +
+
+ + +
+ ))} +
+ + {!loading && subAccounts.length === 0 && ( +
+ No sub-accounts yet. Create one to get started. +
+ )} +
+ + setModalOpen(false)} + onSubmit={handleCreateSubAccount} + isLoading={isCreating} + /> +
+ ); +} diff --git a/dashboard/src/agency/pages/AgencyLogin.tsx b/dashboard/src/agency/pages/AgencyLogin.tsx new file mode 100644 index 000000000..801e32b6b --- /dev/null +++ b/dashboard/src/agency/pages/AgencyLogin.tsx @@ -0,0 +1,227 @@ +import React, { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useAgencyAuth } from '../hooks/useAgencyAuth'; + +export const AgencyLogin: React.FC = () => { + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [error, setError] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const navigate = useNavigate(); + const { signIn } = useAgencyAuth(); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(null); + setIsLoading(true); + + const result = await signIn(email, password); + + if (result.error) { + setError(result.error); + setIsLoading(false); + } else { + navigate(result.redirectTo ?? '/agency'); + } + }; + + const styles = { + container: { + minHeight: '100vh', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + backgroundColor: '#f5f5f5', + fontFamily: 'Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif', + padding: '20px', + } as React.CSSProperties, + card: { + width: '100%', + maxWidth: '400px', + backgroundColor: 'white', + borderRadius: '12px', + border: '1px solid #e5e5e5', + padding: '40px 32px', + boxShadow: '0 1px 3px rgba(0, 0, 0, 0.08)', + } as React.CSSProperties, + title: { + fontSize: '28px', + fontWeight: '700', + color: '#7c3aed', + marginBottom: '8px', + textAlign: 'center' as const, + } as React.CSSProperties, + subtitle: { + fontSize: '14px', + color: '#666666', + textAlign: 'center' as const, + marginBottom: '32px', + } as React.CSSProperties, + form: { + display: 'flex', + flexDirection: 'column' as const, + gap: '16px', + } as React.CSSProperties, + formGroup: { + display: 'flex', + flexDirection: 'column' as const, + gap: '6px', + } as React.CSSProperties, + label: { + fontSize: '14px', + fontWeight: '500', + color: '#1a1a1a', + } as React.CSSProperties, + input: { + width: '100%', + padding: '10px 12px', + fontSize: '14px', + border: '1px solid #e5e5e5', + borderRadius: '8px', + fontFamily: 'inherit', + transition: 'all 0.2s', + boxSizing: 'border-box' as const, + outline: 'none', + } as React.CSSProperties, + inputFocus: { + borderColor: '#7c3aed', + boxShadow: '0 0 0 3px rgba(124, 58, 237, 0.1)', + } as React.CSSProperties, + submitButton: { + width: '100%', + padding: '10px 16px', + marginTop: '8px', + fontSize: '14px', + fontWeight: '600', + backgroundColor: '#7c3aed', + color: 'white', + border: 'none', + borderRadius: '8px', + cursor: 'pointer', + transition: 'all 0.2s', + } as React.CSSProperties, + submitButtonHover: { + backgroundColor: '#6d28d9', + } as React.CSSProperties, + submitButtonDisabled: { + backgroundColor: '#d1d5db', + cursor: 'not-allowed', + opacity: 0.6, + } as React.CSSProperties, + errorMessage: { + padding: '10px 12px', + backgroundColor: '#fef2f2', + border: '1px solid #fecaca', + borderRadius: '8px', + fontSize: '13px', + color: '#dc2626', + marginBottom: '16px', + } as React.CSSProperties, + footer: { + marginTop: '24px', + textAlign: 'center' as const, + fontSize: '13px', + color: '#666666', + } as React.CSSProperties, + link: { + color: '#7c3aed', + textDecoration: 'none', + fontWeight: '600', + cursor: 'pointer', + transition: 'color 0.2s', + } as React.CSSProperties, + linkHover: { + color: '#6d28d9', + } as React.CSSProperties, + }; + + const [focusedField, setFocusedField] = useState(null); + const [hoveredButton, setHoveredButton] = useState(false); + + return ( +
+
+

AgencyOS

+

Sign in to your agency

+ + {error &&
{error}
} + +
+
+ + setEmail(e.target.value)} + onFocus={() => setFocusedField('email')} + onBlur={() => setFocusedField(null)} + style={{ + ...styles.input, + ...(focusedField === 'email' ? styles.inputFocus : {}), + }} + placeholder="you@example.com" + required + disabled={isLoading} + /> +
+ +
+ + setPassword(e.target.value)} + onFocus={() => setFocusedField('password')} + onBlur={() => setFocusedField(null)} + style={{ + ...styles.input, + ...(focusedField === 'password' ? styles.inputFocus : {}), + }} + placeholder="••••••••" + required + disabled={isLoading} + /> +
+ + +
+ + +
+
+ ); +}; + +export default AgencyLogin; diff --git a/dashboard/src/agency/pages/AgencyRegister.tsx b/dashboard/src/agency/pages/AgencyRegister.tsx new file mode 100644 index 000000000..d428405c0 --- /dev/null +++ b/dashboard/src/agency/pages/AgencyRegister.tsx @@ -0,0 +1,249 @@ +import React, { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useAgencyAuth } from '../hooks/useAgencyAuth'; + +export const AgencyRegister: React.FC = () => { + const [agencyName, setAgencyName] = useState(''); + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [error, setError] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const navigate = useNavigate(); + const { signUp } = useAgencyAuth(); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(null); + setIsLoading(true); + + const result = await signUp(email, password, agencyName); + + if (result.error) { + setError(result.error); + setIsLoading(false); + } else { + navigate('/agency'); + } + }; + + const styles = { + container: { + minHeight: '100vh', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + backgroundColor: '#f5f5f5', + fontFamily: 'Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif', + padding: '20px', + } as React.CSSProperties, + card: { + width: '100%', + maxWidth: '400px', + backgroundColor: 'white', + borderRadius: '12px', + border: '1px solid #e5e5e5', + padding: '40px 32px', + boxShadow: '0 1px 3px rgba(0, 0, 0, 0.08)', + } as React.CSSProperties, + title: { + fontSize: '28px', + fontWeight: '700', + color: '#7c3aed', + marginBottom: '8px', + textAlign: 'center' as const, + } as React.CSSProperties, + subtitle: { + fontSize: '14px', + color: '#666666', + textAlign: 'center' as const, + marginBottom: '32px', + } as React.CSSProperties, + form: { + display: 'flex', + flexDirection: 'column' as const, + gap: '16px', + } as React.CSSProperties, + formGroup: { + display: 'flex', + flexDirection: 'column' as const, + gap: '6px', + } as React.CSSProperties, + label: { + fontSize: '14px', + fontWeight: '500', + color: '#1a1a1a', + } as React.CSSProperties, + input: { + width: '100%', + padding: '10px 12px', + fontSize: '14px', + border: '1px solid #e5e5e5', + borderRadius: '8px', + fontFamily: 'inherit', + transition: 'all 0.2s', + boxSizing: 'border-box' as const, + outline: 'none', + } as React.CSSProperties, + inputFocus: { + borderColor: '#7c3aed', + boxShadow: '0 0 0 3px rgba(124, 58, 237, 0.1)', + } as React.CSSProperties, + submitButton: { + width: '100%', + padding: '10px 16px', + marginTop: '8px', + fontSize: '14px', + fontWeight: '600', + backgroundColor: '#7c3aed', + color: 'white', + border: 'none', + borderRadius: '8px', + cursor: 'pointer', + transition: 'all 0.2s', + } as React.CSSProperties, + submitButtonHover: { + backgroundColor: '#6d28d9', + } as React.CSSProperties, + submitButtonDisabled: { + backgroundColor: '#d1d5db', + cursor: 'not-allowed', + opacity: 0.6, + } as React.CSSProperties, + errorMessage: { + padding: '10px 12px', + backgroundColor: '#fef2f2', + border: '1px solid #fecaca', + borderRadius: '8px', + fontSize: '13px', + color: '#dc2626', + marginBottom: '16px', + } as React.CSSProperties, + footer: { + marginTop: '24px', + textAlign: 'center' as const, + fontSize: '13px', + color: '#666666', + } as React.CSSProperties, + link: { + color: '#7c3aed', + textDecoration: 'none', + fontWeight: '600', + cursor: 'pointer', + transition: 'color 0.2s', + } as React.CSSProperties, + linkHover: { + color: '#6d28d9', + } as React.CSSProperties, + }; + + const [focusedField, setFocusedField] = useState(null); + const [hoveredButton, setHoveredButton] = useState(false); + + return ( +
+
+

AgencyOS

+

Create your agency account

+ + {error &&
{error}
} + +
+
+ + setAgencyName(e.target.value)} + onFocus={() => setFocusedField('agencyName')} + onBlur={() => setFocusedField(null)} + style={{ + ...styles.input, + ...(focusedField === 'agencyName' ? styles.inputFocus : {}), + }} + placeholder="Your Agency Name" + required + disabled={isLoading} + /> +
+ +
+ + setEmail(e.target.value)} + onFocus={() => setFocusedField('email')} + onBlur={() => setFocusedField(null)} + style={{ + ...styles.input, + ...(focusedField === 'email' ? styles.inputFocus : {}), + }} + placeholder="you@example.com" + required + disabled={isLoading} + /> +
+ +
+ + setPassword(e.target.value)} + onFocus={() => setFocusedField('password')} + onBlur={() => setFocusedField(null)} + style={{ + ...styles.input, + ...(focusedField === 'password' ? styles.inputFocus : {}), + }} + placeholder="••••••••" + required + disabled={isLoading} + /> +
+ + +
+ + +
+
+ ); +}; + +export default AgencyRegister; diff --git a/dashboard/src/agency/pages/AgencySettings.tsx b/dashboard/src/agency/pages/AgencySettings.tsx new file mode 100644 index 000000000..5bc4ea5cc --- /dev/null +++ b/dashboard/src/agency/pages/AgencySettings.tsx @@ -0,0 +1,286 @@ +import { useEffect, useState } from 'react'; +import { supabase } from '../../lib/supabase'; +import { useAgencyOutlet } from '../components/AgencyLayout'; +import type { Agency } from '../types'; + +type AIProvider = 'openai' | 'gemini' | 'deepseek'; + +export function AgencySettings() { + const { agencyId } = useAgencyOutlet(); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null); + + const [formData, setFormData] = useState({ + agencyName: '', + logoUrl: '', + openwaUrl: '', + openwaApiKey: '', + n8nUrl: '', + n8nApiKey: '', + aiProvider: 'openai' as AIProvider, + aiApiKey: '', + }); + + useEffect(() => { + if (!agencyId) return; + loadAgency(); + }, [agencyId]); + + const loadAgency = async () => { + try { + setLoading(true); + const { data, error } = await supabase + .from('agencies') + .select('*') + .eq('id', agencyId) + .single(); + + if (error) { + console.error('Error loading agency:', error); + } else if (data) { + const agencyData = data as Agency; + setFormData({ + agencyName: agencyData.name || '', + logoUrl: agencyData.logo_url || '', + openwaUrl: agencyData.openwa_url || '', + openwaApiKey: agencyData.openwa_api_key || '', + n8nUrl: agencyData.n8n_url || '', + n8nApiKey: agencyData.n8n_api_key || '', + aiProvider: (agencyData.agency_ai_provider || 'openai') as AIProvider, + aiApiKey: agencyData.agency_ai_api_key || '', + }); + } + } catch (err) { + console.error('Error loading agency:', err); + } finally { + setLoading(false); + } + }; + + const handleInputChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setFormData((prev) => ({ + ...prev, + [name]: value, + })); + }; + + const handleSave = async () => { + if (!agencyId) return; + + try { + setSaving(true); + setMessage(null); + + const { error } = await supabase + .from('agencies') + .update({ + name: formData.agencyName, + logo_url: formData.logoUrl || null, + openwa_url: formData.openwaUrl || null, + openwa_api_key: formData.openwaApiKey || null, + n8n_url: formData.n8nUrl || null, + n8n_api_key: formData.n8nApiKey || null, + agency_ai_provider: formData.aiProvider, + agency_ai_api_key: formData.aiApiKey || null, + }) + .eq('id', agencyId); + + if (error) { + console.error('Error saving agency:', error); + setMessage({ type: 'error', text: 'Failed to save settings' }); + } else { + setMessage({ type: 'success', text: 'Settings saved successfully' }); + loadAgency(); + setTimeout(() => setMessage(null), 3000); + } + } catch (err) { + console.error('Error saving agency:', err); + setMessage({ type: 'error', text: 'Failed to save settings' }); + } finally { + setSaving(false); + } + }; + + const SettingsSection = ({ + title, + fields, + }: { + title: string; + fields: Array<{ + label: string; + name: string; + type?: string; + placeholder?: string; + options?: { label: string; value: string }[]; + }>; + }) => ( +
+

+ {title} +

+
+ {fields.map((field) => ( +
+ + {field.options ? ( + + ) : ( + + )} +
+ ))} +
+
+ ); + + if (loading) { + return ( +
+ Loading settings... +
+ ); + } + + return ( +
+
+

+ Agency Settings +

+

Configure your agency and integrations

+
+ + {message && ( +
+ {message.text} +
+ )} + + + + + + + + + +
+ +
+
+ ); +} diff --git a/dashboard/src/agency/pages/AiAgents.tsx b/dashboard/src/agency/pages/AiAgents.tsx new file mode 100644 index 000000000..532babe95 --- /dev/null +++ b/dashboard/src/agency/pages/AiAgents.tsx @@ -0,0 +1,776 @@ +import { useState, useEffect } from 'react'; +import { useOutletContext } from 'react-router-dom'; +import { Plus, CreditCard as Edit2, Trash2, Send, MessageCircle } from 'lucide-react'; +import { supabase } from '../../lib/supabase'; +import { agencyAiChat } from '../lib/agencyApi'; +import type { AiAgent } from '../types'; + +interface OutletContext { + subAccountId: string; + agency: any; +} + +const PROVIDERS = { + OpenAI: ['gpt-4o', 'gpt-4o-mini', 'gpt-3.5-turbo'], + Gemini: ['gemini-2.0-flash', 'gemini-1.5-pro'], + DeepSeek: ['deepseek-chat', 'deepseek-reasoner'], +}; + +interface ChatMessage { + role: 'user' | 'assistant'; + content: string; +} + +interface NewAgentFormData { + name: string; + channel: 'WhatsApp' | 'WebChat'; + provider: 'OpenAI' | 'Gemini' | 'DeepSeek'; + model: string; + temperature: number; + useAgencyKey: boolean; + customApiKey: string; + systemPrompt: string; + isActive: boolean; +} + +export function AiAgents() { + const { subAccountId, agency } = useOutletContext(); + const [agents, setAgents] = useState([]); + const [showModal, setShowModal] = useState(false); + const [editingAgent, setEditingAgent] = useState(null); + const [chatMessages, setChatMessages] = useState([]); + const [chatInput, setChatInput] = useState(''); + const [loadingChat, setLoadingChat] = useState(false); + const [formData, setFormData] = useState({ + name: '', + channel: 'WhatsApp', + provider: 'OpenAI', + model: 'gpt-4o', + temperature: 0.7, + useAgencyKey: true, + customApiKey: '', + systemPrompt: '', + isActive: true, + }); + + useEffect(() => { + fetchAgents(); + }, [subAccountId]); + + const fetchAgents = async () => { + try { + const { data, error } = await supabase + .from('ai_agents') + .select('*') + .eq('sub_account_id', subAccountId) + .order('created_at', { ascending: false }); + + if (error) throw error; + setAgents(data || []); + } catch (err) { + console.error('Error fetching agents:', err); + } + }; + + const handleOpenModal = (agent?: AiAgent) => { + if (agent) { + setEditingAgent(agent); + setFormData({ + name: agent.name, + channel: agent.channel as 'WhatsApp' | 'WebChat', + provider: agent.ai_provider as 'OpenAI' | 'Gemini' | 'DeepSeek', + model: agent.ai_model, + temperature: agent.temperature, + useAgencyKey: agent.use_agency_key, + customApiKey: agent.custom_api_key || '', + systemPrompt: agent.system_prompt, + isActive: agent.is_active, + }); + } else { + setEditingAgent(null); + setFormData({ + name: '', + channel: 'WhatsApp', + provider: 'OpenAI', + model: 'gpt-4o', + temperature: 0.7, + useAgencyKey: true, + customApiKey: '', + systemPrompt: '', + isActive: true, + }); + } + setChatMessages([]); + setChatInput(''); + setShowModal(true); + }; + + const handleCloseModal = () => { + setShowModal(false); + setEditingAgent(null); + }; + + const handleSaveAgent = async () => { + try { + const payload = { + name: formData.name, + channel: formData.channel.toLowerCase(), + ai_provider: formData.provider, + ai_model: formData.model, + temperature: formData.temperature, + use_agency_key: formData.useAgencyKey, + custom_api_key: formData.useAgencyKey ? null : formData.customApiKey, + system_prompt: formData.systemPrompt, + is_active: formData.isActive, + }; + + if (editingAgent) { + const { error } = await supabase + .from('ai_agents') + .update(payload) + .eq('id', editingAgent.id); + + if (error) throw error; + } else { + const { error } = await supabase.from('ai_agents').insert({ + ...payload, + sub_account_id: subAccountId, + }); + + if (error) throw error; + } + + await fetchAgents(); + handleCloseModal(); + } catch (err) { + console.error('Error saving agent:', err); + } + }; + + const handleToggleActive = async (agent: AiAgent) => { + try { + const { error } = await supabase + .from('ai_agents') + .update({ is_active: !agent.is_active }) + .eq('id', agent.id); + + if (error) throw error; + await fetchAgents(); + } catch (err) { + console.error('Error toggling agent:', err); + } + }; + + const handleDeleteAgent = async (agentId: string) => { + if (!window.confirm('Are you sure you want to delete this agent?')) return; + + try { + const { error } = await supabase + .from('ai_agents') + .delete() + .eq('id', agentId); + + if (error) throw error; + await fetchAgents(); + } catch (err) { + console.error('Error deleting agent:', err); + } + }; + + const handleSendChatMessage = async () => { + if (!chatInput.trim()) return; + + const userMessage: ChatMessage = { role: 'user', content: chatInput }; + setChatMessages([...chatMessages, userMessage]); + setChatInput(''); + setLoadingChat(true); + + try { + const data = await agencyAiChat.chat({ + messages: [...chatMessages, userMessage], + provider: formData.provider.toLowerCase(), + model: formData.model, + apiKey: formData.useAgencyKey ? (agency?.agency_ai_api_key ?? '') : formData.customApiKey, + systemPrompt: formData.systemPrompt, + }); + + if (data.reply) { + setChatMessages((prev) => [ + ...prev, + { role: 'assistant', content: data.reply }, + ]); + } + } catch (err) { + console.error('Error sending message:', err); + } finally { + setLoadingChat(false); + } + }; + + const models = PROVIDERS[formData.provider] || []; + + const getChannelColor = (channel: string) => { + return channel.toLowerCase() === 'whatsapp' ? '#10b981' : '#3b82f6'; + }; + + return ( +
+
+

+ AI Agents +

+ +
+ +
+ {agents.map((agent) => ( +
+

+ {agent.name} +

+ +
+ + {agent.channel} + +
+ +
+

+ Model: {agent.ai_model} +

+

+ Provider: {agent.ai_provider} +

+

+ Temp: {agent.temperature} +

+
+ +
+ +
+ +
+ + +
+
+ ))} +
+ + {showModal && ( +
+
e.stopPropagation()} + > + {/* LEFT: Form */} +
+

+ {editingAgent ? 'Edit Agent' : 'Create New Agent'} +

+ +
+ + + setFormData({ ...formData, name: e.target.value }) + } + style={{ + width: '100%', + padding: '10px 12px', + border: '1px solid #e5e5e5', + borderRadius: '6px', + fontSize: '14px', + boxSizing: 'border-box', + }} + placeholder="e.g., Customer Support Bot" + /> +
+ +
+ +
+ {['WhatsApp', 'WebChat'].map((ch) => ( + + ))} +
+
+ +
+ +
+ {['OpenAI', 'Gemini', 'DeepSeek'].map((prov) => ( + + ))} +
+
+ +
+ + +
+ +
+ + + setFormData({ + ...formData, + temperature: parseFloat(e.target.value), + }) + } + style={{ width: '100%', cursor: 'pointer' }} + /> +
+ +
+ +
+ + {!formData.useAgencyKey && ( +
+ + + setFormData({ + ...formData, + customApiKey: e.target.value, + }) + } + style={{ + width: '100%', + padding: '10px 12px', + border: '1px solid #e5e5e5', + borderRadius: '6px', + fontSize: '14px', + boxSizing: 'border-box', + }} + placeholder="sk-..." + /> +
+ )} + +
+ +