ritesh19180 commited on
Commit
4bb3f21
·
verified ·
1 Parent(s): 7d2f8a2

Upload folder using huggingface_hub

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +23 -0
  2. .gitignore +16 -1
  3. Dockerfile +23 -19
  4. Frontend/eslint.config.js +21 -0
  5. Frontend/jsconfig.json +1 -0
  6. Frontend/package-lock.json +0 -0
  7. Frontend/package.json +18 -4
  8. Frontend/src/App.jsx +122 -152
  9. Frontend/src/admin/components/AdminSidebar.jsx +3 -1
  10. Frontend/src/admin/components/TicketTable.jsx +13 -3
  11. Frontend/src/admin/pages/AdminDashboard.jsx +143 -19
  12. Frontend/src/admin/pages/AdminSettings.jsx +127 -3
  13. Frontend/src/admin/pages/AdminTicketDetail.jsx +32 -2
  14. Frontend/src/admin/pages/AdminTickets.jsx +38 -37
  15. Frontend/src/components/shared/BugReportWidget.jsx +3 -15
  16. Frontend/src/components/shared/ProtectedRoute.jsx +3 -26
  17. Frontend/src/components/shared/TicketChat.jsx +341 -24
  18. Frontend/src/components/shared/WebhookSettings.jsx +227 -0
  19. Frontend/src/config.js +3 -6
  20. Frontend/src/index.css +48 -8
  21. Frontend/src/legacy_ui/Dashboard.jsx +2 -2
  22. Frontend/src/pages/AboutUs.jsx +382 -60
  23. Frontend/src/pages/AdminSignup.jsx +23 -32
  24. Frontend/src/pages/Changelog.jsx +292 -111
  25. Frontend/src/pages/LandingPage.jsx +199 -112
  26. Frontend/src/pages/Login.jsx +68 -25
  27. Frontend/src/pages/Signup.jsx +18 -30
  28. Frontend/src/services/aiAssistant.js +110 -81
  29. Frontend/src/services/api.js +67 -22
  30. Frontend/src/store/authStore.js +152 -48
  31. Frontend/src/store/ticketStore.js +60 -14
  32. Frontend/src/user/components/QuickActions.jsx +6 -3
  33. Frontend/src/user/components/RecentTickets.jsx +5 -0
  34. Frontend/src/user/components/TopNav.jsx +46 -42
  35. Frontend/src/user/pages/AIProcessing.jsx +14 -8
  36. Frontend/src/user/pages/AutoResolveChat.jsx +56 -2
  37. Frontend/src/user/pages/CreateTicket.jsx +236 -87
  38. Frontend/src/user/pages/Dashboard.jsx +2 -1
  39. Frontend/src/user/pages/DuplicateDetection.jsx +47 -30
  40. Frontend/src/user/pages/MyTickets.jsx +13 -0
  41. Frontend/src/user/pages/TicketDetail.jsx +27 -1
  42. Frontend/src/user/pages/TicketTracking.jsx +15 -6
  43. Frontend/vite.config.js +1 -1
  44. MobileApp/App.js +207 -35
  45. MobileApp/app.json +5 -3
  46. MobileApp/assets/adaptive-icon.png +2 -2
  47. MobileApp/assets/icon.png +2 -2
  48. MobileApp/assets/logo_v1.png +2 -2
  49. MobileApp/assets/splash-icon.png +2 -2
  50. MobileApp/package-lock.json +27 -18
.gitattributes CHANGED
@@ -2,6 +2,29 @@
2
  *.pt filter=lfs diff=lfs merge=lfs -text
3
  *.bin filter=lfs diff=lfs merge=lfs -text
4
  *.pkl filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
  Frontend/public/favicon.jpeg filter=lfs diff=lfs merge=lfs -text
6
  Frontend/public/favicon.png filter=lfs diff=lfs merge=lfs -text
7
  Frontend/public/team/pragati_tiwari.jpg filter=lfs diff=lfs merge=lfs -text
 
2
  *.pt filter=lfs diff=lfs merge=lfs -text
3
  *.bin filter=lfs diff=lfs merge=lfs -text
4
  *.pkl filter=lfs diff=lfs merge=lfs -text
5
+ .tmp-ci-venv/Lib/site-packages/dateutil/zoneinfo/dateutil-zoneinfo.tar.gz filter=lfs diff=lfs merge=lfs -text
6
+ .tmp-ci-venv/Lib/site-packages/pip/_vendor/distlib/t64-arm.exe filter=lfs diff=lfs merge=lfs -text
7
+ .tmp-ci-venv/Lib/site-packages/pip/_vendor/distlib/t64.exe filter=lfs diff=lfs merge=lfs -text
8
+ .tmp-ci-venv/Lib/site-packages/pip/_vendor/distlib/w64-arm.exe filter=lfs diff=lfs merge=lfs -text
9
+ .tmp-ci-venv/Lib/site-packages/pip/_vendor/distlib/w64.exe filter=lfs diff=lfs merge=lfs -text
10
+ .tmp-ci-venv/Lib/site-packages/setuptools/cli-arm64.exe filter=lfs diff=lfs merge=lfs -text
11
+ .tmp-ci-venv/Lib/site-packages/setuptools/gui-arm64.exe filter=lfs diff=lfs merge=lfs -text
12
+ .tmp-ci-venv/Scripts/dotenv.exe filter=lfs diff=lfs merge=lfs -text
13
+ .tmp-ci-venv/Scripts/fastapi.exe filter=lfs diff=lfs merge=lfs -text
14
+ .tmp-ci-venv/Scripts/httpx.exe filter=lfs diff=lfs merge=lfs -text
15
+ .tmp-ci-venv/Scripts/idna.exe filter=lfs diff=lfs merge=lfs -text
16
+ .tmp-ci-venv/Scripts/markdown-it.exe filter=lfs diff=lfs merge=lfs -text
17
+ .tmp-ci-venv/Scripts/normalizer.exe filter=lfs diff=lfs merge=lfs -text
18
+ .tmp-ci-venv/Scripts/pip.exe filter=lfs diff=lfs merge=lfs -text
19
+ .tmp-ci-venv/Scripts/pip3.11.exe filter=lfs diff=lfs merge=lfs -text
20
+ .tmp-ci-venv/Scripts/pip3.exe filter=lfs diff=lfs merge=lfs -text
21
+ .tmp-ci-venv/Scripts/pygmentize.exe filter=lfs diff=lfs merge=lfs -text
22
+ .tmp-ci-venv/Scripts/pyiceberg.exe filter=lfs diff=lfs merge=lfs -text
23
+ .tmp-ci-venv/Scripts/python.exe filter=lfs diff=lfs merge=lfs -text
24
+ .tmp-ci-venv/Scripts/pythonw.exe filter=lfs diff=lfs merge=lfs -text
25
+ .tmp-ci-venv/Scripts/uvicorn.exe filter=lfs diff=lfs merge=lfs -text
26
+ .tmp-ci-venv/Scripts/watchfiles.exe filter=lfs diff=lfs merge=lfs -text
27
+ .tmp-ci-venv/Scripts/websockets.exe filter=lfs diff=lfs merge=lfs -text
28
  Frontend/public/favicon.jpeg filter=lfs diff=lfs merge=lfs -text
29
  Frontend/public/favicon.png filter=lfs diff=lfs merge=lfs -text
30
  Frontend/public/team/pragati_tiwari.jpg filter=lfs diff=lfs merge=lfs -text
.gitignore CHANGED
@@ -4,7 +4,10 @@ __pycache__/
4
  *$py.class
5
  venv/
6
  env/
7
- .env
 
 
 
8
 
9
  # Models (Keep them if they are small, but here they are large.
10
  # We should probably only keep the ones we actually use if possible,
@@ -29,3 +32,15 @@ dist/
29
  docs/helpdesk*.pdf
30
  docs/logs/
31
  *.log
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  *$py.class
5
  venv/
6
  env/
7
+ # Ignore all env files anywhere in the repo
8
+ **/.env
9
+ **/.env.*
10
+
11
 
12
  # Models (Keep them if they are small, but here they are large.
13
  # We should probably only keep the ones we actually use if possible,
 
32
  docs/helpdesk*.pdf
33
  docs/logs/
34
  *.log
35
+
36
+ # Scratch and local cache
37
+ scratch/
38
+ .npm-cache/
39
+
40
+ # GSSoC Score Tracking and Temporary Scripts
41
+ scratch/
42
+ .npm-cache/
43
+ *.tmp
44
+ ../MOBILE_SETUP_TEMP.md
45
+ ../CI_YML_TEMP.yml
46
+ ../ENV_EXAMPLE_TEMP.example
Dockerfile CHANGED
@@ -1,36 +1,40 @@
1
- # Use an official Python runtime as a parent image
2
- FROM python:3.10-slim
3
 
4
- LABEL version="1.1.1" rebuild_trigger="2026-03-08-2032"
5
 
6
- # Set the working directory to /app
7
- WORKDIR /app
8
-
9
- # Install system dependencies required for EasyOCR and OpenCV
10
- RUN apt-get update && apt-get install -y \
11
- libgl1 \
12
- libglib2.0-0 \
13
  git \
14
  && rm -rf /var/lib/apt/lists/*
15
 
16
- # Copy the requirements file into the container
17
  COPY requirements.txt .
 
 
 
18
 
19
- # Install dependencies (no-cache-dir keeps the docker image smaller)
20
- RUN pip install --no-cache-dir -r requirements.txt
21
 
22
- # Copy all the remaining files into the container as a 'backend' directory
23
- # This allows absolute imports like 'from backend.services...' to work perfectly
 
 
 
 
 
 
 
24
  COPY . /app/backend
25
 
26
- # Tell Python where to look for modules (so it can find the 'backend' folder)
27
- ENV PYTHONPATH=/app
 
28
 
29
- # Expose port 7860 (Hugging Face Spaces default)
30
  EXPOSE 7860
31
 
32
  HEALTHCHECK --interval=30s --timeout=5s --start-period=120s --retries=3 \
33
  CMD ["python", "backend/healthcheck.py"]
34
 
35
- # Run the FastAPI server via Uvicorn
 
36
  CMD ["uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", "7860"]
 
1
+ # syntax=docker/dockerfile:1
 
2
 
3
+ FROM python:3.10-slim AS builder
4
 
5
+ WORKDIR /build
6
+ RUN apt-get update && apt-get install -y --no-install-recommends \
 
 
 
 
 
7
  git \
8
  && rm -rf /var/lib/apt/lists/*
9
 
 
10
  COPY requirements.txt .
11
+ RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
12
+
13
+ FROM python:3.10-slim AS runtime
14
 
15
+ LABEL org.opencontainers.image.title="helpdesk-backend"
16
+ LABEL org.opencontainers.image.description="HELPDESK.AI FastAPI backend (multi-stage)"
17
 
18
+ WORKDIR /app
19
+
20
+ RUN apt-get update && apt-get install -y --no-install-recommends \
21
+ libgl1 \
22
+ libglib2.0-0 \
23
+ && rm -rf /var/lib/apt/lists/* \
24
+ && useradd --create-home --shell /usr/sbin/nologin appuser
25
+
26
+ COPY --from=builder /install /usr/local
27
  COPY . /app/backend
28
 
29
+ ENV PYTHONPATH=/app \
30
+ PYTHONDONTWRITEBYTECODE=1 \
31
+ PYTHONUNBUFFERED=1
32
 
 
33
  EXPOSE 7860
34
 
35
  HEALTHCHECK --interval=30s --timeout=5s --start-period=120s --retries=3 \
36
  CMD ["python", "backend/healthcheck.py"]
37
 
38
+ USER appuser
39
+
40
  CMD ["uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", "7860"]
Frontend/eslint.config.js CHANGED
@@ -35,4 +35,25 @@ export default defineConfig([
35
  'react-refresh/only-export-components': 'off',
36
  },
37
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  ])
 
35
  'react-refresh/only-export-components': 'off',
36
  },
37
  },
38
+ {
39
+ files: [
40
+ '**/*.{test,spec}.{js,jsx}',
41
+ '**/__tests__/**/*.{js,jsx}',
42
+ 'jest.setup.js',
43
+ 'jest.fileMock.js',
44
+ ],
45
+ languageOptions: {
46
+ globals: {
47
+ ...globals.browser,
48
+ ...globals.node,
49
+ ...globals.jest,
50
+ },
51
+ },
52
+ },
53
+ {
54
+ files: ['jest.fileMock.js'],
55
+ languageOptions: {
56
+ sourceType: 'commonjs',
57
+ },
58
+ },
59
  ])
Frontend/jsconfig.json CHANGED
@@ -1,6 +1,7 @@
1
  {
2
  "compilerOptions": {
3
  "baseUrl": ".",
 
4
  "paths": {
5
  "@/*": [
6
  "./src/*"
 
1
  {
2
  "compilerOptions": {
3
  "baseUrl": ".",
4
+ "ignoreDeprecations": "6.0",
5
  "paths": {
6
  "@/*": [
7
  "./src/*"
Frontend/package-lock.json CHANGED
The diff for this file is too large to render. See raw diff
 
Frontend/package.json CHANGED
@@ -6,10 +6,12 @@
6
  "scripts": {
7
  "dev": "vite",
8
  "build": "vite build",
9
- "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
 
10
  "preview": "vite preview",
11
  "prepare": "husky install",
12
- "format": "prettier --write ."
 
13
  },
14
  "dependencies": {
15
  "@google/generative-ai": "^0.24.1",
@@ -23,7 +25,7 @@
23
  "class-variance-authority": "^0.7.1",
24
  "clsx": "^2.1.1",
25
  "date-fns": "^4.1.0",
26
- "framer-motion": "^12.34.2",
27
  "html2canvas": "^1.4.1",
28
  "lucide-react": "^0.574.0",
29
  "react": "^19.2.0",
@@ -42,11 +44,18 @@
42
  "zustand": "^5.0.11"
43
  },
44
  "devDependencies": {
 
 
 
45
  "@eslint/js": "^9.39.1",
 
 
 
46
  "@types/react": "^19.2.7",
47
  "@types/react-dom": "^19.2.3",
48
  "@vitejs/plugin-react": "^5.1.1",
49
  "autoprefixer": "^10.4.24",
 
50
  "eslint": "^9.39.2",
51
  "eslint-config-prettier": "^10.1.8",
52
  "eslint-plugin-react": "^7.37.5",
@@ -54,11 +63,16 @@
54
  "eslint-plugin-react-refresh": "^0.4.24",
55
  "globals": "^16.5.0",
56
  "husky": "^9.1.7",
 
 
 
57
  "lint-staged": "^16.2.7",
58
  "postcss": "^8.5.6",
59
  "prettier": "^3.8.1",
60
  "tailwindcss": "^3.4.19",
61
  "vite": "^7.3.1"
62
  },
63
- "lint-staged": "{'*.{js,jsx}': ['eslint --fix', 'prettier --write']}"
 
 
64
  }
 
6
  "scripts": {
7
  "dev": "vite",
8
  "build": "vite build",
9
+ "lint": "eslint . --report-unused-disable-directives --max-warnings 0",
10
+ "check:ai-secrets": "node ../scripts/check-frontend-secrets.mjs",
11
  "preview": "vite preview",
12
  "prepare": "husky install",
13
+ "format": "prettier --write .",
14
+ "test": "jest"
15
  },
16
  "dependencies": {
17
  "@google/generative-ai": "^0.24.1",
 
25
  "class-variance-authority": "^0.7.1",
26
  "clsx": "^2.1.1",
27
  "date-fns": "^4.1.0",
28
+ "framer-motion": "^12.39.0",
29
  "html2canvas": "^1.4.1",
30
  "lucide-react": "^0.574.0",
31
  "react": "^19.2.0",
 
44
  "zustand": "^5.0.11"
45
  },
46
  "devDependencies": {
47
+ "@babel/core": "^7.24.0",
48
+ "@babel/preset-env": "^7.24.0",
49
+ "@babel/preset-react": "^7.24.0",
50
  "@eslint/js": "^9.39.1",
51
+ "@testing-library/dom": "^10.4.1",
52
+ "@testing-library/jest-dom": "^6.6.3",
53
+ "@testing-library/react": "^16.2.0",
54
  "@types/react": "^19.2.7",
55
  "@types/react-dom": "^19.2.3",
56
  "@vitejs/plugin-react": "^5.1.1",
57
  "autoprefixer": "^10.4.24",
58
+ "babel-jest": "^29.7.0",
59
  "eslint": "^9.39.2",
60
  "eslint-config-prettier": "^10.1.8",
61
  "eslint-plugin-react": "^7.37.5",
 
63
  "eslint-plugin-react-refresh": "^0.4.24",
64
  "globals": "^16.5.0",
65
  "husky": "^9.1.7",
66
+ "identity-obj-proxy": "^3.0.0",
67
+ "jest": "^29.7.0",
68
+ "jest-environment-jsdom": "^29.7.0",
69
  "lint-staged": "^16.2.7",
70
  "postcss": "^8.5.6",
71
  "prettier": "^3.8.1",
72
  "tailwindcss": "^3.4.19",
73
  "vite": "^7.3.1"
74
  },
75
+ "lint-staged": {
76
+ "*.{js,jsx}": ["eslint --fix", "prettier --write"]
77
+ }
78
  }
Frontend/src/App.jsx CHANGED
@@ -5,88 +5,83 @@ import {
5
  Navigate,
6
  useLocation
7
  } from "react-router-dom";
8
- import React, { useEffect } from "react";
9
- import { AnimatePresence } from "framer-motion";
10
- import { NotFound } from "./components/ui/not-found-2";
11
  import useTicketStore from "./store/ticketStore";
12
- import Toaster from "./components/shared/Toaster";
13
- import BugReportWidget from "./components/shared/BugReportWidget";
14
  import useRealtimeNotifications from "./hooks/useRealtimeNotifications";
15
-
16
- // Auth Components
17
- import Login from "./pages/Login";
18
- import ForgotPassword from "./pages/ForgotPassword";
19
- import ResetPassword from "./pages/ResetPassword";
20
- import Signup from "./pages/Signup";
21
- import AdminSignup from "./pages/AdminSignup";
22
- import AdminLobby from "./pages/AdminLobby";
23
- import UserLobby from "./pages/UserLobby";
24
- import LandingPage from "./pages/LandingPage";
25
- import ContactSales from "./pages/ContactSales";
26
-
27
- // Legacy components
28
- import DuplicateDetection from "./user/pages/DuplicateDetection";
29
- import AutoResolveChat from "./user/pages/AutoResolveChat";
30
- import Resolved from "./user/pages/Resolved";
31
- import TicketTracking from "./user/pages/TicketTracking";
32
- // Layouts
33
- import UserLayout from "./user/UserLayout";
34
- import AdminLayout from "./admin/layout/AdminLayout";
35
-
36
- // User Pages
37
- import Dashboard from "./user/pages/Dashboard";
38
- import CreateTicket from "./user/pages/CreateTicket";
39
- import MyTickets from "./user/pages/MyTickets";
40
- import TicketResult from "./user/pages/TicketResult";
41
- import Profile from "./user/pages/Profile";
42
- import TicketDetail from "./user/pages/TicketDetail";
43
- import TicketProcessing from "./user/pages/AIProcessing"; // Renamed generic import just in case, but keeping AIProcessing
44
- import AIProcessing from "./user/pages/AIProcessing";
45
- import AIUnderstanding from "./user/pages/AIUnderstanding";
46
- import Notifications from "./user/pages/Notifications";
47
- import Help from "./user/pages/Help";
48
- import DocsPortal from "./docs/pages/DocsPortal";
49
-
50
- // New Showcase Pages
51
- import ApiReference from "./pages/ApiReference";
52
- import Changelog from "./pages/Changelog";
53
- import StatusPage from "./pages/StatusPage";
54
- import AboutUs from "./pages/AboutUs";
55
- import Careers from "./pages/Careers";
56
- import CookiePolicy from "./pages/legal/CookiePolicy";
57
-
58
- // NEW Admin Pages (Refactored)
59
- import AdminDashboard from "./admin/pages/AdminDashboard";
60
- import AdminTickets from "./admin/pages/AdminTickets";
61
- import AdminTicketDetail from "./admin/pages/AdminTicketDetail";
62
- import AdminUsers from "./admin/pages/AdminUsers";
63
- import AdminAnalytics from "./admin/pages/AdminAnalytics";
64
- import AdminProfile from "./admin/pages/AdminProfile";
65
- import AdminSettings from "./admin/pages/AdminSettings";
66
- import MasterBugReports from "./master-admin/pages/MasterBugReports";
67
-
68
- // Feature Pages
69
- import AutoCategorizationFeature from "./pages/features/AutoCategorizationFeature";
70
- import PriorityDetectionFeature from "./pages/features/PriorityDetectionFeature";
71
- import SmartResolutionFeature from "./pages/features/SmartResolutionFeature";
72
-
73
- // Legal Pages
74
- import TermsOfService from "./pages/legal/TermsOfService";
75
- import PrivacyPolicy from "./pages/legal/PrivacyPolicy";
76
- import Security from "./pages/legal/Security";
77
  import AdminProtectedRoute from "./components/shared/AdminProtectedRoute";
78
  import MasterAdminProtectedRoute from "./components/shared/MasterAdminProtectedRoute";
79
  import ProtectedRoute from "./components/shared/ProtectedRoute";
80
  import useAuthStore from "./store/authStore";
81
  import NotApproved from "./pages/NotApproved";
82
-
83
- // Master Admin Components
84
- import MasterAdminLogin from "./pages/MasterAdminLogin";
85
- import MasterAdminLayout from "./master-admin/layout/MasterAdminLayout";
86
- import MasterAdminDashboard from "./master-admin/pages/MasterAdminDashboard";
87
- import PendingAdminRequests from "./master-admin/pages/PendingAdminRequests";
88
- import AllCompanies from "./master-admin/pages/AllCompanies";
89
- import AllAdmins from "./master-admin/pages/AllAdmins";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
 
91
 
92
  function TitleUpdater() {
@@ -104,6 +99,7 @@ function TitleUpdater() {
104
  else if (path.startsWith('/admin/analytics')) title = 'Analytics | Admin';
105
  else if (path.startsWith('/admin/profile')) title = 'Admin Profile';
106
  else if (path.startsWith('/admin/settings')) title = 'Settings | Admin';
 
107
  // Master Admin Routes
108
  else if (path.startsWith('/master-admin/dashboard')) title = 'Master Dashboard';
109
  else if (path.startsWith('/master-admin/admin-requests')) title = 'Pending Requests | Master Admin';
@@ -119,19 +115,13 @@ function TitleUpdater() {
119
  else if (path === '/my-tickets') title = 'My Tickets';
120
  else if (path === '/profile') title = 'User Profile';
121
  else if (path === '/notifications') title = 'Notifications';
122
- else if (path === '/docs') title = 'Documentation';
123
- else if (path === '/api-reference') title = 'API Reference';
124
- else if (path === '/changelog') title = 'Changelog';
125
- else if (path === '/status') title = 'Status';
126
- else if (path === '/about') title = 'About Us';
127
- else if (path === '/careers') title = 'Careers';
128
- else if (path === '/cookie-policy') title = 'Cookie Policy';
129
  // Public / Lobby Routes
130
  else if (path === '/login') title = 'Login';
131
  else if (path === '/signup') title = 'Create Account';
132
  else if (path === '/admin-signup') title = 'Admin Signup';
133
  else if (path === '/user-lobby') title = 'User Lobby';
134
  else if (path === '/admin-lobby') title = 'Admin Lobby';
 
135
  else if (path === '/') title = 'Welcome';
136
 
137
  document.title = title === 'HELPDESK.AI' ? title : `${title} | HELPDESK.AI`;
@@ -206,10 +196,11 @@ function AppLayout() {
206
  <Route path="/admin/analytics" element={<AdminAnalytics />} />
207
  <Route path="/admin/profile" element={<AdminProfile />} />
208
  <Route path="/admin/settings" element={<AdminSettings />} />
 
209
  </Route>
210
  </Route>
211
 
212
- <Route path="*" element={<NotFound />} />
213
  </Routes>
214
  </>
215
  );
@@ -223,81 +214,60 @@ function App() {
223
  initialize();
224
  }, [initialize]);
225
 
226
- const isDocsSubdomain = window.location.hostname.startsWith('docs.');
227
-
228
- if (isDocsSubdomain) {
229
- return (
230
- <BrowserRouter>
231
- <TitleUpdater />
232
- <ScrollToTop />
233
- <Toaster />
234
- <BugReportWidget />
235
- <Routes>
236
- <Route path="/" element={<DocsPortal />} />
237
- <Route path="/docs" element={<Navigate to="/" replace />} />
238
- <Route path="/api-reference" element={<ApiReference />} />
239
- <Route path="/changelog" element={<Changelog />} />
240
- <Route path="/status" element={<StatusPage />} />
241
- <Route path="*" element={<DocsPortal />} />
242
- </Routes>
243
- </BrowserRouter>
244
- );
245
- }
246
-
247
  return (
248
  <BrowserRouter>
249
  <TitleUpdater />
250
  <ScrollToTop />
251
- <Toaster />
252
- <BugReportWidget />
253
- <Routes>
254
- {/* Public */}
255
- <Route path="/" element={<LandingPage />} />
256
- <Route path="/login" element={<Login />} />
257
- <Route path="/forgot-password" element={<ForgotPassword />} />
258
- <Route path="/reset-password" element={<ResetPassword />} />
259
- <Route path="/signup" element={<Signup />} />
260
- <Route path="/admin-signup" element={<AdminSignup />} />
261
- <Route path="/admin-lobby" element={<AdminLobby />} />
262
- <Route path="/user-lobby" element={<UserLobby />} />
263
- <Route path="/not-approved" element={<NotApproved />} />
264
- <Route path="/contact-sales" element={<ContactSales />} />
265
- <Route path="/docs" element={<DocsPortal />} />
266
- <Route path="/api-reference" element={<ApiReference />} />
267
- <Route path="/changelog" element={<Changelog />} />
268
- <Route path="/status" element={<StatusPage />} />
269
- <Route path="/about" element={<AboutUs />} />
270
- <Route path="/careers" element={<Careers />} />
271
- <Route path="/cookie-policy" element={<CookiePolicy />} />
272
-
273
- {/* Feature Pages */}
274
- <Route path="/features/categorization" element={<AutoCategorizationFeature />} />
275
- <Route path="/features/priority" element={<PriorityDetectionFeature />} />
276
- <Route path="/features/resolution" element={<SmartResolutionFeature />} />
277
-
278
- {/* Legal Pages */}
279
- <Route path="/terms" element={<TermsOfService />} />
280
- <Route path="/privacy" element={<PrivacyPolicy />} />
281
- <Route path="/security" element={<Security />} />
282
-
283
- {/* Master Admin Portal */}
284
- <Route path="/master-admin-login" element={<MasterAdminLogin />} />
285
-
286
- <Route element={<MasterAdminProtectedRoute />}>
287
- <Route element={<MasterAdminLayout />}>
288
- <Route path="/master-admin/dashboard" element={<MasterAdminDashboard />} />
289
- <Route path="/master-admin/admin-requests" element={<PendingAdminRequests />} />
290
- <Route path="/master-admin/companies" element={<AllCompanies />} />
291
- <Route path="/master-admin/all-admins" element={<AllAdmins />} />
292
- <Route path="/master-admin/bug-reports" element={<MasterBugReports />} />
293
  </Route>
294
- </Route>
295
 
296
- {/* Protected */}
297
- <Route element={<ProtectedRoute />}>
298
- <Route path="/*" element={<AppLayout />} />
299
- </Route>
300
- </Routes>
 
301
  </BrowserRouter>
302
  );
303
  }
 
5
  Navigate,
6
  useLocation
7
  } from "react-router-dom";
8
+ import React, { Suspense, useEffect, lazy } from "react";
 
 
9
  import useTicketStore from "./store/ticketStore";
 
 
10
  import useRealtimeNotifications from "./hooks/useRealtimeNotifications";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  import AdminProtectedRoute from "./components/shared/AdminProtectedRoute";
12
  import MasterAdminProtectedRoute from "./components/shared/MasterAdminProtectedRoute";
13
  import ProtectedRoute from "./components/shared/ProtectedRoute";
14
  import useAuthStore from "./store/authStore";
15
  import NotApproved from "./pages/NotApproved";
16
+ const Login = lazy(() => import("./pages/Login"));
17
+ const AuthCallback = lazy(() => import("./pages/AuthCallback"));
18
+ const ForgotPassword = lazy(() => import("./pages/ForgotPassword"));
19
+ const ResetPassword = lazy(() => import("./pages/ResetPassword"));
20
+ const Signup = lazy(() => import("./pages/Signup"));
21
+ const AdminSignup = lazy(() => import("./pages/AdminSignup"));
22
+ const AdminLobby = lazy(() => import("./pages/AdminLobby"));
23
+ const UserLobby = lazy(() => import("./pages/UserLobby"));
24
+ const LandingPage = lazy(() => import("./pages/LandingPage"));
25
+ const ContactSales = lazy(() => import("./pages/ContactSales"));
26
+
27
+ const DuplicateDetection = lazy(() => import("./user/pages/DuplicateDetection"));
28
+ const AutoResolveChat = lazy(() => import("./user/pages/AutoResolveChat"));
29
+ const Resolved = lazy(() => import("./user/pages/Resolved"));
30
+ const TicketTracking = lazy(() => import("./user/pages/TicketTracking"));
31
+
32
+ const UserLayout = lazy(() => import("./user/UserLayout"));
33
+ const AdminLayout = lazy(() => import("./admin/layout/AdminLayout"));
34
+
35
+ const Dashboard = lazy(() => import("./user/pages/Dashboard"));
36
+ const CreateTicket = lazy(() => import("./user/pages/CreateTicket"));
37
+ const MyTickets = lazy(() => import("./user/pages/MyTickets"));
38
+ const TicketResult = lazy(() => import("./user/pages/TicketResult"));
39
+ const Profile = lazy(() => import("./user/pages/Profile"));
40
+ const TicketDetail = lazy(() => import("./user/pages/TicketDetail"));
41
+ const AIProcessing = lazy(() => import("./user/pages/AIProcessing"));
42
+ const AIUnderstanding = lazy(() => import("./user/pages/AIUnderstanding"));
43
+ const Notifications = lazy(() => import("./user/pages/Notifications"));
44
+ const Help = lazy(() => import("./user/pages/Help"));
45
+
46
+ const AdminDashboard = lazy(() => import("./admin/pages/AdminDashboard"));
47
+ const AdminTickets = lazy(() => import("./admin/pages/AdminTickets"));
48
+ const AdminTicketDetail = lazy(() => import("./admin/pages/AdminTicketDetail"));
49
+ const AdminUsers = lazy(() => import("./admin/pages/AdminUsers"));
50
+ const AdminAnalytics = lazy(() => import("./admin/pages/AdminAnalytics"));
51
+ const AdminProfile = lazy(() => import("./admin/pages/AdminProfile"));
52
+ const AdminSettings = lazy(() => import("./admin/pages/AdminSettings"));
53
+ const SLAPage = lazy(() => import("./admin/pages/SLAPage"));
54
+ const MasterBugReports = lazy(() => import("./master-admin/pages/MasterBugReports"));
55
+
56
+ const AutoCategorizationFeature = lazy(() => import("./pages/features/AutoCategorizationFeature"));
57
+ const PriorityDetectionFeature = lazy(() => import("./pages/features/PriorityDetectionFeature"));
58
+ const SmartResolutionFeature = lazy(() => import("./pages/features/SmartResolutionFeature"));
59
+
60
+ const TermsOfService = lazy(() => import("./pages/legal/TermsOfService"));
61
+ const PrivacyPolicy = lazy(() => import("./pages/legal/PrivacyPolicy"));
62
+ const Security = lazy(() => import("./pages/legal/Security"));
63
+
64
+ const MasterAdminLogin = lazy(() => import("./pages/MasterAdminLogin"));
65
+ const MasterAdminLayout = lazy(() => import("./master-admin/layout/MasterAdminLayout"));
66
+ const MasterAdminDashboard = lazy(() => import("./master-admin/pages/MasterAdminDashboard"));
67
+ const PendingAdminRequests = lazy(() => import("./master-admin/pages/PendingAdminRequests"));
68
+ const AllCompanies = lazy(() => import("./master-admin/pages/AllCompanies"));
69
+ const AllAdmins = lazy(() => import("./master-admin/pages/AllAdmins"));
70
+ const Changelog = lazy(() => import("./pages/Changelog"));
71
+ const AboutUs = lazy(() => import("./pages/AboutUs"));
72
+ const NotFoundPage = lazy(() => import("./components/ui/not-found-2").then((module) => ({ default: module.NotFound })));
73
+ const Toaster = lazy(() => import("./components/shared/Toaster"));
74
+ const BugReportWidget = lazy(() => import("./components/shared/BugReportWidget"));
75
+
76
+ function RouteFallback() {
77
+ return (
78
+ <div className="flex min-h-[40vh] items-center justify-center px-6 py-16">
79
+ <div className="rounded-2xl border border-slate-200 bg-white px-6 py-4 text-sm font-semibold text-slate-500 shadow-sm">
80
+ Loading...
81
+ </div>
82
+ </div>
83
+ );
84
+ }
85
 
86
 
87
  function TitleUpdater() {
 
99
  else if (path.startsWith('/admin/analytics')) title = 'Analytics | Admin';
100
  else if (path.startsWith('/admin/profile')) title = 'Admin Profile';
101
  else if (path.startsWith('/admin/settings')) title = 'Settings | Admin';
102
+ else if (path.startsWith('/admin/sla')) title = 'SLA Monitor | Admin';
103
  // Master Admin Routes
104
  else if (path.startsWith('/master-admin/dashboard')) title = 'Master Dashboard';
105
  else if (path.startsWith('/master-admin/admin-requests')) title = 'Pending Requests | Master Admin';
 
115
  else if (path === '/my-tickets') title = 'My Tickets';
116
  else if (path === '/profile') title = 'User Profile';
117
  else if (path === '/notifications') title = 'Notifications';
 
 
 
 
 
 
 
118
  // Public / Lobby Routes
119
  else if (path === '/login') title = 'Login';
120
  else if (path === '/signup') title = 'Create Account';
121
  else if (path === '/admin-signup') title = 'Admin Signup';
122
  else if (path === '/user-lobby') title = 'User Lobby';
123
  else if (path === '/admin-lobby') title = 'Admin Lobby';
124
+ else if (path === '/about-us') title = 'About Us';
125
  else if (path === '/') title = 'Welcome';
126
 
127
  document.title = title === 'HELPDESK.AI' ? title : `${title} | HELPDESK.AI`;
 
196
  <Route path="/admin/analytics" element={<AdminAnalytics />} />
197
  <Route path="/admin/profile" element={<AdminProfile />} />
198
  <Route path="/admin/settings" element={<AdminSettings />} />
199
+ <Route path="/admin/sla" element={<SLAPage />} />
200
  </Route>
201
  </Route>
202
 
203
+ <Route path="*" element={<NotFoundPage />} />
204
  </Routes>
205
  </>
206
  );
 
214
  initialize();
215
  }, [initialize]);
216
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
217
  return (
218
  <BrowserRouter>
219
  <TitleUpdater />
220
  <ScrollToTop />
221
+ <Suspense fallback={<RouteFallback />}>
222
+ <Toaster />
223
+ <BugReportWidget />
224
+ <Routes>
225
+ {/* Public */}
226
+ <Route path="/" element={<LandingPage />} />
227
+ <Route path="/login" element={<Login />} />
228
+ <Route path="/auth/callback" element={<AuthCallback />} />
229
+ <Route path="/forgot-password" element={<ForgotPassword />} />
230
+ <Route path="/reset-password" element={<ResetPassword />} />
231
+ <Route path="/signup" element={<Signup />} />
232
+ <Route path="/admin-signup" element={<AdminSignup />} />
233
+ <Route path="/admin-lobby" element={<AdminLobby />} />
234
+ <Route path="/user-lobby" element={<UserLobby />} />
235
+ <Route path="/not-approved" element={<NotApproved />} />
236
+ <Route path="/contact-sales" element={<ContactSales />} />
237
+
238
+ {/* Feature Pages */}
239
+ <Route path="/features/categorization" element={<AutoCategorizationFeature />} />
240
+ <Route path="/features/priority" element={<PriorityDetectionFeature />} />
241
+ <Route path="/features/resolution" element={<SmartResolutionFeature />} />
242
+
243
+ {/* Resources Pages */}
244
+ <Route path="/changelog" element={<Changelog />} />
245
+ <Route path="/about-us" element={<AboutUs />} />
246
+
247
+ {/* Legal Pages */}
248
+ <Route path="/terms" element={<TermsOfService />} />
249
+ <Route path="/privacy" element={<PrivacyPolicy />} />
250
+ <Route path="/security" element={<Security />} />
251
+
252
+ {/* Master Admin Portal */}
253
+ <Route path="/master-admin-login" element={<MasterAdminLogin />} />
254
+
255
+ <Route element={<MasterAdminProtectedRoute />}>
256
+ <Route element={<MasterAdminLayout />}>
257
+ <Route path="/master-admin/dashboard" element={<MasterAdminDashboard />} />
258
+ <Route path="/master-admin/admin-requests" element={<PendingAdminRequests />} />
259
+ <Route path="/master-admin/companies" element={<AllCompanies />} />
260
+ <Route path="/master-admin/all-admins" element={<AllAdmins />} />
261
+ <Route path="/master-admin/bug-reports" element={<MasterBugReports />} />
262
+ </Route>
263
  </Route>
 
264
 
265
+ {/* Protected */}
266
+ <Route element={<ProtectedRoute />}>
267
+ <Route path="/*" element={<AppLayout />} />
268
+ </Route>
269
+ </Routes>
270
+ </Suspense>
271
  </BrowserRouter>
272
  );
273
  }
Frontend/src/admin/components/AdminSidebar.jsx CHANGED
@@ -10,7 +10,8 @@ import {
10
  LogOut,
11
  Activity,
12
  ChevronLeft,
13
- ChevronRight
 
14
  } from 'lucide-react';
15
  import useAuthStore from '../../store/authStore';
16
 
@@ -21,6 +22,7 @@ const AdminSidebar = ({ isMobile, onClose, isCollapsed, onToggleCollapse }) => {
21
  { label: 'Users', path: '/admin/users', icon: Users },
22
  { label: 'Analytics', path: '/admin/analytics', icon: BarChart3 },
23
  { label: 'Profile', path: '/admin/profile', icon: UserCircle },
 
24
  ];
25
 
26
  const { logout } = useAuthStore();
 
10
  LogOut,
11
  Activity,
12
  ChevronLeft,
13
+ ChevronRight,
14
+ Clock
15
  } from 'lucide-react';
16
  import useAuthStore from '../../store/authStore';
17
 
 
22
  { label: 'Users', path: '/admin/users', icon: Users },
23
  { label: 'Analytics', path: '/admin/analytics', icon: BarChart3 },
24
  { label: 'Profile', path: '/admin/profile', icon: UserCircle },
25
+ { label: 'SLA Monitor', path: '/admin/sla', icon: Clock },
26
  ];
27
 
28
  const { logout } = useAuthStore();
Frontend/src/admin/components/TicketTable.jsx CHANGED
@@ -73,6 +73,9 @@ const TicketTable = ({ tickets = [], isLoading = false, limit = null }) => {
73
  ? ticket.assigned_team
74
  : (teamMap[effectiveCategory] || ticket.assigned_team || 'L1 Helpdesk');
75
  const statusSt = getStatusStyle(ticket.status);
 
 
 
76
 
77
  // Truncated subject
78
  const subject = ticket.subject || ticket.summary || 'Untitled ticket';
@@ -129,9 +132,16 @@ const TicketTable = ({ tickets = [], isLoading = false, limit = null }) => {
129
  <span style={{ fontSize: '13px', fontWeight: 500, color: '#111827', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
130
  {truncSubject}
131
  </span>
132
- <span style={{ fontSize: '11px', color: '#6b7280' }}>
133
- {effectiveCategory || 'General'}
134
- </span>
 
 
 
 
 
 
 
135
  </div>
136
  </div>
137
  </td>
 
73
  ? ticket.assigned_team
74
  : (teamMap[effectiveCategory] || ticket.assigned_team || 'L1 Helpdesk');
75
  const statusSt = getStatusStyle(ticket.status);
76
+ const translationMeta = ticket?.metadata?.translation;
77
+ const isTranslated = Boolean(translationMeta?.translated);
78
+ const sourceLanguageName = translationMeta?.source_language_name || translationMeta?.source_language || 'Unknown';
79
 
80
  // Truncated subject
81
  const subject = ticket.subject || ticket.summary || 'Untitled ticket';
 
132
  <span style={{ fontSize: '13px', fontWeight: 500, color: '#111827', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
133
  {truncSubject}
134
  </span>
135
+ <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
136
+ <span style={{ fontSize: '11px', color: '#6b7280' }}>
137
+ {effectiveCategory || 'General'}
138
+ </span>
139
+ {isTranslated && (
140
+ <span style={{ fontSize: '10px', color: '#0369a1' }}>
141
+ Translated from {sourceLanguageName}
142
+ </span>
143
+ )}
144
+ </div>
145
  </div>
146
  </div>
147
  </td>
Frontend/src/admin/pages/AdminDashboard.jsx CHANGED
@@ -1,12 +1,13 @@
1
  import React, { useMemo } from 'react';
2
  import { useNavigate } from 'react-router-dom';
3
- import { Activity } from 'lucide-react';
4
 
5
  import useAuthStore from "../../store/authStore";
 
 
6
  import { supabase } from "../../lib/supabaseClient";
7
  import StatCard from "../components/StatCard";
8
  import TicketTable from "../components/TicketTable";
9
- import { formatTimelineDate } from "../../utils/dateUtils";
10
 
11
  // Inline SVG icon components
12
  const TicketIcon = () => (
@@ -67,12 +68,67 @@ const aiIconMap = [
67
  { icon: <DuplicateIcon />, bg: '#FFF7ED', color: '#f97316' },
68
  ];
69
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
  const AdminDashboard = () => {
71
  const navigate = useNavigate();
72
  const { profile } = useAuthStore();
73
- const [tickets, setTickets] = React.useState([]);
74
  const [isLoading, setIsLoading] = React.useState(true);
 
 
 
 
 
 
 
 
 
 
75
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
  React.useEffect(() => {
77
  if (profile) {
78
  const fetchStats = async () => {
@@ -92,19 +148,27 @@ const AdminDashboard = () => {
92
  console.warn("Retrying dashboard fetch without relation...", error);
93
  const { data: basicData, error: basicError } = await supabase.from('tickets').select('*').eq('company', profile?.company).order('created_at', { ascending: false });
94
  if (basicError) throw basicError;
95
- setTickets(basicData || []);
 
 
 
96
  } else {
97
- setTickets(data || []);
 
 
98
  }
99
  } catch (err) { console.error("Dashboard fetch error:", err); }
100
  finally { setIsLoading(false); }
101
  };
102
 
103
  fetchStats();
104
- const interval = setInterval(fetchStats, 30000);
105
- return () => clearInterval(interval);
106
  }
107
- }, [profile]);
 
 
 
 
 
108
 
109
  const metrics = useMemo(() => {
110
  const total = tickets.length;
@@ -126,6 +190,26 @@ const AdminDashboard = () => {
126
  ];
127
  }, [tickets]);
128
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
129
  return (
130
  <div style={{ background: '#f8faf9', minHeight: '100vh', paddingBottom: '40px' }} className="space-y-10 -m-6 p-6 md:-m-10 md:p-10">
131
  {/* Header */}
@@ -135,13 +219,17 @@ const AdminDashboard = () => {
135
  Dashboard
136
  </h1>
137
  <p style={{ color: '#6b7280', fontSize: '13px', marginTop: '4px', display: 'flex', alignItems: 'center', gap: '8px', fontWeight: 500 }}>
138
- <span style={{ width: 6, height: 6, borderRadius: '50%', background: '#22c55e', display: 'inline-block' }}></span>
139
- Real-time updates active
 
 
 
 
140
  </p>
141
  </div>
142
- <div style={{ display: 'flex', alignItems: 'center', gap: '8px', padding: '6px 16px', background: '#F0FDF4', border: '1.5px solid #BBF7D0', borderRadius: '100px' }}>
143
- <span style={{ width: 6, height: 6, borderRadius: '50%', background: '#22c55e', display: 'inline-block', animation: 'pulse-dot 2s infinite' }}></span>
144
- <span style={{ fontSize: '11px', fontWeight: 700, color: '#15803d', letterSpacing: '0.08em', textTransform: 'uppercase' }}>System Active</span>
145
  </div>
146
  </div>
147
 
@@ -161,6 +249,42 @@ const AdminDashboard = () => {
161
  </button>
162
  </div>
163
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
164
  <div className="grid grid-cols-1 lg:grid-cols-12 gap-10">
165
  {/* Recent Activity */}
166
  <div className="lg:col-span-8 space-y-6">
@@ -182,9 +306,9 @@ const AdminDashboard = () => {
182
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#16a34a" strokeWidth="2"><circle cx="12" cy="12" r="3"/><path d="M12 1v4M12 19v4M4.22 4.22l2.83 2.83M16.95 16.95l2.83 2.83M1 12h4M19 12h4M4.22 19.78l2.83-2.83M16.95 7.05l2.83-2.83"/></svg>
183
  AI Status
184
  </h2>
185
- <div style={{ display: 'flex', alignItems: 'center', gap: '6px', background: '#F0FDF4', border: '1px solid #BBF7D0', borderRadius: '100px', padding: '3px 10px' }}>
186
- <span style={{ width: 5, height: 5, borderRadius: '50%', background: '#22c55e', display: 'inline-block', animation: 'pulse-dot 2s infinite' }}></span>
187
- <span style={{ fontSize: '10px', fontWeight: 700, color: '#15803d' }}>LIVE SYNC</span>
188
  </div>
189
  </div>
190
  <div style={{ background: '#fff', borderRadius: '20px', border: '1px solid #f0fdf4', padding: '24px' }}>
@@ -209,9 +333,9 @@ const AdminDashboard = () => {
209
  <div className="pt-4 mt-4 border-t border-gray-100 flex flex-col items-center gap-2">
210
  <p style={{ fontSize: '10px', color: '#9ca3af', letterSpacing: '0.14em', fontWeight: 600, textTransform: 'uppercase' }}>All systems operating normally</p>
211
  <div style={{ display: 'flex', alignItems: 'center', gap: '6px', padding: '4px 10px', background: '#f8faf9', borderRadius: '100px', border: '1px solid #e5e7eb' }}>
212
- <Activity size={10} color="#9ca3af" />
213
- <span style={{ fontSize: '9px', fontWeight: 600, color: '#9ca3af', textTransform: 'uppercase', letterSpacing: '0.1em' }}>
214
- Last Synced: {formatTimelineDate(new Date())}
215
  </span>
216
  </div>
217
  </div>
 
1
  import React, { useMemo } from 'react';
2
  import { useNavigate } from 'react-router-dom';
3
+ import { Activity, AlertTriangle, Clock, ShieldCheck, Wifi, WifiOff } from 'lucide-react';
4
 
5
  import useAuthStore from "../../store/authStore";
6
+ import useTicketStore from "../../store/ticketStore";
7
+ import useWebSocket from "../../hooks/useWebSocket";
8
  import { supabase } from "../../lib/supabaseClient";
9
  import StatCard from "../components/StatCard";
10
  import TicketTable from "../components/TicketTable";
 
11
 
12
  // Inline SVG icon components
13
  const TicketIcon = () => (
 
68
  { icon: <DuplicateIcon />, bg: '#FFF7ED', color: '#f97316' },
69
  ];
70
 
71
+ function isTerminalTicket(ticket) {
72
+ const status = String(ticket?.status || '').toLowerCase();
73
+ return status.includes('resolv') || status.includes('closed');
74
+ }
75
+
76
+ function getSlaDeadline(ticket) {
77
+ const source = ticket?.sla_breach_at || ticket?.slaBreachAt;
78
+ const value = source ? new Date(source).getTime() : NaN;
79
+ return Number.isFinite(value) ? value : null;
80
+ }
81
+
82
+ function getSlaState(ticket, nowMs) {
83
+ if (isTerminalTicket(ticket)) return 'met';
84
+ if (String(ticket?.sla_status || '').toUpperCase() === 'BREACHED') return 'breached';
85
+ const deadline = getSlaDeadline(ticket);
86
+ if (!deadline) return 'active';
87
+ const remaining = deadline - nowMs;
88
+ if (remaining <= 0) return 'breached';
89
+ if (remaining <= 60 * 60 * 1000) return 'warning';
90
+ return 'active';
91
+ }
92
+
93
+ function formatSlaCountdown(deadlineMs, nowMs) {
94
+ if (!deadlineMs) return 'No deadline';
95
+ const remaining = deadlineMs - nowMs;
96
+ if (remaining <= 0) return 'Breached';
97
+ const totalMinutes = Math.ceil(remaining / 60000);
98
+ const hours = Math.floor(totalMinutes / 60);
99
+ const minutes = totalMinutes % 60;
100
+ if (hours > 0) return `${hours}h ${minutes}m`;
101
+ return `${minutes}m`;
102
+ }
103
+
104
  const AdminDashboard = () => {
105
  const navigate = useNavigate();
106
  const { profile } = useAuthStore();
 
107
  const [isLoading, setIsLoading] = React.useState(true);
108
+ const [nowMs, setNowMs] = React.useState(() => Date.now());
109
+
110
+ // WebSocket connection for real-time ticket updates
111
+ const { isConnected: wsConnected, lastMessage } = useWebSocket(profile?.company);
112
+
113
+ // Read tickets from the Zustand store (populated below and updated by WS)
114
+ const tickets = useTicketStore((s) => s.tickets);
115
+ const handleWsMessage = useTicketStore((s) => s.handleWsMessage);
116
+ const setWsConnected = useTicketStore((s) => s.setWsConnected);
117
+ const upsertTicket = useTicketStore((s) => s.upsertTicket);
118
 
119
+ // Sync WebSocket connection status to store
120
+ React.useEffect(() => {
121
+ setWsConnected(wsConnected);
122
+ }, [wsConnected, setWsConnected]);
123
+
124
+ // Route incoming WebSocket messages into the ticket store
125
+ React.useEffect(() => {
126
+ if (lastMessage) {
127
+ handleWsMessage(lastMessage);
128
+ }
129
+ }, [lastMessage, handleWsMessage]);
130
+
131
+ // Initial fetch — populate store from Supabase on mount
132
  React.useEffect(() => {
133
  if (profile) {
134
  const fetchStats = async () => {
 
148
  console.warn("Retrying dashboard fetch without relation...", error);
149
  const { data: basicData, error: basicError } = await supabase.from('tickets').select('*').eq('company', profile?.company).order('created_at', { ascending: false });
150
  if (basicError) throw basicError;
151
+ // Bulk-load into store (avoid duplicates)
152
+ for (const t of basicData || []) {
153
+ upsertTicket(t);
154
+ }
155
  } else {
156
+ for (const t of data || []) {
157
+ upsertTicket(t);
158
+ }
159
  }
160
  } catch (err) { console.error("Dashboard fetch error:", err); }
161
  finally { setIsLoading(false); }
162
  };
163
 
164
  fetchStats();
 
 
165
  }
166
+ }, [profile, upsertTicket]);
167
+
168
+ React.useEffect(() => {
169
+ const timer = setInterval(() => setNowMs(Date.now()), 60 * 1000);
170
+ return () => clearInterval(timer);
171
+ }, []);
172
 
173
  const metrics = useMemo(() => {
174
  const total = tickets.length;
 
190
  ];
191
  }, [tickets]);
192
 
193
+ const slaBoard = useMemo(() => {
194
+ const actionable = tickets.filter(t => !isTerminalTicket(t));
195
+ const breached = actionable.filter(t => getSlaState(t, nowMs) === 'breached');
196
+ const warning = actionable.filter(t => getSlaState(t, nowMs) === 'warning');
197
+ const active = actionable.filter(t => getSlaState(t, nowMs) === 'active');
198
+ const critical = actionable.filter(t => String(t.priority || '').toLowerCase() === 'critical');
199
+ const nextTicket = actionable
200
+ .map(ticket => ({ ticket, deadline: getSlaDeadline(ticket) }))
201
+ .filter(item => item.deadline)
202
+ .sort((a, b) => a.deadline - b.deadline)[0];
203
+
204
+ return {
205
+ breached,
206
+ warning,
207
+ active,
208
+ critical,
209
+ nextTicket,
210
+ };
211
+ }, [tickets, nowMs]);
212
+
213
  return (
214
  <div style={{ background: '#f8faf9', minHeight: '100vh', paddingBottom: '40px' }} className="space-y-10 -m-6 p-6 md:-m-10 md:p-10">
215
  {/* Header */}
 
219
  Dashboard
220
  </h1>
221
  <p style={{ color: '#6b7280', fontSize: '13px', marginTop: '4px', display: 'flex', alignItems: 'center', gap: '8px', fontWeight: 500 }}>
222
+ {wsConnected ? (
223
+ <Wifi size={14} color="#22c55e" />
224
+ ) : (
225
+ <WifiOff size={14} color="#f97316" />
226
+ )}
227
+ {wsConnected ? 'WebSocket connected' : 'Reconnecting...'}
228
  </p>
229
  </div>
230
+ <div style={{ display: 'flex', alignItems: 'center', gap: '8px', padding: '6px 16px', background: wsConnected ? '#F0FDF4' : '#FFF7ED', border: wsConnected ? '1.5px solid #BBF7D0' : '1.5px solid #FED7AA', borderRadius: '100px' }}>
231
+ <span style={{ width: 6, height: 6, borderRadius: '50%', background: wsConnected ? '#22c55e' : '#f97316', display: 'inline-block', animation: 'pulse-dot 2s infinite' }}></span>
232
+ <span style={{ fontSize: '11px', fontWeight: 700, color: wsConnected ? '#15803d' : '#c2410c', letterSpacing: '0.08em', textTransform: 'uppercase' }}>{wsConnected ? 'Live' : 'Reconnecting'}</span>
233
  </div>
234
  </div>
235
 
 
249
  </button>
250
  </div>
251
 
252
+ <div style={{ background: '#ffffff', borderRadius: '20px', border: '1px solid #fee2e2', boxShadow: '0 2px 16px rgba(0,0,0,0.05)', padding: '24px' }}>
253
+ <div className="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-6">
254
+ <div>
255
+ <h2 style={{ fontFamily: 'Syne, sans-serif', fontSize: '16px', fontWeight: 800, color: '#0f1f12', display: 'flex', alignItems: 'center', gap: '8px', margin: 0 }}>
256
+ <Clock size={18} color="#dc2626" /> SLA Compliance
257
+ </h2>
258
+ <p style={{ fontSize: '11px', color: '#9ca3af', marginTop: '6px', fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.08em' }}>
259
+ {slaBoard.nextTicket ? `Next deadline ${formatSlaCountdown(slaBoard.nextTicket.deadline, nowMs)}` : 'No active deadlines'}
260
+ </p>
261
+ </div>
262
+ <button
263
+ onClick={() => navigate('/admin/tickets')}
264
+ className="px-4 py-2 rounded-xl text-[10px] font-black uppercase tracking-widest border border-slate-200 text-slate-600 hover:border-red-200 hover:text-red-600 transition-colors"
265
+ >
266
+ Open Queue
267
+ </button>
268
+ </div>
269
+
270
+ <div className="grid grid-cols-1 md:grid-cols-4 gap-4">
271
+ {[
272
+ { label: 'Breached', value: slaBoard.breached.length, icon: <AlertTriangle size={18} />, bg: '#fef2f2', color: '#dc2626', border: '#fecaca' },
273
+ { label: 'Warning', value: slaBoard.warning.length, icon: <Clock size={18} />, bg: '#fffbeb', color: '#d97706', border: '#fde68a' },
274
+ { label: 'Critical Open', value: slaBoard.critical.length, icon: <Activity size={18} />, bg: '#fff7ed', color: '#ea580c', border: '#fed7aa' },
275
+ { label: 'Healthy', value: slaBoard.active.length, icon: <ShieldCheck size={18} />, bg: '#f0fdf4', color: '#16a34a', border: '#bbf7d0' },
276
+ ].map(item => (
277
+ <div key={item.label} style={{ background: item.bg, border: `1px solid ${item.border}`, borderRadius: '16px', padding: '18px' }}>
278
+ <div className="flex items-center justify-between">
279
+ <span style={{ color: item.color }}>{item.icon}</span>
280
+ <span style={{ color: item.color, fontSize: '28px', fontWeight: 900, lineHeight: 1 }}>{item.value}</span>
281
+ </div>
282
+ <p style={{ margin: '12px 0 0', fontSize: '10px', fontWeight: 800, letterSpacing: '0.12em', textTransform: 'uppercase', color: item.color }}>{item.label}</p>
283
+ </div>
284
+ ))}
285
+ </div>
286
+ </div>
287
+
288
  <div className="grid grid-cols-1 lg:grid-cols-12 gap-10">
289
  {/* Recent Activity */}
290
  <div className="lg:col-span-8 space-y-6">
 
306
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#16a34a" strokeWidth="2"><circle cx="12" cy="12" r="3"/><path d="M12 1v4M12 19v4M4.22 4.22l2.83 2.83M16.95 16.95l2.83 2.83M1 12h4M19 12h4M4.22 19.78l2.83-2.83M16.95 7.05l2.83-2.83"/></svg>
307
  AI Status
308
  </h2>
309
+ <div style={{ display: 'flex', alignItems: 'center', gap: '6px', background: wsConnected ? '#F0FDF4' : '#FFF7ED', border: wsConnected ? '1px solid #BBF7D0' : '1px solid #FED7AA', borderRadius: '100px', padding: '3px 10px' }}>
310
+ <span style={{ width: 5, height: 5, borderRadius: '50%', background: wsConnected ? '#22c55e' : '#f97316', display: 'inline-block', animation: 'pulse-dot 2s infinite' }}></span>
311
+ <span style={{ fontSize: '10px', fontWeight: 700, color: wsConnected ? '#15803d' : '#c2410c' }}>{wsConnected ? 'WS CONNECTED' : 'RECONNECTING'}</span>
312
  </div>
313
  </div>
314
  <div style={{ background: '#fff', borderRadius: '20px', border: '1px solid #f0fdf4', padding: '24px' }}>
 
333
  <div className="pt-4 mt-4 border-t border-gray-100 flex flex-col items-center gap-2">
334
  <p style={{ fontSize: '10px', color: '#9ca3af', letterSpacing: '0.14em', fontWeight: 600, textTransform: 'uppercase' }}>All systems operating normally</p>
335
  <div style={{ display: 'flex', alignItems: 'center', gap: '6px', padding: '4px 10px', background: '#f8faf9', borderRadius: '100px', border: '1px solid #e5e7eb' }}>
336
+ {wsConnected ? <Activity size={10} color="#22c55e" /> : <Activity size={10} color="#f97316" />}
337
+ <span style={{ fontSize: '9px', fontWeight: 600, color: wsConnected ? '#16a34a' : '#9ca3af', textTransform: 'uppercase', letterSpacing: '0.1em' }}>
338
+ {wsConnected ? 'Live via WebSocket' : 'Reconnecting via WebSocket...'}
339
  </span>
340
  </div>
341
  </div>
Frontend/src/admin/pages/AdminSettings.jsx CHANGED
@@ -1,4 +1,6 @@
1
- import React from 'react';
 
 
2
  import {
3
  Settings,
4
  Cpu,
@@ -8,7 +10,15 @@ import {
8
  ShieldCheck
9
  } from 'lucide-react';
10
  import useAdminStore from '../store/adminStore';
11
- import { Card, CardContent } from "../../components/ui/card";
 
 
 
 
 
 
 
 
12
  import { Select } from "../../components/ui/select";
13
 
14
  /**
@@ -17,12 +27,97 @@ import { Select } from "../../components/ui/select";
17
  */
18
  const AdminSettings = () => {
19
  const { settings, updateSettings } = useAdminStore();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
 
21
- // Handlers
22
  const handleChange = (key, value) => {
23
  updateSettings({ [key]: value });
 
 
24
  };
25
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  return (
27
  <div className="max-w-4xl mx-auto py-6 space-y-10 pb-20 animate-in fade-in duration-700">
28
  {/* 1. Header Area */}
@@ -35,6 +130,22 @@ const AdminSettings = () => {
35
  <ShieldCheck size={14} className="text-emerald-500" /> Administrator Account
36
  </p>
37
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  </div>
39
 
40
  <div className="space-y-8">
@@ -167,8 +278,21 @@ const AdminSettings = () => {
167
  <div className={`absolute top-1 w-6 h-6 bg-white rounded-full transition-all duration-300 shadow-md ${settings.adminAlerts ? 'right-1' : 'left-1'}`}></div>
168
  </button>
169
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
170
  </CardContent>
171
  </Card>
 
172
  </div>
173
  </div>
174
  );
 
1
+ import { Card, CardContent } from "../../components/ui/card";
2
+ import WebhookSettings from "../../components/shared/WebhookSettings";
3
+ import React, { useCallback, useEffect, useMemo, useState } from 'react';
4
  import {
5
  Settings,
6
  Cpu,
 
10
  ShieldCheck
11
  } from 'lucide-react';
12
  import useAdminStore from '../store/adminStore';
13
+ import useAuthStore from '../../store/authStore';
14
+ import { supabase } from '../../lib/supabaseClient';
15
+ import {
16
+ DEFAULT_ADMIN_SETTINGS,
17
+ resolveCompanyId,
18
+ settingsFromSystemSettingsRow,
19
+ settingsToSystemSettingsRow
20
+ } from '../../utils/adminSettingsPersistence';
21
+
22
  import { Select } from "../../components/ui/select";
23
 
24
  /**
 
27
  */
28
  const AdminSettings = () => {
29
  const { settings, updateSettings } = useAdminStore();
30
+ const { user, profile } = useAuthStore();
31
+ const [isLoadingSettings, setIsLoadingSettings] = useState(false);
32
+ const [isSavingSettings, setIsSavingSettings] = useState(false);
33
+ const [statusMessage, setStatusMessage] = useState('');
34
+ const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
35
+
36
+ const companyId = useMemo(() => resolveCompanyId(profile, user), [profile, user]);
37
+
38
+ useEffect(() => {
39
+ let isMounted = true;
40
+
41
+ const loadCompanySettings = async () => {
42
+ if (!companyId) {
43
+ setStatusMessage('Company profile is required before settings can be synced.');
44
+ return;
45
+ }
46
+
47
+ setIsLoadingSettings(true);
48
+ setStatusMessage('');
49
+
50
+ const { data, error } = await supabase
51
+ .from('system_settings')
52
+ .select('ai_confidence_threshold, duplicate_sensitivity, enable_auto_resolve, auto_close_days, email_notifications, admin_alerts')
53
+ .eq('company_id', companyId)
54
+ .maybeSingle();
55
+
56
+ if (!isMounted) return;
57
+
58
+ if (error) {
59
+ setStatusMessage(`Unable to load saved settings: ${error.message}`);
60
+ } else if (data) {
61
+ updateSettings(settingsFromSystemSettingsRow(data, DEFAULT_ADMIN_SETTINGS));
62
+ setHasUnsavedChanges(false);
63
+ setStatusMessage('Saved company settings loaded.');
64
+ }
65
+
66
+ setIsLoadingSettings(false);
67
+ };
68
+
69
+ loadCompanySettings();
70
+
71
+ return () => {
72
+ isMounted = false;
73
+ };
74
+ }, [companyId, updateSettings]);
75
+
76
+ const saveCompanySettings = useCallback(async (nextSettings, { silent = false } = {}) => {
77
+ if (!companyId) {
78
+ setStatusMessage('Company profile is required before settings can be saved.');
79
+ return;
80
+ }
81
+
82
+ setIsSavingSettings(true);
83
+ if (!silent) {
84
+ setStatusMessage('');
85
+ }
86
+
87
+ const { error } = await supabase
88
+ .from('system_settings')
89
+ .upsert(settingsToSystemSettingsRow(nextSettings, companyId), { onConflict: 'company_id' });
90
+
91
+ if (error) {
92
+ setStatusMessage(`Unable to save settings: ${error.message}`);
93
+ } else {
94
+ setHasUnsavedChanges(false);
95
+ setStatusMessage('Settings saved for this company.');
96
+ }
97
+
98
+ setIsSavingSettings(false);
99
+ }, [companyId]);
100
 
 
101
  const handleChange = (key, value) => {
102
  updateSettings({ [key]: value });
103
+ setHasUnsavedChanges(true);
104
+ setStatusMessage('Saving changes...');
105
  };
106
 
107
+ const handleSaveSettings = useCallback(() => {
108
+ saveCompanySettings(settings);
109
+ }, [saveCompanySettings, settings]);
110
+
111
+ useEffect(() => {
112
+ if (!hasUnsavedChanges || isLoadingSettings || isSavingSettings) return undefined;
113
+
114
+ const saveTimer = window.setTimeout(() => {
115
+ saveCompanySettings(settings, { silent: true });
116
+ }, 800);
117
+
118
+ return () => window.clearTimeout(saveTimer);
119
+ }, [hasUnsavedChanges, isLoadingSettings, isSavingSettings, saveCompanySettings, settings]);
120
+
121
  return (
122
  <div className="max-w-4xl mx-auto py-6 space-y-10 pb-20 animate-in fade-in duration-700">
123
  {/* 1. Header Area */}
 
130
  <ShieldCheck size={14} className="text-emerald-500" /> Administrator Account
131
  </p>
132
  </div>
133
+ <div className="flex flex-col items-start md:items-end gap-2">
134
+ <button
135
+ type="button"
136
+ onClick={handleSaveSettings}
137
+ disabled={!companyId || isLoadingSettings || isSavingSettings || !hasUnsavedChanges}
138
+ className="inline-flex items-center gap-2 rounded-xl bg-slate-900 px-5 py-3 text-xs font-black uppercase tracking-widest text-white shadow-xl shadow-slate-200 transition-all hover:bg-indigo-600 disabled:cursor-not-allowed disabled:bg-slate-300 disabled:shadow-none"
139
+ >
140
+ <Save size={16} />
141
+ {isSavingSettings ? 'Saving...' : 'Save Now'}
142
+ </button>
143
+ {statusMessage && (
144
+ <p className="max-w-xs text-left md:text-right text-[10px] font-bold uppercase tracking-widest text-slate-400">
145
+ {statusMessage}
146
+ </p>
147
+ )}
148
+ </div>
149
  </div>
150
 
151
  <div className="space-y-8">
 
278
  <div className={`absolute top-1 w-6 h-6 bg-white rounded-full transition-all duration-300 shadow-md ${settings.adminAlerts ? 'right-1' : 'left-1'}`}></div>
279
  </button>
280
  </div>
281
+ </CardContent>
282
+ </Card>
283
+
284
+ {/* 5. Webhook Notification Settings */}
285
+ <Card className="border-none shadow-2xl shadow-slate-200/40 rounded-[2rem] overflow-hidden bg-white">
286
+ <div className="px-8 py-6 bg-slate-50 border-b border-slate-100 flex items-center justify-between">
287
+ <h3 className="text-sm font-black text-slate-900 uppercase italic tracking-tight flex items-center gap-3">
288
+ <Bell size={18} className="text-blue-500" /> Slack & Teams Webhooks
289
+ </h3>
290
+ </div>
291
+ <CardContent className="p-8">
292
+ <WebhookSettings />
293
  </CardContent>
294
  </Card>
295
+
296
  </div>
297
  </div>
298
  );
Frontend/src/admin/pages/AdminTicketDetail.jsx CHANGED
@@ -16,6 +16,7 @@ import { formatTicketId } from "../../utils/format";
16
  import SLABadge from "../components/SLABadge";
17
  import { formatFullTimestamp } from "../../utils/dateUtils";
18
  import TicketTimeline from "../../user/components/TicketTimeline";
 
19
 
20
  const AdminTicketDetail = () => {
21
  const { ticket_id } = useParams();
@@ -34,6 +35,7 @@ const AdminTicketDetail = () => {
34
  const [imageUrl, setImageUrl] = useState(null);
35
  const [isUpdating, setIsUpdating] = useState(null);
36
  const [isLive, setIsLive] = useState(false);
 
37
 
38
  const [correctionForm, setCorrectionForm] = useState({
39
  category: '',
@@ -197,6 +199,11 @@ const AdminTicketDetail = () => {
197
  const displayPriority = ticket.priority || 'Medium';
198
  const displaySummary = ticket.summary || ticket.subject || 'No Summary';
199
  const displayText = ticket.description || ticket.text || displaySummary;
 
 
 
 
 
200
 
201
  return (
202
  <div style={{ background: '#f8faf9', minHeight: '100vh', paddingBottom: '80px' }} className="-m-6 p-6 md:-m-10 md:p-10 space-y-6 animate-in fade-in duration-700">
@@ -237,7 +244,14 @@ const AdminTicketDetail = () => {
237
  <span style={{ fontSize: '10px', fontWeight: 700, color: '#475569', background: '#f1f5f9', padding: '2px 8px', borderRadius: '100px', textTransform: 'uppercase' }}>
238
  {ticket.assignee?.full_name || 'UNASSIGNED'}
239
  </span>
240
- <SLABadge priority={displayPriority} createdAt={ticket.created_at} status={displayStatus} compact />
 
 
 
 
 
 
 
241
  </div>
242
  </div>
243
  </div>
@@ -305,8 +319,22 @@ const AdminTicketDetail = () => {
305
  <span style={{ fontSize: '10px', color: '#9ca3af', fontWeight: 600, textTransform: 'uppercase' }}>{formatFullTimestamp(ticket.created_at)}</span>
306
  </div>
307
  <div style={{ padding: '28px' }}>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
308
  <div style={{ background: 'linear-gradient(135deg, #0f1f12, #1a3320)', color: '#ffffff', borderRadius: '16px', padding: '24px 28px', fontSize: '15px', fontStyle: 'italic', lineHeight: 1.7 }}>
309
- "{displayText}"
310
  </div>
311
 
312
  {imageUrl && (
@@ -335,6 +363,8 @@ const AdminTicketDetail = () => {
335
  </div>
336
  <TicketTimeline ticket={ticket} />
337
  </div>
 
 
338
  </div>
339
 
340
  {/* AI Column */}
 
16
  import SLABadge from "../components/SLABadge";
17
  import { formatFullTimestamp } from "../../utils/dateUtils";
18
  import TicketTimeline from "../../user/components/TicketTimeline";
19
+ import TicketAuditTimeline from "../components/TicketAuditTimeline";
20
 
21
  const AdminTicketDetail = () => {
22
  const { ticket_id } = useParams();
 
35
  const [imageUrl, setImageUrl] = useState(null);
36
  const [isUpdating, setIsUpdating] = useState(null);
37
  const [isLive, setIsLive] = useState(false);
38
+ const [showOriginalText, setShowOriginalText] = useState(false);
39
 
40
  const [correctionForm, setCorrectionForm] = useState({
41
  category: '',
 
199
  const displayPriority = ticket.priority || 'Medium';
200
  const displaySummary = ticket.summary || ticket.subject || 'No Summary';
201
  const displayText = ticket.description || ticket.text || displaySummary;
202
+ const translationMeta = ticket.metadata?.translation;
203
+ const originalTextMeta = ticket.metadata?.original_text;
204
+ const isTranslated = Boolean(translationMeta?.translated && originalTextMeta?.description);
205
+ const sourceLanguageName = translationMeta?.source_language_name || translationMeta?.source_language || 'Unknown';
206
+ const renderedText = showOriginalText && isTranslated ? originalTextMeta.description : displayText;
207
 
208
  return (
209
  <div style={{ background: '#f8faf9', minHeight: '100vh', paddingBottom: '80px' }} className="-m-6 p-6 md:-m-10 md:p-10 space-y-6 animate-in fade-in duration-700">
 
244
  <span style={{ fontSize: '10px', fontWeight: 700, color: '#475569', background: '#f1f5f9', padding: '2px 8px', borderRadius: '100px', textTransform: 'uppercase' }}>
245
  {ticket.assignee?.full_name || 'UNASSIGNED'}
246
  </span>
247
+ <SLABadge
248
+ priority={displayPriority}
249
+ createdAt={ticket.created_at}
250
+ slaBreachAt={ticket.sla_breach_at}
251
+ slaStatus={ticket.sla_status}
252
+ status={displayStatus}
253
+ compact
254
+ />
255
  </div>
256
  </div>
257
  </div>
 
319
  <span style={{ fontSize: '10px', color: '#9ca3af', fontWeight: 600, textTransform: 'uppercase' }}>{formatFullTimestamp(ticket.created_at)}</span>
320
  </div>
321
  <div style={{ padding: '28px' }}>
322
+ {isTranslated && (
323
+ <div style={{ marginBottom: '16px', border: '1px solid #bae6fd', background: '#f0f9ff', borderRadius: '12px', padding: '10px 12px', display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '8px' }}>
324
+ <span style={{ fontSize: '12px', fontWeight: 600, color: '#0c4a6e' }}>
325
+ Translated from {sourceLanguageName}
326
+ </span>
327
+ <button
328
+ type="button"
329
+ onClick={() => setShowOriginalText(prev => !prev)}
330
+ style={{ fontSize: '11px', fontWeight: 700, color: '#0369a1', background: 'transparent', border: 'none', cursor: 'pointer' }}
331
+ >
332
+ {showOriginalText ? 'View English' : 'View Original'}
333
+ </button>
334
+ </div>
335
+ )}
336
  <div style={{ background: 'linear-gradient(135deg, #0f1f12, #1a3320)', color: '#ffffff', borderRadius: '16px', padding: '24px 28px', fontSize: '15px', fontStyle: 'italic', lineHeight: 1.7 }}>
337
+ "{renderedText}"
338
  </div>
339
 
340
  {imageUrl && (
 
363
  </div>
364
  <TicketTimeline ticket={ticket} />
365
  </div>
366
+
367
+ <TicketAuditTimeline ticketId={ticket.id} companyId={ticket.company_id} />
368
  </div>
369
 
370
  {/* AI Column */}
Frontend/src/admin/pages/AdminTickets.jsx CHANGED
@@ -1,8 +1,9 @@
1
- import React, { useState, useMemo, useEffect } from 'react';
2
  import { useNavigate, useLocation } from 'react-router-dom';
3
  import useAuthStore from "../../store/authStore";
4
  import useToastStore from "../../store/toastStore";
5
  import { supabase } from "../../lib/supabaseClient";
 
6
  import {
7
  Search,
8
  Filter,
@@ -29,7 +30,7 @@ import { formatTimelineDate } from "../../utils/dateUtils";
29
  const AdminTickets = () => {
30
  const navigate = useNavigate();
31
  const location = useLocation();
32
- const { user } = useAuthStore();
33
  const { showToast } = useToastStore();
34
 
35
  // Data State
@@ -46,6 +47,27 @@ const AdminTickets = () => {
46
  const [teamFilter, setTeamFilter] = useState('All');
47
  const [agents, setAgents] = useState([]); // All staff/admins in the company
48
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
  const fetchInitialData = async () => {
50
  setLoading(true);
51
  try {
@@ -113,38 +135,6 @@ const AdminTickets = () => {
113
 
114
  useEffect(() => {
115
  fetchInitialData();
116
-
117
- // 4. Real-time subscription to ticket changes
118
- const { profile } = useAuthStore.getState();
119
- const channel = supabase
120
- .channel('admin_tickets_realtime')
121
- .on(
122
- 'postgres_changes',
123
- {
124
- event: '*',
125
- schema: 'public',
126
- table: 'tickets',
127
- filter: profile?.company ? `company=eq.${profile.company}` : undefined
128
- },
129
- (payload) => {
130
- console.log("Admin tickets sync event:", payload.eventType, payload.new);
131
- if (payload.eventType === 'INSERT') {
132
- setTickets(prev => [payload.new, ...prev]);
133
- showToast(`New Incident Reported: #${payload.new.id}`, "success");
134
- // Play a subtle sound or visual cue if needed
135
- } else if (payload.eventType === 'UPDATE') {
136
- setTickets(prev => prev.map(t => t.id === payload.new.id ? { ...t, ...payload.new } : t));
137
- } else if (payload.eventType === 'DELETE') {
138
- setTickets(prev => prev.filter(t => t.id === payload.old.id));
139
- }
140
- }
141
- )
142
- .subscribe();
143
-
144
- return () => {
145
- supabase.removeChannel(channel);
146
- };
147
-
148
  }, [statusFilter, categoryFilter, priorityFilter, teamFilter]);
149
 
150
  // Seed search from URL
@@ -305,8 +295,11 @@ const AdminTickets = () => {
305
  </tr>
306
  </thead>
307
  <tbody className="divide-y divide-slate-50">
308
- {filteredTickets.map((ticket) => (
309
- <tr key={ticket.id} className={`hover:bg-slate-50/50 transition-colors group ${isUpdating === ticket.id ? 'opacity-50 pointer-events-none' : ''}`}>
 
 
 
310
  {/* Ticket ID */}
311
  <td className="px-6 py-6">
312
  <span className="font-mono text-xs font-black text-emerald-600">#{formatTicketId(ticket.id)}</span>
@@ -347,6 +340,11 @@ const AdminTickets = () => {
347
  {ticket.category}
348
  <span className="text-[9px] font-medium text-slate-300">• {formatTimelineDate(ticket.created_at)}</span>
349
  </span>
 
 
 
 
 
350
  </div>
351
  </td>
352
 
@@ -437,6 +435,8 @@ const AdminTickets = () => {
437
  <SLABadge
438
  priority={ticket.priority}
439
  createdAt={ticket.created_at}
 
 
440
  status={ticket.status}
441
  />
442
  </td>
@@ -454,7 +454,8 @@ const AdminTickets = () => {
454
  </div>
455
  </td>
456
  </tr>
457
- ))}
 
458
  </tbody>
459
  </table>
460
  </div>
 
1
+ import React, { useCallback, useState, useMemo, useEffect } from 'react';
2
  import { useNavigate, useLocation } from 'react-router-dom';
3
  import useAuthStore from "../../store/authStore";
4
  import useToastStore from "../../store/toastStore";
5
  import { supabase } from "../../lib/supabaseClient";
6
+ import useTicketsRealtime from "../../hooks/useTicketsRealtime";
7
  import {
8
  Search,
9
  Filter,
 
30
  const AdminTickets = () => {
31
  const navigate = useNavigate();
32
  const location = useLocation();
33
+ const { user, profile } = useAuthStore();
34
  const { showToast } = useToastStore();
35
 
36
  // Data State
 
47
  const [teamFilter, setTeamFilter] = useState('All');
48
  const [agents, setAgents] = useState([]); // All staff/admins in the company
49
 
50
+ const ticketMatchesFilters = useCallback((ticket) => {
51
+ if (statusFilter !== 'All' && String(ticket.status || '').toLowerCase() !== statusFilter.toLowerCase()) return false;
52
+ if (categoryFilter !== 'All' && ticket.category !== categoryFilter) return false;
53
+ if (priorityFilter !== 'All' && String(ticket.priority || '').toLowerCase() !== priorityFilter.toLowerCase()) return false;
54
+ if (teamFilter !== 'All' && ticket.assigned_team !== teamFilter) return false;
55
+ return true;
56
+ }, [categoryFilter, priorityFilter, statusFilter, teamFilter]);
57
+
58
+ const handleRealtimeInsert = useCallback((ticket) => {
59
+ showToast(`New Incident Reported: #${formatTicketId(ticket.id)}`, "success");
60
+ }, [showToast]);
61
+
62
+ const { lastChangedTicketId } = useTicketsRealtime({
63
+ company: profile?.company,
64
+ enabled: Boolean(profile),
65
+ onTicketsChange: setTickets,
66
+ onInsert: handleRealtimeInsert,
67
+ shouldInclude: ticketMatchesFilters,
68
+ channelName: 'admin_tickets_realtime',
69
+ });
70
+
71
  const fetchInitialData = async () => {
72
  setLoading(true);
73
  try {
 
135
 
136
  useEffect(() => {
137
  fetchInitialData();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
138
  }, [statusFilter, categoryFilter, priorityFilter, teamFilter]);
139
 
140
  // Seed search from URL
 
295
  </tr>
296
  </thead>
297
  <tbody className="divide-y divide-slate-50">
298
+ {filteredTickets.map((ticket) => {
299
+ const wasLiveChanged = String(lastChangedTicketId) === String(ticket.id);
300
+
301
+ return (
302
+ <tr key={ticket.id} className={`hover:bg-slate-50/50 transition-colors group ${wasLiveChanged ? 'bg-emerald-50/70 ring-1 ring-emerald-100' : ''} ${isUpdating === ticket.id ? 'opacity-50 pointer-events-none' : ''}`}>
303
  {/* Ticket ID */}
304
  <td className="px-6 py-6">
305
  <span className="font-mono text-xs font-black text-emerald-600">#{formatTicketId(ticket.id)}</span>
 
340
  {ticket.category}
341
  <span className="text-[9px] font-medium text-slate-300">• {formatTimelineDate(ticket.created_at)}</span>
342
  </span>
343
+ {ticket?.metadata?.translation?.translated && (
344
+ <span className="text-[10px] text-sky-700 mt-1">
345
+ Translated from {ticket.metadata.translation.source_language_name || ticket.metadata.translation.source_language || 'Unknown'}
346
+ </span>
347
+ )}
348
  </div>
349
  </td>
350
 
 
435
  <SLABadge
436
  priority={ticket.priority}
437
  createdAt={ticket.created_at}
438
+ slaBreachAt={ticket.sla_breach_at}
439
+ slaStatus={ticket.sla_status}
440
  status={ticket.status}
441
  />
442
  </td>
 
454
  </div>
455
  </td>
456
  </tr>
457
+ );
458
+ })}
459
  </tbody>
460
  </table>
461
  </div>
Frontend/src/components/shared/BugReportWidget.jsx CHANGED
@@ -23,7 +23,6 @@ function useDiagnostics() {
23
  const browserInfo = navigator.userAgent;
24
  const screenInfo = `${window.innerWidth}x${window.innerHeight}`;
25
 
26
-
27
  setDiagnostics(prev => ({
28
  ...prev,
29
  url: window.location.href,
@@ -31,16 +30,6 @@ function useDiagnostics() {
31
  screen: screenInfo
32
  }));
33
 
34
- // Intercept console.error
35
- const originalConsoleError = console.error;
36
- console.error = function (...args) {
37
- setDiagnostics(prev => ({
38
- ...prev,
39
- consoleErrors: [...prev.consoleErrors, args.join(' ')].slice(-10) // keep last 10
40
- }));
41
- originalConsoleError.apply(console, args);
42
- };
43
-
44
  // Global Error Listener
45
  const handleError = (e) => {
46
  setDiagnostics(prev => ({
@@ -51,7 +40,6 @@ function useDiagnostics() {
51
  window.addEventListener('error', handleError);
52
 
53
  return () => {
54
- console.error = originalConsoleError;
55
  window.removeEventListener('error', handleError);
56
  };
57
  }, []);
@@ -417,7 +405,7 @@ const BugReportWidget = ({ advanced = false, customTrigger = null }) => {
417
  animate={{ opacity: 1, y: 0, scale: 1 }}
418
  exit={{ opacity: 0, y: 10, scale: 0.98 }}
419
  transition={{ duration: 0.2, ease: "easeOut" }}
420
- className="bg-white rounded-2xl shadow-2xl w-full max-w-2xl relative z-50 my-auto border border-slate-100 flex flex-col max-h-[90vh]"
421
  >
422
  {/* Header */}
423
  <div className="flex items-center justify-between p-5 border-b border-slate-100 shrink-0">
@@ -425,7 +413,7 @@ const BugReportWidget = ({ advanced = false, customTrigger = null }) => {
425
  <div className="p-2 bg-[#13ec80]/10 rounded-lg">
426
  <Bug className="w-6 h-6" />
427
  </div>
428
- <h2 className="text-xl font-bold text-slate-800">Report a Bug</h2>
429
  </div>
430
  <button
431
  onClick={handleClose}
@@ -650,7 +638,7 @@ const BugReportWidget = ({ advanced = false, customTrigger = null }) => {
650
  </div>
651
 
652
  {/* Footer */}
653
- <div className="p-5 border-t border-slate-100 bg-slate-50 rounded-b-2xl shrink-0 flex justify-end gap-3">
654
  <button
655
  type="button"
656
  onClick={handleClose}
 
23
  const browserInfo = navigator.userAgent;
24
  const screenInfo = `${window.innerWidth}x${window.innerHeight}`;
25
 
 
26
  setDiagnostics(prev => ({
27
  ...prev,
28
  url: window.location.href,
 
30
  screen: screenInfo
31
  }));
32
 
 
 
 
 
 
 
 
 
 
 
33
  // Global Error Listener
34
  const handleError = (e) => {
35
  setDiagnostics(prev => ({
 
40
  window.addEventListener('error', handleError);
41
 
42
  return () => {
 
43
  window.removeEventListener('error', handleError);
44
  };
45
  }, []);
 
405
  animate={{ opacity: 1, y: 0, scale: 1 }}
406
  exit={{ opacity: 0, y: 10, scale: 0.98 }}
407
  transition={{ duration: 0.2, ease: "easeOut" }}
408
+ className="bg-white dark:bg-gray-800 rounded-2xl shadow-2xl w-full max-w-2xl relative z-50 my-auto border border-slate-100 dark:border-gray-700 flex flex-col max-h-[90vh]"
409
  >
410
  {/* Header */}
411
  <div className="flex items-center justify-between p-5 border-b border-slate-100 shrink-0">
 
413
  <div className="p-2 bg-[#13ec80]/10 rounded-lg">
414
  <Bug className="w-6 h-6" />
415
  </div>
416
+ <h2 className="text-xl font-bold text-slate-800 dark:text-white">Report a Bug</h2>
417
  </div>
418
  <button
419
  onClick={handleClose}
 
638
  </div>
639
 
640
  {/* Footer */}
641
+ <div className="p-5 border-t border-slate-100 dark:border-gray-700 bg-slate-50 dark:bg-gray-900 rounded-b-2xl shrink-0 flex justify-end gap-3">
642
  <button
643
  type="button"
644
  onClick={handleClose}
Frontend/src/components/shared/ProtectedRoute.jsx CHANGED
@@ -1,6 +1,5 @@
1
- import React, { useEffect, useState } from 'react';
2
  import { Navigate, Outlet } from 'react-router-dom';
3
- import { supabase } from '../../lib/supabaseClient';
4
  import useAuthStore from '../../store/authStore';
5
 
6
  /**
@@ -9,31 +8,9 @@ import useAuthStore from '../../store/authStore';
9
  * Redirects to the login page if not authenticated.
10
  */
11
  const ProtectedRoute = () => {
12
- const { user, profile, loading, getCurrentUser } = useAuthStore();
13
- const [isChecking, setIsChecking] = useState(true);
14
 
15
- useEffect(() => {
16
- const checkSession = async () => {
17
- // Re-verify the session via Supabase as requested
18
- const { data: { session } } = await supabase.auth.getSession();
19
-
20
- if (!session) {
21
- // If no session found, ensure our store also knows there's no user
22
- setIsChecking(false);
23
- return;
24
- }
25
-
26
- // If session exists, also sync the store if needed
27
- if (!user) {
28
- await getCurrentUser();
29
- }
30
- setIsChecking(false);
31
- };
32
-
33
- checkSession();
34
- }, [user, getCurrentUser]);
35
-
36
- if (loading || isChecking) {
37
  return (
38
  <div className="flex h-screen w-screen items-center justify-center bg-white">
39
  <div className="h-12 w-12 animate-spin rounded-full border-4 border-emerald-600 border-t-transparent"></div>
 
1
+ import React from 'react';
2
  import { Navigate, Outlet } from 'react-router-dom';
 
3
  import useAuthStore from '../../store/authStore';
4
 
5
  /**
 
8
  * Redirects to the login page if not authenticated.
9
  */
10
  const ProtectedRoute = () => {
11
+ const { user, profile, isCheckingSession } = useAuthStore();
 
12
 
13
+ if (isCheckingSession) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  return (
15
  <div className="flex h-screen w-screen items-center justify-center bg-white">
16
  <div className="h-12 w-12 animate-spin rounded-full border-4 border-emerald-600 border-t-transparent"></div>
Frontend/src/components/shared/TicketChat.jsx CHANGED
@@ -1,5 +1,8 @@
1
  import React, { useState, useRef, useEffect, useCallback } from 'react';
2
- import { Send, User, ShieldCheck, Bot, MessageSquare, Circle, Loader2 } from 'lucide-react';
 
 
 
3
  import { supabase } from "../../lib/supabaseClient";
4
  import useAuthStore from "../../store/authStore";
5
 
@@ -15,6 +18,24 @@ const TicketChat = ({ ticketId, currentUserRole = 'user' }) => {
15
  const [isInternal, setIsInternal] = useState(false);
16
  const [isStaff, setIsStaff] = useState(false);
17
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  const { user, profile } = useAuthStore();
19
  const messagesEndRef = useRef(null);
20
  const scrollContainerRef = useRef(null);
@@ -128,6 +149,9 @@ const TicketChat = ({ ticketId, currentUserRole = 'user' }) => {
128
 
129
  return () => {
130
  supabase.removeChannel(channel);
 
 
 
131
  };
132
 
133
 
@@ -233,6 +257,125 @@ const TicketChat = ({ ticketId, currentUserRole = 'user' }) => {
233
  } catch { return ''; }
234
  };
235
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
236
  const grouped = [];
237
  let lastDate = null;
238
  messages.forEach((msg) => {
@@ -245,7 +388,20 @@ const TicketChat = ({ ticketId, currentUserRole = 'user' }) => {
245
  });
246
 
247
  return (
248
- <div style={{ background: '#ffffff', borderRadius: '20px', border: '1px solid #f0fdf4', height: '100%', display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
 
 
 
 
 
 
 
 
 
 
 
 
 
249
  {/* Header */}
250
  <div style={{ padding: '12px 20px', borderBottom: '1px solid #f0fdf4', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
251
  <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
@@ -254,16 +410,18 @@ const TicketChat = ({ ticketId, currentUserRole = 'user' }) => {
254
  </h2>
255
  </div>
256
 
257
- <div className="flex items-center gap-4">
258
  {isStaff && (
259
  <div style={{ display: 'flex', alignItems: 'center', background: 'transparent', gap: '4px' }}>
260
  <button
 
261
  onClick={() => setIsInternal(false)}
262
  style={{ padding: '4px 12px', fontSize: '10px', fontWeight: 700, borderRadius: '8px', cursor: 'pointer', border: 'none', transition: 'all 0.2s', ...( !isInternal ? { background: '#0f1f12', color: '#ffffff' } : { background: 'transparent', color: '#6b7280' } ) }}
263
  >
264
  PUBLIC
265
  </button>
266
  <button
 
267
  onClick={() => setIsInternal(true)}
268
  style={{ padding: '4px 12px', fontSize: '10px', fontWeight: 700, borderRadius: '8px', cursor: 'pointer', border: 'none', transition: 'all 0.2s', ...( isInternal ? { background: '#0f1f12', color: '#ffffff' } : { background: 'transparent', color: '#6b7280' } ) }}
269
  >
@@ -271,6 +429,26 @@ const TicketChat = ({ ticketId, currentUserRole = 'user' }) => {
271
  </button>
272
  </div>
273
  )}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
274
  </div>
275
  </div>
276
 
@@ -304,6 +482,12 @@ const TicketChat = ({ ticketId, currentUserRole = 'user' }) => {
304
  const msg = item.data;
305
  const isMe = msg.sender_id === user?.id;
306
  const isAdmin = msg.sender_role === 'admin' || msg.sender_role === 'super_admin';
 
 
 
 
 
 
307
 
308
  return (
309
  <div key={msg.id || i} className={`flex gap-2.5 ${isMe ? 'justify-end' : 'justify-start'} group py-1`}>
@@ -322,6 +506,15 @@ const TicketChat = ({ ticketId, currentUserRole = 'user' }) => {
322
  <span className="text-[8px] font-bold text-slate-300">
323
  {formatTime(msg.created_at)}
324
  </span>
 
 
 
 
 
 
 
 
 
325
  </div>
326
 
327
  <div style={{
@@ -334,7 +527,47 @@ const TicketChat = ({ ticketId, currentUserRole = 'user' }) => {
334
  background: '#0f1f12', color: '#ffffff', borderRadius: '14px'
335
  })
336
  }}>
337
- {msg.message}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
338
  </div>
339
  </div>
340
 
@@ -358,27 +591,111 @@ const TicketChat = ({ ticketId, currentUserRole = 'user' }) => {
358
 
359
  {/* Input */}
360
  <div style={{ padding: '16px 20px', borderTop: '1px solid #f0fdf4', background: '#ffffff' }}>
361
- <form onSubmit={handleSend} className="flex gap-3">
362
- <input
363
- ref={inputRef}
364
- type="text"
365
- value={inputValue}
366
- onChange={handleInputChange}
367
- placeholder="Type your message..."
368
- style={{ flex: 1, background: '#f9fafb', border: '1.5px solid #e5e7eb', borderRadius: '12px', padding: '10px 16px', fontSize: '13px', outline: 'none' }}
369
- className="focus:border-emerald-500 transition-colors"
370
- />
371
- <button
372
- type="submit"
373
- disabled={!inputValue.trim() || !user}
374
- style={{ padding: '10px 20px', background: '#16a34a', color: '#ffffff', borderRadius: '10px', fontWeight: 600, fontSize: '12px', border: 'none', cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '8px' }}
375
- className="active:scale-95 transition-transform disabled:opacity-50"
376
- >
377
- {isInternal ? <ShieldCheck size={14} /> : <Send size={14} />}
378
- <span className="hidden sm:inline">{isInternal ? 'Note' : 'Send'}</span>
379
- </button>
380
- </form>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
381
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
382
  </div>
383
  );
384
  };
 
1
  import React, { useState, useRef, useEffect, useCallback } from 'react';
2
+ import {
3
+ Send, User, ShieldCheck, Bot, MessageSquare, Circle, Loader2,
4
+ Phone, Video, Mic, Smile, Paperclip, Play, Pause, X, Check, CheckCheck, MicOff, Volume2, Shield
5
+ } from 'lucide-react';
6
  import { supabase } from "../../lib/supabaseClient";
7
  import useAuthStore from "../../store/authStore";
8
 
 
18
  const [isInternal, setIsInternal] = useState(false);
19
  const [isStaff, setIsStaff] = useState(false);
20
 
21
+ // Simulated calling states
22
+ const [activeCall, setActiveCall] = useState(null); // 'Audio' | 'Video' | null
23
+ const [callDuration, setCallDuration] = useState(0);
24
+ const [callStatus, setCallStatus] = useState('Ringing...'); // 'Ringing...' | 'Connected'
25
+ const callTimerRef = useRef(null);
26
+ const [isMuted, setIsMuted] = useState(false);
27
+ const [isSpeakerOn, setIsSpeakerOn] = useState(true);
28
+
29
+ // Voice note simulation states
30
+ const [isRecording, setIsRecording] = useState(false);
31
+ const [recordDuration, setRecordDuration] = useState(0);
32
+ const recordTimerRef = useRef(null);
33
+
34
+ // Voice playback states
35
+ const [playingVoiceId, setPlayingVoiceId] = useState(null);
36
+ const [voiceProgress, setVoiceProgress] = useState({});
37
+ const voicePlayTimerRef = useRef(null);
38
+
39
  const { user, profile } = useAuthStore();
40
  const messagesEndRef = useRef(null);
41
  const scrollContainerRef = useRef(null);
 
149
 
150
  return () => {
151
  supabase.removeChannel(channel);
152
+ if (callTimerRef.current) clearInterval(callTimerRef.current);
153
+ if (recordTimerRef.current) clearInterval(recordTimerRef.current);
154
+ if (voicePlayTimerRef.current) clearInterval(voicePlayTimerRef.current);
155
  };
156
 
157
 
 
257
  } catch { return ''; }
258
  };
259
 
260
+ // Voice note simulation helpers
261
+ const startRecording = () => {
262
+ setIsRecording(true);
263
+ setRecordDuration(0);
264
+ recordTimerRef.current = setInterval(() => {
265
+ setRecordDuration(prev => prev + 1);
266
+ }, 1000);
267
+ };
268
+
269
+ const stopRecording = async (shouldSave = true) => {
270
+ setIsRecording(false);
271
+ if (recordTimerRef.current) clearInterval(recordTimerRef.current);
272
+ if (shouldSave && recordDuration > 0) {
273
+ const durationStr = formatDuration(recordDuration);
274
+ const content = `🎤 Voice message (${durationStr})`;
275
+ const tempMessage = {
276
+ id: `temp-${Date.now()}`,
277
+ ticket_id: ticketId,
278
+ sender_id: user.id,
279
+ sender_name: profile?.full_name || user.email,
280
+ sender_role: profile?.role || 'user',
281
+ message: content,
282
+ is_internal: false,
283
+ created_at: new Date().toISOString()
284
+ };
285
+
286
+ setMessages(prev => [...prev, tempMessage]);
287
+ setTimeout(() => scrollToBottom(), 50);
288
+
289
+ try {
290
+ const { error } = await supabase
291
+ .from('ticket_messages')
292
+ .insert([{
293
+ ticket_id: ticketId,
294
+ sender_id: user.id,
295
+ sender_name: profile?.full_name || user.email,
296
+ sender_role: profile?.role || 'user',
297
+ message: content
298
+ }]);
299
+ if (error) throw error;
300
+ } catch (err) {
301
+ console.error("Error sending voice message:", err);
302
+ setMessages(prev => prev.filter(m => m.id !== tempMessage.id));
303
+ }
304
+ }
305
+ };
306
+
307
+ const playVoiceNote = (msgId, durationStr) => {
308
+ if (playingVoiceId === msgId) {
309
+ setPlayingVoiceId(null);
310
+ if (voicePlayTimerRef.current) clearInterval(voicePlayTimerRef.current);
311
+ return;
312
+ }
313
+
314
+ if (voicePlayTimerRef.current) clearInterval(voicePlayTimerRef.current);
315
+ setPlayingVoiceId(msgId);
316
+
317
+ const parts = durationStr.split(':');
318
+ const totalSecs = parseInt(parts[0] || '0') * 60 + parseInt(parts[1] || '0');
319
+ let currentSecs = voiceProgress[msgId] ? (voiceProgress[msgId] / 100) * totalSecs : 0;
320
+
321
+ voicePlayTimerRef.current = setInterval(() => {
322
+ currentSecs += 0.25;
323
+ const percentage = Math.min((currentSecs / totalSecs) * 100, 100);
324
+
325
+ setVoiceProgress(prev => ({
326
+ ...prev,
327
+ [msgId]: percentage
328
+ }));
329
+
330
+ if (percentage >= 100) {
331
+ setPlayingVoiceId(null);
332
+ clearInterval(voicePlayTimerRef.current);
333
+ setVoiceProgress(prev => ({
334
+ ...prev,
335
+ [msgId]: 0
336
+ }));
337
+ }
338
+ }, 250);
339
+ };
340
+
341
+ const handleCallPress = (type) => {
342
+ setActiveCall(type);
343
+ setCallStatus('Ringing...');
344
+ setCallDuration(0);
345
+ setIsMuted(false);
346
+ setIsSpeakerOn(true);
347
+
348
+ setTimeout(() => {
349
+ setCallStatus('Connected');
350
+ callTimerRef.current = setInterval(() => {
351
+ setCallDuration(prev => prev + 1);
352
+ }, 1000);
353
+ }, 2000);
354
+ };
355
+
356
+ const endCall = () => {
357
+ setActiveCall(null);
358
+ if (callTimerRef.current) clearInterval(callTimerRef.current);
359
+ };
360
+
361
+ const formatDuration = (sec) => {
362
+ const mins = Math.floor(sec / 60);
363
+ const secs = sec % 60;
364
+ return `${mins}:${secs < 10 ? '0' : ''}${secs}`;
365
+ };
366
+
367
+ const getTickColor = (msg) => {
368
+ if (String(msg.id).startsWith('temp-')) return '#9ca3af'; // sending / unsaved
369
+ // Double blue ticks if any admin/AI replied after this message
370
+ const hasAdminReply = messages.some(
371
+ m => m.created_at &&
372
+ new Date(m.created_at) > new Date(msg.created_at) &&
373
+ (m.sender_role === 'admin' || m.sender_role === 'super_admin' || m.sender_role === 'ai')
374
+ );
375
+ if (hasAdminReply) return '#38bdf8'; // WhatsApp active blue ticks!
376
+ return '#9ca3af'; // Grey double-ticks!
377
+ };
378
+
379
  const grouped = [];
380
  let lastDate = null;
381
  messages.forEach((msg) => {
 
388
  });
389
 
390
  return (
391
+ <div style={{ background: '#ffffff', borderRadius: '20px', border: '1px solid #f0fdf4', height: '100%', display: 'flex', flexDirection: 'column', overflow: 'hidden', position: 'relative' }}>
392
+
393
+ {/* Pulsing Dot Styles */}
394
+ <style>{`
395
+ @keyframes pulse {
396
+ 0% { opacity: 0.3; }
397
+ 50% { opacity: 1; }
398
+ 100% { opacity: 0.3; }
399
+ }
400
+ .pulse-dot {
401
+ animation: pulse 1.2s infinite;
402
+ }
403
+ `}</style>
404
+
405
  {/* Header */}
406
  <div style={{ padding: '12px 20px', borderBottom: '1px solid #f0fdf4', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
407
  <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
 
410
  </h2>
411
  </div>
412
 
413
+ <div className="flex items-center gap-4" style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
414
  {isStaff && (
415
  <div style={{ display: 'flex', alignItems: 'center', background: 'transparent', gap: '4px' }}>
416
  <button
417
+ type="button"
418
  onClick={() => setIsInternal(false)}
419
  style={{ padding: '4px 12px', fontSize: '10px', fontWeight: 700, borderRadius: '8px', cursor: 'pointer', border: 'none', transition: 'all 0.2s', ...( !isInternal ? { background: '#0f1f12', color: '#ffffff' } : { background: 'transparent', color: '#6b7280' } ) }}
420
  >
421
  PUBLIC
422
  </button>
423
  <button
424
+ type="button"
425
  onClick={() => setIsInternal(true)}
426
  style={{ padding: '4px 12px', fontSize: '10px', fontWeight: 700, borderRadius: '8px', cursor: 'pointer', border: 'none', transition: 'all 0.2s', ...( isInternal ? { background: '#0f1f12', color: '#ffffff' } : { background: 'transparent', color: '#6b7280' } ) }}
427
  >
 
429
  </button>
430
  </div>
431
  )}
432
+
433
+ {/* WhatsApp Call Header Buttons */}
434
+ <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
435
+ <button
436
+ type="button"
437
+ onClick={() => handleCallPress('Audio')}
438
+ style={{ background: 'transparent', border: 'none', cursor: 'pointer', padding: '6px', color: '#374151' }}
439
+ className="hover:text-emerald-600 transition-colors"
440
+ >
441
+ <Phone size={18} />
442
+ </button>
443
+ <button
444
+ type="button"
445
+ onClick={() => handleCallPress('Video')}
446
+ style={{ background: 'transparent', border: 'none', cursor: 'pointer', padding: '6px', color: '#374151' }}
447
+ className="hover:text-emerald-600 transition-colors"
448
+ >
449
+ <Video size={18} />
450
+ </button>
451
+ </div>
452
  </div>
453
  </div>
454
 
 
482
  const msg = item.data;
483
  const isMe = msg.sender_id === user?.id;
484
  const isAdmin = msg.sender_role === 'admin' || msg.sender_role === 'super_admin';
485
+ const isVoiceNote = msg.message && msg.message.startsWith('🎤 Voice message');
486
+ let voiceDuration = "0:00";
487
+ if (isVoiceNote) {
488
+ const match = msg.message.match(/\((.*?)\)/);
489
+ if (match) voiceDuration = match[1];
490
+ }
491
 
492
  return (
493
  <div key={msg.id || i} className={`flex gap-2.5 ${isMe ? 'justify-end' : 'justify-start'} group py-1`}>
 
506
  <span className="text-[8px] font-bold text-slate-300">
507
  {formatTime(msg.created_at)}
508
  </span>
509
+ {isMe && (
510
+ <span style={{ marginLeft: '4px' }}>
511
+ {String(msg.id).startsWith('temp-') ? (
512
+ <Check size={11} className="text-slate-300" />
513
+ ) : (
514
+ <CheckCheck size={11} style={{ color: getTickColor(msg) }} />
515
+ )}
516
+ </span>
517
+ )}
518
  </div>
519
 
520
  <div style={{
 
527
  background: '#0f1f12', color: '#ffffff', borderRadius: '14px'
528
  })
529
  }}>
530
+ {isVoiceNote ? (
531
+ <div style={{ display: 'flex', alignItems: 'center', gap: '12px', width: '220px' }}>
532
+ <button
533
+ type="button"
534
+ onClick={() => playVoiceNote(msg.id, voiceDuration)}
535
+ style={{ width: '32px', height: '32px', borderRadius: '50%', border: 'none', background: 'rgba(255,255,255,0.15)', color: isMe ? '#ffffff' : '#0f1f12', display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer' }}
536
+ >
537
+ {playingVoiceId === msg.id ? <Pause size={14} fill="currentColor" /> : <Play size={14} fill="currentColor" style={{ marginLeft: '2px' }} />}
538
+ </button>
539
+ <div style={{ flex: 1, display: 'flex', flexDirection: 'column', gap: '4px' }}>
540
+ {/* Waveform bars */}
541
+ <div style={{ display: 'flex', alignItems: 'center', gap: '3px', height: '18px' }}>
542
+ {[8, 14, 18, 10, 6, 12, 16, 10, 14, 20, 12, 8, 14, 18, 10, 8, 12, 6, 14, 8].map((barHeight, idx) => {
543
+ const barProgress = (idx / 20) * 100;
544
+ const isPlayed = (voiceProgress[msg.id] || 0) >= barProgress;
545
+ return (
546
+ <div
547
+ key={idx}
548
+ style={{
549
+ width: '2px',
550
+ borderRadius: '1px',
551
+ height: `${barHeight}px`,
552
+ background: isPlayed
553
+ ? (isMe ? '#34d399' : '#0f1f12')
554
+ : (isMe ? 'rgba(255,255,255,0.25)' : 'rgba(0,0,0,0.15)')
555
+ }}
556
+ />
557
+ );
558
+ })}
559
+ </div>
560
+ <span style={{ fontSize: '9px', fontWeight: 600, opacity: 0.8 }}>
561
+ {playingVoiceId === msg.id
562
+ ? formatDuration(Math.round(((voiceProgress[msg.id] || 0) / 100) * (parseInt(voiceDuration.split(':')[0] || '0') * 60 + parseInt(voiceDuration.split(':')[1] || '0'))))
563
+ : voiceDuration
564
+ }
565
+ </span>
566
+ </div>
567
+ </div>
568
+ ) : (
569
+ msg.message
570
+ )}
571
  </div>
572
  </div>
573
 
 
591
 
592
  {/* Input */}
593
  <div style={{ padding: '16px 20px', borderTop: '1px solid #f0fdf4', background: '#ffffff' }}>
594
+ {isRecording ? (
595
+ <div style={{ display: 'flex', alignItems: 'center', gap: '12px', background: '#f9fafb', border: '1.5px dashed #f87171', borderRadius: '12px', padding: '10px 16px', fontSize: '13px' }}>
596
+ <div className="pulse-dot" style={{ width: '8px', height: '8px', borderRadius: '50%', background: '#ef4444' }} />
597
+ <span style={{ flex: 1, color: '#ef4444', fontWeight: 700 }}>Recording {formatDuration(recordDuration)}</span>
598
+ <button
599
+ type="button"
600
+ onClick={() => stopRecording(false)}
601
+ style={{ background: 'transparent', border: 'none', color: '#9ca3af', cursor: 'pointer', fontSize: '11px', fontWeight: 700 }}
602
+ >
603
+ Cancel
604
+ </button>
605
+ <button
606
+ type="button"
607
+ onClick={() => stopRecording(true)}
608
+ style={{ background: '#ef4444', border: 'none', color: '#ffffff', borderRadius: '8px', padding: '6px 12px', cursor: 'pointer', fontSize: '11px', fontWeight: 700, display: 'flex', alignItems: 'center', gap: '4px' }}
609
+ >
610
+ <Send size={10} />
611
+ Send
612
+ </button>
613
+ </div>
614
+ ) : (
615
+ <form onSubmit={handleSend} className="flex gap-3">
616
+ <input
617
+ ref={inputRef}
618
+ type="text"
619
+ value={inputValue}
620
+ onChange={handleInputChange}
621
+ placeholder="Type your message..."
622
+ style={{ flex: 1, background: '#f9fafb', border: '1.5px solid #e5e7eb', borderRadius: '12px', padding: '10px 16px', fontSize: '13px', outline: 'none' }}
623
+ className="focus:border-emerald-500 transition-colors"
624
+ />
625
+ <button
626
+ type="button"
627
+ onClick={startRecording}
628
+ style={{ padding: '10px', background: 'rgba(0,0,0,0.05)', color: '#374151', borderRadius: '10px', border: 'none', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center' }}
629
+ className="active:scale-95 transition-transform"
630
+ >
631
+ <Mic size={16} />
632
+ </button>
633
+ <button
634
+ type="submit"
635
+ disabled={!inputValue.trim() || !user}
636
+ style={{ padding: '10px 20px', background: '#16a34a', color: '#ffffff', borderRadius: '10px', fontWeight: 600, fontSize: '12px', border: 'none', cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '8px' }}
637
+ className="active:scale-95 transition-transform disabled:opacity-50"
638
+ >
639
+ {isInternal ? <ShieldCheck size={14} /> : <Send size={14} />}
640
+ <span className="hidden sm:inline">{isInternal ? 'Note' : 'Send'}</span>
641
+ </button>
642
+ </form>
643
+ )}
644
  </div>
645
+
646
+ {/* High-Fidelity Call Overlay Modal */}
647
+ {activeCall && (
648
+ <div style={{ position: 'absolute', top: 0, bottom: 0, left: 0, right: 0, background: '#0b132b', color: '#ffffff', display: 'flex', flexDirection: 'column', justifyContent: 'space-between', padding: '40px 20px', zIndex: 99999 }}>
649
+ <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', gap: '6px', opacity: 0.6 }}>
650
+ <Shield size={12} className="text-emerald-500" />
651
+ <span style={{ fontSize: '9px', fontWeight: 800, letterSpacing: '0.1em' }}>SECURE END-TO-END ENCRYPTED</span>
652
+ </div>
653
+
654
+ <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '14px' }}>
655
+ <div style={{ width: '90px', height: '90px', borderRadius: '50%', background: '#16a34a', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '32px', fontWeight: 900, boxShadow: '0 10px 25px rgba(22,163,74,0.3)' }}>
656
+ {(ticketId || 'A')[0].toUpperCase()}
657
+ </div>
658
+ <h3 style={{ fontSize: '20px', fontWeight: 800, margin: 0 }}>Support Desk Call</h3>
659
+ <span style={{ fontSize: '13px', opacity: 0.7 }}>
660
+ {callStatus === 'Connected' ? formatDuration(callDuration) : callStatus}
661
+ </span>
662
+ </div>
663
+
664
+ {/* Web Video call screen camera mock */}
665
+ {activeCall === 'Video' && (
666
+ <div style={{ width: '100%', height: '140px', background: 'rgba(255,255,255,0.03)', borderRadius: '16px', border: '1px solid rgba(255,255,255,0.08)', position: 'relative', overflow: 'hidden', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
667
+ <div style={{ width: '40px', height: '60px', borderRadius: '8px', background: '#1d2d44', border: '1px solid rgba(255,255,255,0.1)', position: 'absolute', bottom: '12px', right: '12px' }} />
668
+ <span style={{ fontSize: '10px', opacity: 0.4 }}>Camera active stream</span>
669
+ </div>
670
+ )}
671
+
672
+ <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', gap: '20px' }}>
673
+ <button
674
+ type="button"
675
+ onClick={() => setIsMuted(!isMuted)}
676
+ style={{ width: '48px', height: '48px', borderRadius: '50%', border: 'none', background: isMuted ? '#ffffff' : 'rgba(255,255,255,0.08)', color: isMuted ? '#0b132b' : '#ffffff', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center' }}
677
+ >
678
+ {isMuted ? <MicOff size={18} /> : <Mic size={18} />}
679
+ </button>
680
+
681
+ <button
682
+ type="button"
683
+ onClick={endCall}
684
+ style={{ width: '48px', height: '48px', borderRadius: '50%', border: 'none', background: '#ef4444', color: '#ffffff', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center' }}
685
+ >
686
+ <Phone size={18} style={{ transform: 'rotate(135deg)' }} />
687
+ </button>
688
+
689
+ <button
690
+ type="button"
691
+ onClick={() => setIsSpeakerOn(!isSpeakerOn)}
692
+ style={{ width: '48px', height: '48px', borderRadius: '50%', border: 'none', background: isSpeakerOn ? '#ffffff' : 'rgba(255,255,255,0.08)', color: isSpeakerOn ? '#16a34a' : '#ffffff', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center' }}
693
+ >
694
+ <Volume2 size={18} />
695
+ </button>
696
+ </div>
697
+ </div>
698
+ )}
699
  </div>
700
  );
701
  };
Frontend/src/components/shared/WebhookSettings.jsx ADDED
@@ -0,0 +1,227 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect } from "react";
2
+ import { supabase } from "../../lib/supabaseClient";
3
+ import useAuthStore from "../../store/authStore";
4
+
5
+ /**
6
+ * WebhookSettings — Admin component to configure Slack/Teams webhook URL.
7
+ * Stores webhook URL per company tenant in Supabase.
8
+ */
9
+ const WebhookSettings = () => {
10
+ const { profile } = useAuthStore();
11
+ const [webhookUrl, setWebhookUrl] = useState("");
12
+ const [isEnabled, setIsEnabled] = useState(false);
13
+ const [loading, setLoading] = useState(false);
14
+ const [fetching, setFetching] = useState(true);
15
+ const [message, setMessage] = useState({ text: "", type: "" });
16
+
17
+ const companyId = profile?.company_id;
18
+
19
+ useEffect(() => {
20
+ const fetchSettings = async () => {
21
+ if (!companyId) return;
22
+ setFetching(true);
23
+ try {
24
+ const { data, error } = await supabase
25
+ .from("webhook_settings")
26
+ .select("webhook_url, is_enabled")
27
+ .eq("company_id", companyId)
28
+ .single();
29
+
30
+ if (data) {
31
+ setWebhookUrl(data.webhook_url || "");
32
+ setIsEnabled(data.is_enabled || false);
33
+ }
34
+ } catch (err) {
35
+ console.log("No webhook settings found — first time setup");
36
+ } finally {
37
+ setFetching(false);
38
+ }
39
+ };
40
+
41
+ fetchSettings();
42
+ }, [companyId]);
43
+
44
+ const detectPlatform = (url) => {
45
+ if (url.includes("hooks.slack.com")) return "Slack";
46
+ if (url.includes("webhook.office.com") || url.includes("outlook.office.com"))
47
+ return "Microsoft Teams";
48
+ return null;
49
+ };
50
+
51
+ const validateUrl = (url) => {
52
+ if (!url) return false;
53
+ return url.startsWith("https://") && detectPlatform(url) !== null;
54
+ };
55
+
56
+ const handleSave = async () => {
57
+ if (!validateUrl(webhookUrl)) {
58
+ setMessage({
59
+ text: "❌ Please enter a valid Slack or Microsoft Teams webhook URL.",
60
+ type: "error",
61
+ });
62
+ return;
63
+ }
64
+
65
+ setLoading(true);
66
+ setMessage({ text: "", type: "" });
67
+
68
+ try {
69
+ const { error } = await supabase.from("webhook_settings").upsert(
70
+ {
71
+ company_id: companyId,
72
+ webhook_url: webhookUrl,
73
+ is_enabled: isEnabled,
74
+ updated_at: new Date().toISOString(),
75
+ },
76
+ { onConflict: "company_id" }
77
+ );
78
+
79
+ if (error) throw error;
80
+
81
+ setMessage({
82
+ text: `✅ Webhook saved successfully for ${detectPlatform(webhookUrl)}!`,
83
+ type: "success",
84
+ });
85
+ } catch (err) {
86
+ setMessage({
87
+ text: `❌ Failed to save: ${err.message}`,
88
+ type: "error",
89
+ });
90
+ } finally {
91
+ setLoading(false);
92
+ }
93
+ };
94
+
95
+ const handleToggle = async () => {
96
+ const newValue = !isEnabled;
97
+ setIsEnabled(newValue);
98
+
99
+ if (webhookUrl) {
100
+ await supabase.from("webhook_settings").upsert(
101
+ {
102
+ company_id: companyId,
103
+ webhook_url: webhookUrl,
104
+ is_enabled: newValue,
105
+ updated_at: new Date().toISOString(),
106
+ },
107
+ { onConflict: "company_id" }
108
+ );
109
+ }
110
+ };
111
+
112
+ if (fetching) {
113
+ return (
114
+ <div className="flex items-center justify-center p-8">
115
+ <div className="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-500"></div>
116
+ </div>
117
+ );
118
+ }
119
+
120
+ const platform = detectPlatform(webhookUrl);
121
+
122
+ return (
123
+ <div className="bg-gray-900 border border-gray-700 rounded-xl p-6 max-w-2xl">
124
+ <div className="flex items-center gap-3 mb-6">
125
+ <div className="bg-blue-600 p-2 rounded-lg">
126
+ <svg className="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
127
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
128
+ d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" />
129
+ </svg>
130
+ </div>
131
+ <div>
132
+ <h3 className="text-white font-semibold text-lg">Webhook Notifications</h3>
133
+ <p className="text-gray-400 text-sm">Get critical ticket alerts in Slack or Microsoft Teams</p>
134
+ </div>
135
+ </div>
136
+
137
+ {/* Webhook URL Input */}
138
+ <div className="mb-4">
139
+ <label className="block text-gray-300 text-sm font-medium mb-2">
140
+ Webhook URL
141
+ </label>
142
+ <input
143
+ type="url"
144
+ value={webhookUrl}
145
+ onChange={(e) => {
146
+ setWebhookUrl(e.target.value);
147
+ setMessage({ text: "", type: "" });
148
+ }}
149
+ placeholder="https://hooks.slack.com/... or https://outlook.office.com/webhook/..."
150
+ className="w-full bg-gray-800 border border-gray-600 rounded-lg px-4 py-3 text-white text-sm placeholder-gray-500 focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500 transition-colors"
151
+ />
152
+ {/* Platform Detection Badge */}
153
+ {platform && (
154
+ <div className="mt-2 flex items-center gap-2">
155
+ <span className={`text-xs px-2 py-1 rounded-full font-medium ${
156
+ platform === "Slack"
157
+ ? "bg-green-900 text-green-300"
158
+ : "bg-blue-900 text-blue-300"
159
+ }`}>
160
+ ✓ {platform} webhook detected
161
+ </span>
162
+ </div>
163
+ )}
164
+ </div>
165
+
166
+ {/* Toggle */}
167
+ <div className="flex items-center justify-between mb-6 p-4 bg-gray-800 rounded-lg">
168
+ <div>
169
+ <p className="text-white text-sm font-medium">Enable Notifications</p>
170
+ <p className="text-gray-400 text-xs">
171
+ Send alerts for Critical/High priority tickets
172
+ </p>
173
+ </div>
174
+ <button
175
+ onClick={handleToggle}
176
+ className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none ${
177
+ isEnabled ? "bg-blue-600" : "bg-gray-600"
178
+ }`}
179
+ >
180
+ <span
181
+ className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
182
+ isEnabled ? "translate-x-6" : "translate-x-1"
183
+ }`}
184
+ />
185
+ </button>
186
+ </div>
187
+
188
+ {/* Message */}
189
+ {message.text && (
190
+ <div className={`mb-4 p-3 rounded-lg text-sm ${
191
+ message.type === "success"
192
+ ? "bg-green-900/50 text-green-300 border border-green-700"
193
+ : "bg-red-900/50 text-red-300 border border-red-700"
194
+ }`}>
195
+ {message.text}
196
+ </div>
197
+ )}
198
+
199
+ {/* Save Button */}
200
+ <button
201
+ onClick={handleSave}
202
+ disabled={loading || !webhookUrl}
203
+ className="w-full bg-blue-600 hover:bg-blue-700 disabled:bg-gray-600 disabled:cursor-not-allowed text-white font-medium py-3 px-4 rounded-lg transition-colors text-sm"
204
+ >
205
+ {loading ? (
206
+ <span className="flex items-center justify-center gap-2">
207
+ <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
208
+ Saving...
209
+ </span>
210
+ ) : (
211
+ "Save Webhook Settings"
212
+ )}
213
+ </button>
214
+
215
+ {/* Info */}
216
+ <div className="mt-4 p-3 bg-gray-800 rounded-lg">
217
+ <p className="text-gray-400 text-xs leading-relaxed">
218
+ <span className="text-yellow-400 font-medium">ℹ️ How to get webhook URL: </span>
219
+ For Slack: Create an Incoming Webhook in your Slack App settings.
220
+ For Teams: Go to channel → Connectors → Incoming Webhook.
221
+ </p>
222
+ </div>
223
+ </div>
224
+ );
225
+ };
226
+
227
+ export default WebhookSettings;
Frontend/src/config.js CHANGED
@@ -6,16 +6,13 @@ const getBackendUrl = () => {
6
  const envUrl = import.meta.env.VITE_BACKEND_URL;
7
  if (envUrl) return envUrl.trim().replace(/\/$/, '');
8
 
9
- // Dynamically deduce backend URL if running locally or on custom domain
10
- if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {
11
- return 'http://localhost:8000';
12
- }
13
- // Default to the live Hugging Face Space for stability in production deployment
14
  return 'https://ritesh19180-ai-helpdesk-api.hf.space';
15
  };
16
 
17
  export const API_CONFIG = {
18
  BACKEND_URL: getBackendUrl(),
19
  FRONTEND_URL: window.location.origin,
20
- IS_PROD: import.meta.env.PROD
 
21
  };
 
6
  const envUrl = import.meta.env.VITE_BACKEND_URL;
7
  if (envUrl) return envUrl.trim().replace(/\/$/, '');
8
 
9
+ // Default to the live Hugging Face Space for stability
 
 
 
 
10
  return 'https://ritesh19180-ai-helpdesk-api.hf.space';
11
  };
12
 
13
  export const API_CONFIG = {
14
  BACKEND_URL: getBackendUrl(),
15
  FRONTEND_URL: window.location.origin,
16
+ IS_PROD: import.meta.env.PROD,
17
+ USE_MOCK: import.meta.env.VITE_USE_MOCK !== 'false' // default true
18
  };
Frontend/src/index.css CHANGED
@@ -43,10 +43,12 @@
43
  }
44
 
45
  body {
46
- @apply bg-[#f6f8f7] text-slate-900;
47
  }
48
 
49
  .dark {
 
 
50
  --background: 224 71.4% 4.1%;
51
  --foreground: 210 20% 98%;
52
  --card: 224 71.4% 4.1%;
@@ -91,16 +93,54 @@
91
  }
92
  }
93
 
94
- html{
95
  overflow-y: scroll;
96
- scrollbar-width: none; /* Firefox */
97
- }
98
-
99
- html::-webkit-scrollbar{
100
- width: 0;
101
- height: 0;
102
  }
103
 
104
  body{
105
  overflow-x: hidden;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
  }
 
43
  }
44
 
45
  body {
46
+ @apply bg-[#f6f8f7] text-slate-900 dark:bg-gray-900 dark:text-white;
47
  }
48
 
49
  .dark {
50
+ background-color: #111827;
51
+ color: #f9fafb;
52
  --background: 224 71.4% 4.1%;
53
  --foreground: 210 20% 98%;
54
  --card: 224 71.4% 4.1%;
 
93
  }
94
  }
95
 
96
+ html {
97
  overflow-y: scroll;
 
 
 
 
 
 
98
  }
99
 
100
  body{
101
  overflow-x: hidden;
102
+ }
103
+
104
+ /* Dark Mode Global Overrides */
105
+ .dark .bg-white {
106
+ background-color: #111827 !important;
107
+ }
108
+ .dark .bg-gray-50 {
109
+ background-color: #1f2937 !important;
110
+ }
111
+ .dark .bg-gray-100 {
112
+ background-color: #374151 !important;
113
+ }
114
+ .dark .text-gray-900 {
115
+ color: #f9fafb !important;
116
+ }
117
+ .dark .text-gray-800 {
118
+ color: #f3f4f6 !important;
119
+ }
120
+ .dark .text-gray-700 {
121
+ color: #e5e7eb !important;
122
+ }
123
+ .dark .text-gray-600 {
124
+ color: #d1d5db !important;
125
+ }
126
+ .dark .text-gray-500 {
127
+ color: #9ca3af !important;
128
+ }
129
+ .dark .border-gray-100 {
130
+ border-color: #374151 !important;
131
+ }
132
+ .dark .border-gray-200 {
133
+ border-color: #4b5563 !important;
134
+ }
135
+ .dark .shadow-sm {
136
+ box-shadow: 0 1px 2px rgba(0,0,0,0.5) !important;
137
+ }
138
+ .dark nav {
139
+ background-color: #1f2937 !important;
140
+ }
141
+ .dark nav a {
142
+ color: #e5e7eb !important;
143
+ }
144
+ .dark nav span {
145
+ color: #f9fafb !important;
146
  }
Frontend/src/legacy_ui/Dashboard.jsx CHANGED
@@ -63,7 +63,7 @@ const Dashboard = () => {
63
  }
64
  };
65
  fetchTickets();
66
- // eslint-disable-next-line react-hooks/exhaustive-deps
67
  }, []);
68
 
69
  // Detect newly inserted or updated tickets for highlight animation
@@ -84,7 +84,7 @@ const Dashboard = () => {
84
  });
85
 
86
  prevTicketsRef.current = tickets;
87
- // eslint-disable-next-line react-hooks/exhaustive-deps
88
  }, [tickets]);
89
 
90
  // Summary Counts
 
63
  }
64
  };
65
  fetchTickets();
66
+
67
  }, []);
68
 
69
  // Detect newly inserted or updated tickets for highlight animation
 
84
  });
85
 
86
  prevTicketsRef.current = tickets;
87
+
88
  }, [tickets]);
89
 
90
  // Summary Counts
Frontend/src/pages/AboutUs.jsx CHANGED
@@ -1,65 +1,387 @@
1
- import React from 'react';
2
- import { useNavigate } from 'react-router-dom';
3
- import { ShieldCheck, Heart, Sparkles, ArrowLeft, Target, Award } from 'lucide-react';
4
- import { Card } from '../components/ui/card';
5
-
6
- export default function AboutUs() {
7
- const navigate = useNavigate();
8
-
9
- return (
10
- <div className="min-h-screen bg-[#f6f8f7] pb-20">
11
- {/* Header */}
12
- <header className="w-full bg-white border-b border-gray-200 sticky top-0 z-50">
13
- <div className="max-w-[1100px] mx-auto px-4 md:px-6 h-16 flex items-center justify-between">
14
- <div className="flex items-center gap-3 cursor-pointer" onClick={() => navigate('/')}>
15
- <img src="/favicon.png" alt="HELPDESK.AI Logo" className="w-7 h-7 object-contain" />
16
- <div className="flex items-baseline gap-2">
17
- <h1 className="text-xl font-black tracking-tighter text-gray-900 italic">HELPDESK.AI</h1>
18
- <span className="px-2 py-0.5 text-[10px] font-black bg-emerald-100 text-emerald-800 rounded-md uppercase tracking-wider">About</span>
19
- </div>
20
- </div>
21
- <button
22
- onClick={() => navigate('/')}
23
- className="flex items-center gap-2 text-xs font-bold text-gray-600 hover:text-emerald-600 transition-colors bg-gray-50 hover:bg-emerald-50 px-3.5 py-2 rounded-xl border border-gray-200"
24
- >
25
- <ArrowLeft size={14} /> Back to Home
26
- </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  </div>
28
- </header>
29
-
30
- <div className="max-w-[800px] mx-auto px-4 md:px-6 mt-12 space-y-12">
31
- <div className="space-y-4">
32
- <div className="inline-flex items-center gap-2 px-3 py-1 bg-emerald-500/10 border border-emerald-500/20 rounded-full text-emerald-700 text-xs font-bold">
33
- <Heart size={14} /> Our Mission
34
- </div>
35
- <h1 className="text-4xl font-black text-slate-900 tracking-tight">Pioneering Intelligent Triage</h1>
36
- <p className="text-slate-600 text-base leading-relaxed">
37
- At HELPDESK.AI, we strive to build local machine learning workflows that eliminate manual ticket tagging, priority guessing, and routing bottlenecks for modern Indian businesses.
38
- </p>
39
  </div>
 
 
 
40
 
41
- <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
42
- <Card className="p-6 rounded-[2rem] border border-slate-200 bg-white space-y-3">
43
- <div className="w-8 h-8 rounded-xl bg-emerald-50 text-emerald-600 flex items-center justify-center">
44
- <Target size={16} />
45
- </div>
46
- <h4 className="font-extrabold text-slate-800 text-sm">Self-Healing Backups</h4>
47
- <p className="text-xs text-slate-500 leading-relaxed font-semibold">
48
- By backing up offline sentence embeddings with fast Gemini failover pipelines, we achieve 100% platform availability under tight network margins.
49
- </p>
50
- </Card>
51
-
52
- <Card className="p-6 rounded-[2rem] border border-slate-200 bg-white space-y-3">
53
- <div className="w-8 h-8 rounded-xl bg-blue-50 text-blue-600 flex items-center justify-center">
54
- <Award size={16} />
55
- </div>
56
- <h4 className="font-extrabold text-slate-800 text-sm">100% Indian Data Sovereignty</h4>
57
- <p className="text-xs text-slate-500 leading-relaxed font-semibold">
58
- All ticket summaries, OCR attachments, and database timelines remain securely locked under regional cloud networks.
59
- </p>
60
- </Card>
61
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
  </div>
63
- </div>
64
- );
65
- }
 
 
 
 
 
 
 
1
+ import React from "react";
2
+ import { useNavigate } from "react-router-dom";
3
+ import { motion } from "framer-motion";
4
+ import {
5
+ ArrowLeft,
6
+ FolderOpen,
7
+ Zap,
8
+ Layers,
9
+ ShieldCheck,
10
+ Database,
11
+ Server,
12
+ Laptop,
13
+ Github,
14
+ ExternalLink,
15
+ Users,
16
+ BookOpen,
17
+ HelpCircle,
18
+ Code
19
+ } from "lucide-react";
20
+
21
+ // Framer Motion Animation Variants
22
+ const fadeInUp = {
23
+ hidden: { opacity: 0, y: 30 },
24
+ visible: { opacity: 1, y: 0, transition: { duration: 0.6, ease: "easeOut" } }
25
+ };
26
+
27
+ const staggerContainer = {
28
+ hidden: { opacity: 0 },
29
+ visible: {
30
+ opacity: 1,
31
+ transition: {
32
+ staggerChildren: 0.1
33
+ }
34
+ }
35
+ };
36
+
37
+ const AboutUs = () => {
38
+ const navigate = useNavigate();
39
+
40
+ const capabilities = [
41
+ {
42
+ icon: <FolderOpen className="w-6 h-6 text-emerald-400" />,
43
+ title: "AI Classification",
44
+ desc: "Instantly categorizes requests into Network, Hardware, Software, or Access categories using natural language understanding."
45
+ },
46
+ {
47
+ icon: <Zap className="w-6 h-6 text-emerald-400" />,
48
+ title: "Smart Resolution",
49
+ desc: "Leverages historical ticket resolutions to automatically generate accurate solution drafts or self-solve simple issues."
50
+ },
51
+ {
52
+ icon: <Layers className="w-6 h-6 text-emerald-400" />,
53
+ title: "Duplicate Detection",
54
+ desc: "Uses vector similarity search to identify and group duplicate incident reports in real time, preventing agent alert fatigue."
55
+ },
56
+ {
57
+ icon: <ShieldCheck className="w-6 h-6 text-emerald-400" />,
58
+ title: "Multi-Tenant Isolation",
59
+ desc: "Ensures strict enterprise-grade security isolation and customized configuration profiles for each registered organization."
60
+ }
61
+ ];
62
+
63
+ const pipelineStages = [
64
+ {
65
+ step: "01",
66
+ title: "Ingestion",
67
+ desc: "Support tickets are ingested seamlessly via email parsers, customer web widgets, or direct dashboard submission."
68
+ },
69
+ {
70
+ step: "02",
71
+ title: "Extraction",
72
+ desc: "System runs OCR to extract text from screenshots and applies Named Entity Recognition (NER) to pull system names and keywords."
73
+ },
74
+ {
75
+ step: "03",
76
+ title: "Classification",
77
+ desc: "Advanced NLP models predict the correct ticket category and calculate priority based on urgency cues."
78
+ },
79
+ {
80
+ step: "04",
81
+ title: "Similarity Scan",
82
+ desc: "A vector similarity scan determines if this incident is already being resolved, linking duplicate cases automatically."
83
+ },
84
+ {
85
+ step: "05",
86
+ title: "Auto-Resolution",
87
+ desc: "Matched resolutions are evaluated; if confidence thresholds are met, the system auto-resolves the ticket and emails the user."
88
+ },
89
+ {
90
+ step: "06",
91
+ title: "Smart Triage",
92
+ desc: "Unresolved tickets are automatically routed to the precise engineering team queues with complete AI logs attached."
93
+ }
94
+ ];
95
+
96
+ const techStack = [
97
+ {
98
+ category: "Frontend Experience",
99
+ icon: <Laptop className="w-5 h-5 text-emerald-400" />,
100
+ techs: ["React (Vite)", "Tailwind CSS", "Framer Motion", "Lucide React"]
101
+ },
102
+ {
103
+ category: "Intelligence & Backend",
104
+ icon: <Server className="w-5 h-5 text-emerald-400" />,
105
+ techs: ["FastAPI (Python)", "Hugging Face Hub", "Transformers & PyTorch", "OCR Engine"]
106
+ },
107
+ {
108
+ category: "Database & Services",
109
+ icon: <Database className="w-5 h-5 text-emerald-400" />,
110
+ techs: ["Supabase", "PostgreSQL", "pgvector (Similarity)", "Realtime Webhooks"]
111
+ }
112
+ ];
113
+
114
+ return (
115
+ <div className="min-h-screen bg-[#021510] text-white relative overflow-hidden font-sans">
116
+ {/* Background Ambient Glows */}
117
+ <div className="absolute top-0 left-0 w-[500px] h-[500px] bg-emerald-500/10 blur-[130px] rounded-full pointer-events-none" />
118
+ <div className="absolute bottom-0 right-0 w-[500px] h-[500px] bg-teal-400/10 blur-[130px] rounded-full pointer-events-none" />
119
+ <div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[350px] h-[350px] bg-green-500/5 blur-[120px] rounded-full pointer-events-none" />
120
+
121
+ {/* Main Container */}
122
+ <div className="max-w-6xl mx-auto px-6 py-12 relative z-10">
123
+
124
+ {/* Navigation & Header */}
125
+ <motion.div
126
+ initial={{ opacity: 0, y: -10 }}
127
+ animate={{ opacity: 1, y: 0 }}
128
+ transition={{ duration: 0.5 }}
129
+ className="mb-12 flex justify-between items-center"
130
+ >
131
+ <button
132
+ onClick={() => navigate("/")}
133
+ className="group inline-flex items-center gap-2 bg-emerald-500/10 border border-emerald-500/20 text-emerald-400 hover:bg-emerald-500/20 hover:border-emerald-500/30 px-4 py-2 rounded-full text-sm font-semibold tracking-wide transition-all shadow-md"
134
+ >
135
+ <ArrowLeft size={16} className="group-hover:-translate-x-1 transition-transform" />
136
+ Back to Home
137
+ </button>
138
+
139
+ <div className="flex items-center gap-2">
140
+ <span className="w-2.5 h-2.5 rounded-full bg-emerald-500 animate-pulse" />
141
+ <span className="text-xs font-bold text-emerald-400 uppercase tracking-widest">GSSoC 2026 Compliant</span>
142
+ </div>
143
+ </motion.div>
144
+
145
+ {/* Hero Section */}
146
+ <section className="text-center mb-24">
147
+ <motion.div
148
+ initial="hidden"
149
+ animate="visible"
150
+ variants={fadeInUp}
151
+ className="max-w-3xl mx-auto"
152
+ >
153
+ <h1 className="text-4xl md:text-6xl font-extrabold mb-6 tracking-tight leading-tight">
154
+ About
155
+ <span className="bg-gradient-to-r from-emerald-400 to-teal-300 bg-clip-text text-transparent">
156
+ {" "}HELPDESK.AI
157
+ </span>
158
+ </h1>
159
+ <p className="text-gray-400 text-lg md:text-xl leading-relaxed mb-8">
160
+ A modern, intelligent, and autonomous helpdesk platform designed to end manual ticket triage. By utilizing advanced Natural Language Processing and Vector similarity, we streamline support workflows from chaos to clarity.
161
+ </p>
162
+ </motion.div>
163
+ </section>
164
+
165
+ {/* Mission Statement */}
166
+ <section className="mb-28">
167
+ <motion.div
168
+ initial="hidden"
169
+ whileInView="visible"
170
+ viewport={{ once: true, margin: "-100px" }}
171
+ variants={fadeInUp}
172
+ className="bg-white/5 border border-white/10 rounded-3xl p-8 md:p-12 relative overflow-hidden backdrop-blur-xl"
173
+ >
174
+ <div className="absolute inset-0 bg-gradient-to-br from-emerald-500/5 to-transparent pointer-events-none" />
175
+ <div className="relative z-10 max-w-4xl mx-auto text-center">
176
+ <span className="text-xs font-bold uppercase tracking-widest text-emerald-400 mb-4 block">Our Vision</span>
177
+ <h2 className="text-2xl md:text-3xl font-bold mb-6 text-white tracking-tight">
178
+ "Eliminating manual ticket routing and solving repeating support incidents instantly."
179
+ </h2>
180
+ <p className="text-gray-400 leading-relaxed max-w-2xl mx-auto">
181
+ We believe engineering and IT support teams shouldn't spend their valuable hours reviewing, tagging, and assigning tickets. Our vision is a self-driving support ecosystem that analyzes context, remembers historical resolutions, and automates resolution at scale.
182
+ </p>
183
+ </div>
184
+ </motion.div>
185
+ </section>
186
+
187
+ {/* Core Capabilities */}
188
+ <section className="mb-28">
189
+ <div className="text-center mb-16">
190
+ <span className="text-xs font-bold uppercase tracking-widest text-emerald-400 mb-3 block">Platform Intelligence</span>
191
+ <h2 className="text-3xl font-extrabold tracking-tight">Core Capabilities</h2>
192
+ </div>
193
+
194
+ <motion.div
195
+ initial="hidden"
196
+ whileInView="visible"
197
+ viewport={{ once: true, margin: "-50px" }}
198
+ variants={staggerContainer}
199
+ className="grid grid-cols-1 md:grid-cols-2 gap-8"
200
+ >
201
+ {capabilities.map((cap, idx) => (
202
+ <motion.div
203
+ key={idx}
204
+ variants={fadeInUp}
205
+ whileHover={{ scale: 1.01, borderColor: "rgba(16,185,129,0.3)" }}
206
+ className="bg-white/5 border border-white/10 p-8 rounded-3xl backdrop-blur-xl transition-colors duration-300 group"
207
+ >
208
+ <div className="w-12 h-12 rounded-2xl bg-emerald-500/10 border border-emerald-500/20 flex items-center justify-center mb-6 group-hover:bg-emerald-500/20 transition-all shadow-inner">
209
+ {cap.icon}
210
+ </div>
211
+ <h3 className="text-xl font-bold mb-3 text-white">{cap.title}</h3>
212
+ <p className="text-gray-400 text-sm leading-relaxed">{cap.desc}</p>
213
+ </motion.div>
214
+ ))}
215
+ </motion.div>
216
+ </section>
217
+
218
+ {/* AI Pipeline Architecture */}
219
+ <section className="mb-28">
220
+ <div className="text-center mb-16">
221
+ <span className="text-xs font-bold uppercase tracking-widest text-emerald-400 mb-3 block">System Workflow</span>
222
+ <h2 className="text-3xl font-extrabold tracking-tight">AI Pipeline Architecture</h2>
223
+ <p className="text-gray-400 mt-2 text-sm">How HELPDESK.AI processes and resolves issues in milliseconds</p>
224
+ </div>
225
+
226
+ <motion.div
227
+ initial="hidden"
228
+ whileInView="visible"
229
+ viewport={{ once: true, margin: "-50px" }}
230
+ variants={staggerContainer}
231
+ className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"
232
+ >
233
+ {pipelineStages.map((stage, idx) => (
234
+ <motion.div
235
+ key={idx}
236
+ variants={fadeInUp}
237
+ className="bg-white/5 border border-white/10 p-6 rounded-2xl relative overflow-hidden backdrop-blur-xl hover:border-emerald-500/20 transition-colors"
238
+ >
239
+ <div className="absolute top-4 right-4 text-4xl font-extrabold text-emerald-500/10 select-none">
240
+ {stage.step}
241
+ </div>
242
+ <div className="inline-flex items-center justify-center w-8 h-8 rounded-lg bg-emerald-500/10 text-emerald-400 font-bold text-xs mb-4 border border-emerald-500/25">
243
+ {stage.step}
244
+ </div>
245
+ <h3 className="text-lg font-bold mb-2 text-white">{stage.title}</h3>
246
+ <p className="text-gray-400 text-xs leading-relaxed">{stage.desc}</p>
247
+ </motion.div>
248
+ ))}
249
+ </motion.div>
250
+ </section>
251
+
252
+ {/* Tech Stack Grid */}
253
+ <section className="mb-28">
254
+ <div className="text-center mb-16">
255
+ <span className="text-xs font-bold uppercase tracking-widest text-emerald-400 mb-3 block">Technology Stack</span>
256
+ <h2 className="text-3xl font-extrabold tracking-tight">Built with Modern Tech</h2>
257
+ </div>
258
+
259
+ <motion.div
260
+ initial="hidden"
261
+ whileInView="visible"
262
+ viewport={{ once: true, margin: "-50px" }}
263
+ variants={staggerContainer}
264
+ className="grid grid-cols-1 md:grid-cols-3 gap-8"
265
+ >
266
+ {techStack.map((stack, idx) => (
267
+ <motion.div
268
+ key={idx}
269
+ variants={fadeInUp}
270
+ className="bg-white/5 border border-white/10 p-8 rounded-3xl backdrop-blur-xl relative overflow-hidden"
271
+ >
272
+ <div className="flex items-center gap-3 mb-6">
273
+ <div className="p-2 rounded-xl bg-emerald-500/10 border border-emerald-500/20">
274
+ {stack.icon}
275
+ </div>
276
+ <h3 className="text-lg font-bold text-white leading-tight">{stack.category}</h3>
277
+ </div>
278
+ <ul className="space-y-3">
279
+ {stack.techs.map((tech, techIdx) => (
280
+ <li key={techIdx} className="flex items-center gap-2.5 text-gray-400 text-sm">
281
+ <span className="w-1.5 h-1.5 rounded-full bg-emerald-500" />
282
+ {tech}
283
+ </li>
284
+ ))}
285
+ </ul>
286
+ </motion.div>
287
+ ))}
288
+ </motion.div>
289
+ </section>
290
+
291
+ {/* Resource Links */}
292
+ <section className="mb-28">
293
+ <div className="text-center mb-12">
294
+ <span className="text-xs font-bold uppercase tracking-widest text-emerald-400 mb-3 block">External Resources</span>
295
+ <h2 className="text-3xl font-extrabold tracking-tight">Project Documentation & Links</h2>
296
+ </div>
297
+
298
+ <div className="grid grid-cols-1 sm:grid-cols-3 gap-6">
299
+ <motion.a
300
+ href="https://github.com/ritesh-1918/HELPDESK.AI"
301
+ target="_blank"
302
+ rel="noopener noreferrer"
303
+ whileHover={{ y: -4, borderColor: "rgba(16,185,129,0.3)" }}
304
+ className="bg-white/5 border border-white/10 p-6 rounded-2xl flex items-center justify-between group backdrop-blur-xl transition-all"
305
+ >
306
+ <div className="flex items-center gap-4">
307
+ <div className="p-3 bg-emerald-500/10 rounded-xl text-emerald-400 group-hover:bg-emerald-500/20 transition-all">
308
+ <Github size={22} />
309
  </div>
310
+ <div>
311
+ <h4 className="font-bold text-white">GitHub Repository</h4>
312
+ <p className="text-xs text-gray-500">Source code & issues</p>
 
 
 
 
 
 
 
 
313
  </div>
314
+ </div>
315
+ <ExternalLink size={16} className="text-gray-500 group-hover:text-emerald-400 transition-colors" />
316
+ </motion.a>
317
 
318
+ <motion.a
319
+ href="#"
320
+ whileHover={{ y: -4, borderColor: "rgba(16,185,129,0.3)" }}
321
+ className="bg-white/5 border border-white/10 p-6 rounded-2xl flex items-center justify-between group backdrop-blur-xl transition-all"
322
+ >
323
+ <div className="flex items-center gap-4">
324
+ <div className="p-3 bg-emerald-500/10 rounded-xl text-emerald-400 group-hover:bg-emerald-500/20 transition-all">
325
+ <BookOpen size={22} />
 
 
 
 
 
 
 
 
 
 
 
 
326
  </div>
327
+ <div>
328
+ <h4 className="font-bold text-white">API Reference</h4>
329
+ <p className="text-xs text-gray-500">Swagger & system docs</p>
330
+ </div>
331
+ </div>
332
+ <ExternalLink size={16} className="text-gray-500 group-hover:text-emerald-400 transition-colors" />
333
+ </motion.a>
334
+
335
+ <motion.a
336
+ href="#"
337
+ whileHover={{ y: -4, borderColor: "rgba(16,185,129,0.3)" }}
338
+ className="bg-white/5 border border-white/10 p-6 rounded-2xl flex items-center justify-between group backdrop-blur-xl transition-all"
339
+ >
340
+ <div className="flex items-center gap-4">
341
+ <div className="p-3 bg-emerald-500/10 rounded-xl text-emerald-400 group-hover:bg-emerald-500/20 transition-all">
342
+ <HelpCircle size={22} />
343
+ </div>
344
+ <div>
345
+ <h4 className="font-bold text-white">Platform Demo</h4>
346
+ <p className="text-xs text-gray-500">Video & Presentation</p>
347
+ </div>
348
+ </div>
349
+ <ExternalLink size={16} className="text-gray-500 group-hover:text-emerald-400 transition-colors" />
350
+ </motion.a>
351
+ </div>
352
+ </section>
353
+
354
+ {/* Community & GSSoC Section */}
355
+ <section>
356
+ <motion.div
357
+ initial="hidden"
358
+ whileInView="visible"
359
+ viewport={{ once: true }}
360
+ variants={fadeInUp}
361
+ className="bg-gradient-to-br from-emerald-950/80 to-emerald-900/50 border border-emerald-500/25 rounded-3xl p-8 md:p-12 text-center backdrop-blur-xl"
362
+ >
363
+ <div className="max-w-2xl mx-auto">
364
+ <Users size={40} className="text-emerald-400 mx-auto mb-6" />
365
+ <h2 className="text-2xl md:text-3xl font-extrabold text-white mb-4">Join our Community</h2>
366
+ <p className="text-gray-300 text-sm leading-relaxed mb-8">
367
+ HELPDESK.AI is proudly participating in GSSoC 2026. We are built by and for the open source community. Explore our issues, raise a pull request, and help us design the future of autonomous helpdesks!
368
+ </p>
369
+ <a
370
+ href="https://github.com/ritesh-1918/HELPDESK.AI"
371
+ target="_blank"
372
+ rel="noopener noreferrer"
373
+ className="inline-flex items-center gap-2 bg-emerald-500 hover:bg-emerald-600 text-black font-bold px-6 py-3.5 rounded-xl transition-all shadow-lg hover:shadow-emerald-500/20"
374
+ >
375
+ <Code size={18} />
376
+ Start Contributing
377
+ </a>
378
  </div>
379
+ </motion.div>
380
+ </section>
381
+
382
+ </div>
383
+ </div>
384
+ );
385
+ };
386
+
387
+ export default AboutUs;
Frontend/src/pages/AdminSignup.jsx CHANGED
@@ -10,6 +10,7 @@ import {
10
  } from "lucide-react";
11
  import useAuthStore from "../store/authStore";
12
  import { Select } from "../components/ui/select";
 
13
 
14
  /**
15
  * AdminSignup — Premium Multi-step Company Registration
@@ -44,6 +45,14 @@ function AdminSignup() {
44
 
45
  const navigate = useNavigate();
46
  const { signup, loading, user, profile } = useAuthStore();
 
 
 
 
 
 
 
 
47
 
48
  // Redirect if already logged in and verified
49
  useEffect(() => {
@@ -52,15 +61,6 @@ function AdminSignup() {
52
  }
53
  }, [user, profile, navigate]);
54
 
55
- // Password complexity validator — mirrors Supabase's policy
56
- const validatePassword = (pw) => {
57
- if (pw.length < 8) return 'Password must be at least 8 characters long.';
58
- if (!/[a-z]/.test(pw)) return 'Password must contain at least one lowercase letter (a-z).';
59
- if (!/[A-Z]/.test(pw)) return 'Password must contain at least one uppercase letter (A-Z).';
60
- if (!/[0-9]/.test(pw)) return 'Password must contain at least one number (0-9).';
61
- return null; // valid
62
- };
63
-
64
  // Password strength calculation
65
  useEffect(() => {
66
  const pw = formData.password;
@@ -69,7 +69,9 @@ function AdminSignup() {
69
  if (/[A-Z]/.test(pw)) strength += 25;
70
  if (/[0-9]/.test(pw)) strength += 25;
71
  if (/[^A-Za-z0-9]/.test(pw)) strength += 25;
 
72
  setPasswordStrength(strength);
 
73
  }, [formData.password]);
74
 
75
  const handleChange = (e) => {
@@ -87,9 +89,8 @@ function AdminSignup() {
87
  setError("Please fill in all required personal information.");
88
  return;
89
  }
90
- const pwError = validatePassword(formData.password);
91
- if (pwError) {
92
- setError(pwError);
93
  return;
94
  }
95
  if (formData.password !== formData.confirmPassword) {
@@ -428,15 +429,13 @@ function AdminSignup() {
428
  {showPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
429
  </button>
430
  </div>
431
- {/* Password Requirements */}
432
- <div className="mt-2 space-y-1">
433
- {formData.password && (
434
  <div className="flex justify-between items-center text-[10px] font-bold uppercase tracking-widest text-gray-400">
435
  <span>Strength: {getStrengthText()}</span>
436
  <span>{passwordStrength}%</span>
437
  </div>
438
- )}
439
- {formData.password && (
440
  <div className="h-1 w-full bg-gray-100 rounded-full overflow-hidden">
441
  <motion.div
442
  className={`h-full ${getStrengthColor()}`}
@@ -444,22 +443,14 @@ function AdminSignup() {
444
  animate={{ width: `${passwordStrength}%` }}
445
  />
446
  </div>
447
- )}
448
- <div className="grid grid-cols-2 gap-x-3 gap-y-0.5 mt-2">
449
- {[
450
- { label: '8+ characters', ok: formData.password.length >= 8 },
451
- { label: 'Uppercase (A-Z)', ok: /[A-Z]/.test(formData.password) },
452
- { label: 'Lowercase (a-z)', ok: /[a-z]/.test(formData.password) },
453
- { label: 'Number (0-9)', ok: /[0-9]/.test(formData.password) },
454
- ].map(({ label, ok }) => (
455
- <span key={label} className={`text-[10px] font-semibold flex items-center gap-1 transition-colors ${
456
- formData.password ? (ok ? 'text-emerald-600' : 'text-red-400') : 'text-gray-300'
457
- }`}>
458
- <span>{ok ? '✓' : '○'}</span> {label}
459
- </span>
460
- ))}
461
  </div>
462
- </div>
463
  </div>
464
  <div className="space-y-2">
465
  <label className="text-xs font-bold text-gray-400 uppercase tracking-wider flex items-center gap-2">
 
10
  } from "lucide-react";
11
  import useAuthStore from "../store/authStore";
12
  import { Select } from "../components/ui/select";
13
+ import { getPasswordValidation, getPasswordValidationMessage } from "../utils/passwordValidation";
14
 
15
  /**
16
  * AdminSignup — Premium Multi-step Company Registration
 
45
 
46
  const navigate = useNavigate();
47
  const { signup, loading, user, profile } = useAuthStore();
48
+ const passwordRules = {
49
+ minLength: 8,
50
+ requireUppercase: true,
51
+ requireNumber: true,
52
+ requireSpecial: true,
53
+ };
54
+ const passwordChecks = getPasswordValidation(formData.password, passwordRules);
55
+ const passwordWarning = getPasswordValidationMessage(passwordChecks, passwordRules);
56
 
57
  // Redirect if already logged in and verified
58
  useEffect(() => {
 
61
  }
62
  }, [user, profile, navigate]);
63
 
 
 
 
 
 
 
 
 
 
64
  // Password strength calculation
65
  useEffect(() => {
66
  const pw = formData.password;
 
69
  if (/[A-Z]/.test(pw)) strength += 25;
70
  if (/[0-9]/.test(pw)) strength += 25;
71
  if (/[^A-Za-z0-9]/.test(pw)) strength += 25;
72
+
73
  setPasswordStrength(strength);
74
+
75
  }, [formData.password]);
76
 
77
  const handleChange = (e) => {
 
89
  setError("Please fill in all required personal information.");
90
  return;
91
  }
92
+ if (passwordWarning) {
93
+ setError(passwordWarning);
 
94
  return;
95
  }
96
  if (formData.password !== formData.confirmPassword) {
 
429
  {showPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
430
  </button>
431
  </div>
432
+ {/* Strength Meter */}
433
+ {formData.password && (
434
+ <div className="mt-2 space-y-2">
435
  <div className="flex justify-between items-center text-[10px] font-bold uppercase tracking-widest text-gray-400">
436
  <span>Strength: {getStrengthText()}</span>
437
  <span>{passwordStrength}%</span>
438
  </div>
 
 
439
  <div className="h-1 w-full bg-gray-100 rounded-full overflow-hidden">
440
  <motion.div
441
  className={`h-full ${getStrengthColor()}`}
 
443
  animate={{ width: `${passwordStrength}%` }}
444
  />
445
  </div>
446
+ <div
447
+ aria-live="polite"
448
+ className={`text-[11px] font-semibold ${passwordWarning ? "text-red-600" : "text-emerald-700"}`}
449
+ >
450
+ {passwordWarning || "Password requirements met."}
451
+ </div>
 
 
 
 
 
 
 
 
452
  </div>
453
+ )}
454
  </div>
455
  <div className="space-y-2">
456
  <label className="text-xs font-bold text-gray-400 uppercase tracking-wider flex items-center gap-2">
Frontend/src/pages/Changelog.jsx CHANGED
@@ -1,116 +1,297 @@
1
- import React from 'react';
2
- import { useNavigate } from 'react-router-dom';
3
- import { Calendar, GitCommit, ArrowLeft, Heart, Zap, Sparkles } from 'lucide-react';
4
- import { Card } from '../components/ui/card';
5
-
6
- export default function Changelog() {
7
- const navigate = useNavigate();
8
-
9
- const changes = [
10
- {
11
- version: 'v1.2.0',
12
- date: 'May 2026',
13
- badge: 'Latest Release',
14
- highlight: 'Standalone Docs Portal & Dynamic Search',
15
- items: [
16
- 'Created complete standalone Docs Portal (/docs) outside authenticated routing grids.',
17
- 'Integrated interactive on-screen endpoint payload simulation and terminal output logs.',
18
- 'Resolved jsconfig compiler issues by deprecating outdated ignoreDeprecations keys.',
19
- 'Optimized responsive layout transitions across both mobile drawer and desktop headers.'
20
- ]
21
- },
22
- {
23
- version: 'v1.1.0',
24
- date: 'April 2026',
25
- badge: 'Update',
26
- highlight: 'Local Tesseract.js OCR and Siri Voice Recognition',
27
- items: [
28
- 'Added client-side webkitSpeechRecognition API dictation assistant with live amplitude waveforms.',
29
- 'Implemented offline image OCR with local Tesseract.js engine parsing image attachment telemetry.',
30
- 'Connected Supabase tables to log AI entities scanned directly from ticket descriptions.',
31
- 'Enacted dynamic SLA fallback computations based on category routing priorities.'
32
- ]
33
- },
34
- {
35
- version: 'v1.0.0',
36
- date: 'March 2026',
37
- badge: 'Major Release',
38
- highlight: 'Core AI Routing Engine Deployment',
39
- items: [
40
- 'Launched main multi-tenant portal with full dashboard telemetry, tracking, and company lobbies.',
41
- 'Connected Gemini API fallback interfaces to self-heal local database load degradation issues.',
42
- 'Created admin console enabling support agents to claim, claim-override, or resolve tickets live.'
43
- ]
44
- }
45
- ];
46
-
47
- return (
48
- <div className="min-h-screen bg-[#f6f8f7] pb-20">
49
- {/* Header */}
50
- <header className="w-full bg-white border-b border-gray-200 sticky top-0 z-50">
51
- <div className="max-w-[1100px] mx-auto px-4 md:px-6 h-16 flex items-center justify-between">
52
- <div className="flex items-center gap-3 cursor-pointer" onClick={() => navigate('/')}>
53
- <img src="/favicon.png" alt="HELPDESK.AI Logo" className="w-7 h-7 object-contain" />
54
- <div className="flex items-baseline gap-2">
55
- <h1 className="text-xl font-black tracking-tighter text-gray-900 italic">HELPDESK.AI</h1>
56
- <span className="px-2 py-0.5 text-[10px] font-black bg-indigo-100 text-indigo-800 rounded-md uppercase tracking-wider">Changelog</span>
57
- </div>
58
- </div>
59
- <button
60
- onClick={() => navigate('/')}
61
- className="flex items-center gap-2 text-xs font-bold text-gray-600 hover:text-emerald-600 transition-colors bg-gray-50 hover:bg-emerald-50 px-3.5 py-2 rounded-xl border border-gray-200"
62
- >
63
- <ArrowLeft size={14} /> Back to Home
64
- </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
  </div>
66
- </header>
67
 
68
- <div className="max-w-[800px] mx-auto px-4 md:px-6 mt-12 space-y-12">
69
- <div className="space-y-4">
70
- <div className="inline-flex items-center gap-2 px-3 py-1 bg-indigo-500/10 border border-indigo-500/20 rounded-full text-indigo-700 text-xs font-bold">
71
- <Sparkles size={14} /> System Changelog
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
  </div>
73
- <h1 className="text-4xl font-black text-slate-900 tracking-tight">Product Updates & Features</h1>
74
- <p className="text-slate-600 text-base leading-relaxed">
75
- Stay informed about system features, framework optimizations, and local AI training models introduced into HELPDESK.AI.
76
- </p>
77
- </div>
78
 
79
- {/* Timeline */}
80
- <div className="relative border-l border-slate-200 pl-6 ml-4 space-y-12">
81
- {changes.map((change, idx) => (
82
- <div key={change.version} className="relative">
83
- {/* Dot icon */}
84
- <span className="absolute -left-[35px] top-1.5 w-6 h-6 rounded-full bg-indigo-50 border border-indigo-300 flex items-center justify-center text-indigo-600">
85
- <GitCommit size={14} />
86
- </span>
87
-
88
- <div className="space-y-4">
89
- <div className="flex items-center gap-3 flex-wrap">
90
- <span className="text-xl font-extrabold text-slate-900">{change.version}</span>
91
- <span className="text-xs font-bold text-slate-400 flex items-center gap-1">
92
- <Calendar size={12} /> {change.date}
93
- </span>
94
- <span className="px-2 py-0.5 rounded bg-emerald-50 border border-emerald-100 text-[10px] font-black text-emerald-800 uppercase tracking-widest">
95
- {change.badge}
96
- </span>
97
- </div>
98
-
99
- <Card className="p-6 rounded-[2rem] border border-slate-100 shadow-sm bg-white space-y-4">
100
- <h4 className="font-extrabold text-slate-800 text-sm flex items-center gap-2">
101
- <Zap size={14} className="text-indigo-500" /> {change.highlight}
102
- </h4>
103
- <ul className="list-disc pl-5 text-xs text-slate-600 space-y-2 leading-relaxed">
104
- {change.items.map((item, itemIdx) => (
105
- <li key={itemIdx}>{item}</li>
106
- ))}
107
- </ul>
108
- </Card>
109
- </div>
110
- </div>
111
- ))}
112
- </div>
113
- </div>
 
 
 
 
 
 
 
 
 
114
  </div>
115
- );
116
- }
 
 
 
 
 
1
+ import React from "react";
2
+ import {
3
+ Rocket,
4
+ Sparkles,
5
+ Bug,
6
+ ShieldCheck,
7
+ CalendarDays,
8
+ GitCommitHorizontal,
9
+ } from "lucide-react";
10
+
11
+ import { motion } from "framer-motion";
12
+
13
+ const releases = [
14
+ {
15
+ version: "v2.1.0",
16
+ date: "May 20, 2026",
17
+ type: "Major Release",
18
+ status: "Live",
19
+ features: [
20
+ "Introduced AI-powered smart ticket routing engine",
21
+ "Added enterprise analytics dashboard with live metrics",
22
+ "Integrated multilingual support for global organizations",
23
+ ],
24
+ enhancements: [
25
+ "Improved dashboard loading performance by 35%",
26
+ "Enhanced accessibility contrast and readability",
27
+ "Optimized backend request caching for faster responses",
28
+ ],
29
+ fixes: [
30
+ "Fixed session timeout issue after inactivity",
31
+ "Resolved notification synchronization bug",
32
+ "Fixed mobile sidebar overlap on smaller devices",
33
+ ],
34
+ },
35
+
36
+ {
37
+ version: "v2.0.2",
38
+ date: "May 10, 2026",
39
+ type: "Patch Update",
40
+ status: "Stable",
41
+ features: [
42
+ "Added bulk export functionality for support tickets",
43
+ "Introduced advanced ticket filtering system",
44
+ ],
45
+ enhancements: [
46
+ "Improved authentication flow security",
47
+ "Optimized API request handling",
48
+ ],
49
+ fixes: [
50
+ "Resolved login redirect issue",
51
+ "Fixed navbar responsiveness on tablets",
52
+ ],
53
+ },
54
+
55
+ {
56
+ version: "v2.0.0",
57
+ date: "April 28, 2026",
58
+ type: "Platform Launch",
59
+ status: "Released",
60
+ features: [
61
+ "Official launch of HelpDesk.AI platform",
62
+ "AI-based issue auto-categorization system",
63
+ "Role-based admin management implementation",
64
+ ],
65
+ enhancements: [
66
+ "Modernized UI architecture using Tailwind CSS",
67
+ "Improved dark mode consistency",
68
+ ],
69
+ fixes: [
70
+ "Initial stability improvements",
71
+ "Security hardening patches",
72
+ ],
73
+ },
74
+ ];
75
+
76
+ const FadeIn = {
77
+ hidden: {
78
+ opacity: 0,
79
+ y: 40,
80
+ },
81
+ visible: {
82
+ opacity: 1,
83
+ y: 0,
84
+ },
85
+ };
86
+
87
+ const Section = ({
88
+ title,
89
+ icon,
90
+ items,
91
+ glow,
92
+ }) => {
93
+ return (
94
+ <div className="mb-8">
95
+
96
+ <div className="flex items-center gap-3 mb-4">
97
+ <div
98
+ className={`p-2 rounded-xl ${glow}`}
99
+ >
100
+ {icon}
101
+ </div>
102
+
103
+ <h3 className="text-2xl font-semibold text-white">
104
+ {title}
105
+ </h3>
106
+ </div>
107
+
108
+ <div className="space-y-3">
109
+ {items.map((item, index) => (
110
+ <motion.div
111
+ key={index}
112
+ whileHover={{ x: 6 }}
113
+ className="flex gap-3 text-gray-300 leading-relaxed"
114
+ >
115
+ <span className="text-green-400 mt-1">
116
+
117
+ </span>
118
+
119
+ <p>{item}</p>
120
+ </motion.div>
121
+ ))}
122
+ </div>
123
+ </div>
124
+ );
125
+ };
126
+
127
+ const Changelog = () => {
128
+ return (
129
+ <div className="min-h-screen bg-[#021510] overflow-hidden text-white relative">
130
+
131
+ {/* Background Glow Effects */}
132
+ <div className="absolute top-0 left-0 w-[400px] h-[400px] bg-green-500/10 blur-[120px]" />
133
+
134
+ <div className="absolute bottom-0 right-0 w-[400px] h-[400px] bg-emerald-400/10 blur-[120px]" />
135
+
136
+ {/* Hero Section */}
137
+ <section className="relative z-10 px-6 pt-24 pb-16">
138
+
139
+ <motion.div
140
+ initial="hidden"
141
+ animate="visible"
142
+ variants={FadeIn}
143
+ transition={{ duration: 0.7 }}
144
+ className="max-w-6xl mx-auto text-center"
145
+ >
146
+
147
+ {/* Badge */}
148
+ <div className="inline-flex items-center gap-2 bg-green-500/10 border border-green-500/20 px-5 py-2 rounded-full text-green-400 mb-8 backdrop-blur-md">
149
+
150
+ <GitCommitHorizontal size={16} />
151
+
152
+ <span className="text-sm font-medium tracking-wide">
153
+ PRODUCT RELEASE NOTES
154
+ </span>
155
+ </div>
156
+
157
+ {/* Heading */}
158
+ <h1 className="text-5xl md:text-7xl font-bold mb-6 tracking-tight">
159
+
160
+ Platform
161
+ <span className="text-green-400">
162
+ {" "}Changelog
163
+ </span>
164
+ </h1>
165
+
166
+ {/* Description */}
167
+ <p className="text-gray-400 text-lg md:text-xl max-w-3xl mx-auto leading-relaxed">
168
+
169
+ Stay updated with the latest platform improvements,
170
+ AI enhancements, security patches, and feature
171
+ releases across HelpDesk.AI.
172
+ </p>
173
+
174
+ </motion.div>
175
+ </section>
176
+
177
+ {/* Timeline */}
178
+ <section className="relative z-10 px-6 pb-24">
179
+
180
+ <div className="max-w-5xl mx-auto relative">
181
+
182
+ {/* Vertical Line */}
183
+ <div className="absolute left-4 top-0 w-[2px] h-full bg-gradient-to-b from-green-500/70 via-green-500/20 to-transparent" />
184
+
185
+ {/* Release Cards */}
186
+ <div className="space-y-16">
187
+
188
+ {releases.map((release, index) => (
189
+ <motion.div
190
+ key={index}
191
+ initial="hidden"
192
+ whileInView="visible"
193
+ viewport={{ once: true }}
194
+ variants={FadeIn}
195
+ transition={{
196
+ duration: 0.6,
197
+ delay: index * 0.1,
198
+ }}
199
+ className="relative pl-14"
200
+ >
201
+
202
+ {/* Timeline Dot */}
203
+ <div className="absolute left-0 top-8">
204
+
205
+ <div className="w-8 h-8 rounded-full bg-green-500 border-4 border-[#021510] shadow-[0_0_25px_rgba(34,197,94,0.8)]" />
206
+
207
  </div>
 
208
 
209
+ {/* Card */}
210
+ <motion.div
211
+ whileHover={{
212
+ scale: 1.01,
213
+ }}
214
+ transition={{
215
+ duration: 0.3,
216
+ }}
217
+ className="relative bg-white/5 border border-white/10 backdrop-blur-xl rounded-3xl p-8 md:p-10 shadow-2xl overflow-hidden"
218
+ >
219
+
220
+ {/* Gradient Overlay */}
221
+ <div className="absolute inset-0 bg-gradient-to-br from-green-500/5 to-transparent pointer-events-none" />
222
+
223
+ {/* Header */}
224
+ <div className="relative flex flex-wrap items-center justify-between gap-4 mb-10">
225
+
226
+ <div className="flex items-center gap-4 flex-wrap">
227
+
228
+ {/* Version */}
229
+ <div className="bg-green-500 text-black font-bold px-5 py-2 rounded-full text-sm shadow-lg">
230
+ {release.version}
231
+ </div>
232
+
233
+ {/* Type */}
234
+ <div className="bg-white/10 border border-white/10 text-gray-300 px-4 py-2 rounded-full text-sm">
235
+ {release.type}
236
+ </div>
237
+
238
+ {/* Status */}
239
+ <div className="flex items-center gap-2 text-green-400 text-sm">
240
+
241
+ <ShieldCheck size={16} />
242
+
243
+ {release.status}
244
+ </div>
245
  </div>
 
 
 
 
 
246
 
247
+ {/* Date */}
248
+ <div className="flex items-center gap-2 text-gray-400">
249
+
250
+ <CalendarDays size={18} />
251
+
252
+ <span>{release.date}</span>
253
+ </div>
254
+ </div>
255
+
256
+ {/* Sections */}
257
+ <div className="relative">
258
+
259
+ <Section
260
+ title="New Features"
261
+ glow="bg-green-500/20"
262
+ icon={
263
+ <Rocket className="text-green-400" />
264
+ }
265
+ items={release.features}
266
+ />
267
+
268
+ <Section
269
+ title="Enhancements"
270
+ glow="bg-blue-500/20"
271
+ icon={
272
+ <Sparkles className="text-blue-400" />
273
+ }
274
+ items={release.enhancements}
275
+ />
276
+
277
+ <Section
278
+ title="Bug Fixes"
279
+ glow="bg-red-500/20"
280
+ icon={
281
+ <Bug className="text-red-400" />
282
+ }
283
+ items={release.fixes}
284
+ />
285
+
286
+ </div>
287
+ </motion.div>
288
+ </motion.div>
289
+ ))}
290
+ </div>
291
  </div>
292
+ </section>
293
+ </div>
294
+ );
295
+ };
296
+
297
+ export default Changelog;
Frontend/src/pages/LandingPage.jsx CHANGED
@@ -1,9 +1,9 @@
1
- import React, { useRef, useEffect, useState } from 'react';
2
 
3
  import { motion, AnimatePresence } from 'framer-motion';
4
  import { useNavigate } from 'react-router-dom';
5
  import {
6
- Menu, X, Check, Activity,
7
  MapPin, AlertCircle, Folder, Zap, Bot, ArrowRight,
8
  Clock, CheckCircle,
9
  Star, Twitter, Linkedin, Github, Globe, MessageSquare,
@@ -14,6 +14,12 @@ import {
14
  import useAuthStore from '../store/authStore';
15
  import TeamSection from '../components/landing/TeamSection';
16
 
 
 
 
 
 
 
17
  // ---- Count-up animation component ----
18
  function AnimatedStat({ target, suffix = '', prefix = '', label, isWord = false }) {
19
  const [display, setDisplay] = useState(isWord ? target : '0');
@@ -124,9 +130,9 @@ function DemoModal({ onClose }) {
124
  <div className="flex gap-3 w-full md:w-auto">
125
  <button
126
  onClick={() => { onClose(); window.location.href = '/admin-signup'; }}
127
- className="flex-1 md:px-8 bg-emerald-600 hover:bg-emerald-500 text-white py-3 rounded-xl font-black italic uppercase tracking-wider transition-all flex items-center justify-center gap-2 shadow-lg shadow-emerald-500/20"
128
  >
129
- Start Free <ArrowRight className="w-4 h-4 ml-1" />
130
  </button>
131
  </div>
132
  </div>
@@ -145,7 +151,7 @@ export default function LandingPage() {
145
  const [activeStep, setActiveStep] = useState(0);
146
  const [isRedirecting, setIsRedirecting] = useState(false);
147
 
148
- const steps = [
149
  {
150
  num: '01',
151
  title: 'Messy User Input',
@@ -208,7 +214,7 @@ export default function LandingPage() {
208
  </div>
209
  )
210
  }
211
- ];
212
 
213
  useEffect(() => {
214
  if (!loading && user && profile) {
@@ -225,7 +231,7 @@ export default function LandingPage() {
225
  period: '/mo',
226
  desc: 'Perfect for small teams exploring AI helpdesk.',
227
  cta: 'Get Started Free',
228
- ctaStyle: 'border border-gray-200 text-gray-700 hover:border-emerald-900 hover:text-emerald-800',
229
  features: ['Up to 50 tickets/mo', 'Basic AI Categorization', 'Email Support', '1 Team Member', 'Public API Access'],
230
  popular: false,
231
  },
@@ -235,7 +241,7 @@ export default function LandingPage() {
235
  period: '/mo',
236
  desc: 'For growing IT teams needing full automation.',
237
  cta: 'Start Free Trial',
238
- ctaStyle: 'bg-emerald-900 text-white hover:bg-emerald-800 shadow-lg shadow-emerald-900/20',
239
  features: ['Up to 500 tickets/mo', 'Advanced AI Parsing', 'Priority Detection Engine', 'Duplicate Detection', '5 Team Members', 'Priority Email Support'],
240
  popular: true,
241
  },
@@ -245,7 +251,7 @@ export default function LandingPage() {
245
  period: '',
246
  desc: 'For large organizations with complex IT landscapes.',
247
  cta: 'Contact Sales',
248
- ctaStyle: 'border border-gray-200 text-gray-700 hover:border-emerald-900 hover:text-emerald-800',
249
  features: ['Unlimited tickets', 'Custom AI Fine-Tuning', 'SSO & Audit Logs', 'Dedicated SLA Manager', 'Unlimited Members', 'VAPT & Compliance Reports'],
250
  popular: false,
251
  },
@@ -273,79 +279,163 @@ export default function LandingPage() {
273
  };
274
 
275
  return (
276
- <div className="min-h-screen bg-white font-sans text-slate-800">
277
  {showDemo && <DemoModal onClose={() => setShowDemo(false)} />}
278
 
279
  {/* ==================== NAV ==================== */}
280
- <nav className="sticky top-0 z-50 bg-white/90 backdrop-blur-md border-b border-gray-100 shadow-sm">
281
- <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
282
- <div className="flex justify-between items-center h-16">
283
- {/* Logo */}
284
- <div className="flex items-center gap-2 cursor-pointer" onClick={() => navigate('/')}>
285
- <img src="/favicon.png" alt="H" className="w-8 h-8 object-contain" />
286
- <span className="font-black text-2xl tracking-tighter text-emerald-900 italic uppercase">HelpDesk.ai</span>
287
- </div>
288
-
289
- {/* Desktop Links */}
290
- <div className="hidden md:flex items-center gap-8">
291
- <a href="#features" className="text-sm font-semibold text-gray-600 hover:text-emerald-800 transition-colors">Features</a>
292
- <a href="#how-it-works" className="text-sm font-semibold text-gray-600 hover:text-emerald-800 transition-colors">How It Works</a>
293
- <a href="#pricing" className="text-sm font-semibold text-gray-600 hover:text-emerald-800 transition-colors">Pricing</a>
294
- </div>
295
-
296
- {/* CTA Buttons */}
297
- <div className="hidden md:flex items-center gap-3">
298
- <button
299
- onClick={() => navigate('/login')}
300
- className="text-sm font-semibold text-gray-700 hover:text-emerald-800 transition-colors px-4 py-2 rounded-lg hover:bg-gray-50"
301
- >
302
- Sign In
303
- </button>
304
- <button
305
- onClick={() => setShowDemo(true)}
306
- className="text-sm font-semibold text-emerald-800 border border-emerald-200 px-4 py-2 rounded-lg hover:bg-emerald-50 transition-all flex items-center gap-1.5"
307
- >
308
- <Play className="w-3.5 h-3.5 fill-emerald-700" /> Watch Demo
309
- </button>
310
- <button
311
- onClick={() => navigate('/admin-signup')}
312
- className="bg-emerald-900 hover:bg-emerald-800 text-white px-5 py-2.5 rounded-lg text-sm font-semibold transition-all shadow-lg shadow-emerald-900/20"
313
- >
314
- Get Started Free
315
- </button>
316
- </div>
317
-
318
- {/* Mobile Menu Button */}
319
- <div className="md:hidden">
320
- <button onClick={() => setIsMenuOpen(!isMenuOpen)} className="text-gray-600 hover:text-emerald-800 p-2">
321
- {isMenuOpen ? <X className="w-6 h-6" /> : <Menu className="w-6 h-6" />}
322
- </button>
323
- </div>
324
- </div>
325
- </div>
326
-
327
- {/* Mobile Menu */}
328
- {isMenuOpen && (
329
- <div className="md:hidden bg-white border-t border-gray-100 absolute w-full shadow-xl z-50">
330
- <div className="px-5 pt-3 pb-6 space-y-4">
331
- <a href="#features" onClick={() => setIsMenuOpen(false)} className="block text-base font-semibold text-gray-700 hover:text-emerald-800 py-2">Features</a>
332
- <a href="#how-it-works" onClick={() => setIsMenuOpen(false)} className="block text-base font-semibold text-gray-700 hover:text-emerald-800 py-2">How It Works</a>
333
- <a href="#pricing" onClick={() => setIsMenuOpen(false)} className="block text-base font-semibold text-gray-700 hover:text-emerald-800 py-2">Pricing</a>
334
- <div className="pt-4 flex flex-col gap-3 border-t border-gray-100">
335
- <button onClick={() => { setIsMenuOpen(false); setShowDemo(true); }} className="w-full text-center py-2.5 text-emerald-800 font-semibold border border-emerald-200 rounded-lg flex items-center justify-center gap-2">
336
- <Play className="w-4 h-4 fill-emerald-700" /> Watch Demo
337
- </button>
338
- <button onClick={() => navigate('/login')} className="w-full text-center py-2.5 text-gray-700 font-semibold border border-gray-100 rounded-lg">
339
- Sign In
340
- </button>
341
- <button onClick={() => navigate('/admin-signup')} className="w-full bg-emerald-900 text-white py-3 rounded-lg font-semibold shadow">
342
- Get Started Free
343
- </button>
344
- </div>
345
- </div>
346
- </div>
347
- )}
348
- </nav>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
349
 
350
  {/* ==================== HERO ==================== */}
351
  <section className="relative pt-12 md:pt-20 pb-20 md:pb-32 overflow-hidden">
@@ -369,15 +459,15 @@ export default function LandingPage() {
369
  <div className="flex flex-col sm:flex-row items-center justify-center gap-4 mb-6">
370
  <button
371
  onClick={() => navigate('/admin-signup')}
372
- className="w-full sm:w-auto px-8 py-4 bg-emerald-900 text-white rounded-xl font-bold shadow-xl shadow-emerald-900/25 hover:bg-emerald-800 hover:scale-[1.02] active:scale-[0.98] transition-all flex items-center justify-center gap-2 text-base"
373
  >
374
- Get Started Free <ArrowRight className="w-5 h-5" />
375
  </button>
376
  <button
377
  onClick={() => setShowDemo(true)}
378
- className="w-full sm:w-auto px-8 py-4 bg-white text-gray-700 border border-gray-200 rounded-xl font-semibold hover:border-emerald-500 hover:text-emerald-700 transition-all flex items-center justify-center gap-2 text-base"
379
  >
380
- <Play className="w-4 h-4 fill-gray-500" /> Watch a Demo
381
  </button>
382
  </div>
383
 
@@ -414,7 +504,7 @@ export default function LandingPage() {
414
  <div className="mb-4">
415
  <h3 className="text-sm font-bold text-gray-800 mb-1">Subject: Wifi down again in Lab 3??</h3>
416
  <p className="text-sm text-gray-600 leading-relaxed">
417
- Hey support, the wifi in <span className="bg-yellow-100 px-1 rounded">downstairs lab 3</span> is acting up again.
418
  Can't connect at all. Class starts in 20 mins, need this fixed ASAP!<br /><br />
419
  Thanks,<br />Sarah
420
  </p>
@@ -645,7 +735,7 @@ export default function LandingPage() {
645
  <div className="inline-flex items-center gap-2 px-3 py-1 bg-emerald-500/10 text-emerald-400 rounded-full text-[10px] md:text-xs font-bold uppercase tracking-widest border border-emerald-500/20 mb-4 md:mb-6">
646
  The Journey
647
  </div>
648
- <h2 className="text-3xl sm:text-4xl md:text-6xl font-black text-white tracking-tight leading-[0.9] mb-8 md:mb-12 italic uppercase">
649
  From Chaos <br />
650
  to <span className="text-emerald-500">Clarity.</span>
651
  </h2>
@@ -656,28 +746,25 @@ export default function LandingPage() {
656
  key={idx}
657
  onMouseEnter={() => setActiveStep(idx)}
658
  onClick={() => setActiveStep(idx)}
659
- className={`group cursor-pointer p-6 rounded-3xl transition-all duration-500 border ${activeStep === idx
660
  ? 'bg-white/10 border-white/20 shadow-2xl shadow-black/20'
661
  : 'bg-transparent border-transparent hover:bg-white/5 opacity-40 hover:opacity-100'
662
  }`}
663
  >
664
  <div className="flex items-start gap-4 md:gap-6">
665
- <div className={`shrink-0 w-10 md:w-12 h-10 md:h-12 rounded-xl md:rounded-2xl flex items-center justify-center font-black text-lg md:text-xl italic transition-all duration-500 ${activeStep === idx ? 'bg-emerald-500 text-white shadow-lg shadow-emerald-500/20 rotate-12 scale-110' : 'bg-white/10 text-white/40'}`}>
666
  {step.num}
667
  </div>
668
  <div>
669
- <h3 className={`text-xl font-black italic uppercase transition-colors duration-500 ${activeStep === idx ? 'text-white' : 'text-white/60'}`}>
670
  {step.title}
671
  </h3>
672
- {activeStep === idx && (
673
- <motion.p
674
- initial={{ opacity: 0, height: 0 }}
675
- animate={{ opacity: 1, height: 'auto' }}
676
- className="text-white/60 text-sm mt-3 leading-relaxed max-w-sm"
677
- >
678
- {step.desc}
679
- </motion.p>
680
- )}
681
  </div>
682
  </div>
683
  </div>
@@ -688,14 +775,14 @@ export default function LandingPage() {
688
  {/* Right: Visual Display */}
689
  <div className="w-full md:w-1/2 h-[350px] md:h-[500px] relative">
690
  <div className="absolute inset-0 bg-gradient-to-br from-emerald-500/10 to-transparent rounded-[32px] md:rounded-[40px] border border-white/5 backdrop-blur-3xl overflow-hidden p-6 md:p-12 flex items-center justify-center">
691
- <AnimatePresence mode="wait">
692
  <motion.div
693
  key={activeStep}
694
- initial={{ opacity: 0, scale: 0.9, y: 20 }}
695
  animate={{ opacity: 1, scale: 1, y: 0 }}
696
- exit={{ opacity: 0, scale: 1.1, y: -20 }}
697
- transition={{ type: 'spring', damping: 20, stiffness: 100 }}
698
- className="w-full h-full flex flex-col items-center justify-center"
699
  >
700
  <div className="mb-6 inline-flex items-center gap-2 px-4 py-1.5 bg-white/5 text-white/50 rounded-full text-[10px] font-black uppercase tracking-[0.2em]">
701
  <div className={`w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse`} />
@@ -759,7 +846,7 @@ export default function LandingPage() {
759
  <button
760
  onClick={() => handlePricingClick(name)}
761
  disabled={isRedirecting && name === 'Growth'}
762
- className={`w-full flex items-center justify-center gap-2 py-3 rounded-xl font-semibold transition-all mb-8 text-sm ${ctaStyle} ${isRedirecting && name === 'Growth' ? 'opacity-80 cursor-not-allowed' : ''}`}
763
  >
764
  {isRedirecting && name === 'Growth' ? (
765
  <>
@@ -802,15 +889,15 @@ export default function LandingPage() {
802
  <div className="flex flex-col sm:flex-row items-center justify-center gap-4">
803
  <button
804
  onClick={() => navigate('/admin-signup')}
805
- className="w-full sm:w-auto px-8 py-4 bg-white text-emerald-900 font-bold rounded-xl hover:bg-green-50 transition-all shadow-xl"
806
  >
807
  Get Started Free
808
  </button>
809
  <button
810
  onClick={() => setShowDemo(true)}
811
- className="w-full sm:w-auto px-8 py-4 border border-white/30 text-white font-semibold rounded-xl hover:bg-white/10 transition-all flex items-center justify-center gap-2"
812
  >
813
- <Play className="w-4 h-4 fill-white" /> Watch Demo
814
  </button>
815
  </div>
816
  <div className="mt-8">
@@ -862,17 +949,17 @@ export default function LandingPage() {
862
  {
863
  heading: 'Resources',
864
  links: [
865
- { label: 'Documentation', href: '/docs' },
866
- { label: 'API Reference', href: '/api-reference' },
867
  { label: 'Changelog', href: '/changelog' },
868
- { label: 'Status Page', href: '/status' },
869
  ]
870
  },
871
  {
872
  heading: 'Company',
873
  links: [
874
- { label: 'About Us', href: '/about' },
875
- { label: 'Careers', href: '/careers' },
876
  { label: 'Privacy Policy', href: '/privacy' },
877
  { label: 'Terms of Service', href: '/terms' },
878
  ]
@@ -883,7 +970,7 @@ export default function LandingPage() {
883
  { label: 'Security Overview', href: '/security' },
884
  { label: 'Privacy Policy', href: '/privacy' },
885
  { label: 'Terms of Service', href: '/terms' },
886
- { label: 'Cookie Policy', href: '/cookie-policy' },
887
  ]
888
  },
889
  ].map(({ heading, links }) => (
 
1
+ import React, { useRef, useEffect, useState, useMemo } from 'react';
2
 
3
  import { motion, AnimatePresence } from 'framer-motion';
4
  import { useNavigate } from 'react-router-dom';
5
  import {
6
+ Menu, X, Check, Activity, Moon, Sun,
7
  MapPin, AlertCircle, Folder, Zap, Bot, ArrowRight,
8
  Clock, CheckCircle,
9
  Star, Twitter, Linkedin, Github, Globe, MessageSquare,
 
14
  import useAuthStore from '../store/authStore';
15
  import TeamSection from '../components/landing/TeamSection';
16
 
17
+ const CTA_TRANSITION = 'transition-all duration-200 ease-out transform-gpu will-change-transform';
18
+ const CTA_SCALE = 'hover:scale-105 active:scale-[0.98]';
19
+ const CTA_PRIMARY_GLOW = 'shadow-lg shadow-emerald-900/20 hover:shadow-emerald-900/35';
20
+ const CTA_ICON_SHIFT = 'transition-transform duration-200 group-hover:translate-x-1';
21
+ const CTA_PLAY_SHIFT = 'transition-transform duration-200 group-hover:translate-x-0.5';
22
+
23
  // ---- Count-up animation component ----
24
  function AnimatedStat({ target, suffix = '', prefix = '', label, isWord = false }) {
25
  const [display, setDisplay] = useState(isWord ? target : '0');
 
130
  <div className="flex gap-3 w-full md:w-auto">
131
  <button
132
  onClick={() => { onClose(); window.location.href = '/admin-signup'; }}
133
+ className={`group flex-1 md:px-8 bg-emerald-600 hover:bg-emerald-500 text-white py-3 rounded-xl font-black italic uppercase tracking-wider ${CTA_TRANSITION} ${CTA_SCALE} ${CTA_PRIMARY_GLOW} flex items-center justify-center gap-2`}
134
  >
135
+ Start Free <ArrowRight className={`w-4 h-4 ml-1 ${CTA_ICON_SHIFT}`} />
136
  </button>
137
  </div>
138
  </div>
 
151
  const [activeStep, setActiveStep] = useState(0);
152
  const [isRedirecting, setIsRedirecting] = useState(false);
153
 
154
+ const steps = useMemo(() => [
155
  {
156
  num: '01',
157
  title: 'Messy User Input',
 
214
  </div>
215
  )
216
  }
217
+ ], []);
218
 
219
  useEffect(() => {
220
  if (!loading && user && profile) {
 
231
  period: '/mo',
232
  desc: 'Perfect for small teams exploring AI helpdesk.',
233
  cta: 'Get Started Free',
234
+ ctaStyle: `border border-gray-200 text-gray-700 hover:border-emerald-900 hover:text-emerald-800 group ${CTA_TRANSITION} ${CTA_SCALE}`,
235
  features: ['Up to 50 tickets/mo', 'Basic AI Categorization', 'Email Support', '1 Team Member', 'Public API Access'],
236
  popular: false,
237
  },
 
241
  period: '/mo',
242
  desc: 'For growing IT teams needing full automation.',
243
  cta: 'Start Free Trial',
244
+ ctaStyle: `bg-emerald-900 text-white hover:bg-emerald-800 group ${CTA_TRANSITION} ${CTA_SCALE} ${CTA_PRIMARY_GLOW}`,
245
  features: ['Up to 500 tickets/mo', 'Advanced AI Parsing', 'Priority Detection Engine', 'Duplicate Detection', '5 Team Members', 'Priority Email Support'],
246
  popular: true,
247
  },
 
251
  period: '',
252
  desc: 'For large organizations with complex IT landscapes.',
253
  cta: 'Contact Sales',
254
+ ctaStyle: `border border-gray-200 text-gray-700 hover:border-emerald-900 hover:text-emerald-800 group ${CTA_TRANSITION} ${CTA_SCALE}`,
255
  features: ['Unlimited tickets', 'Custom AI Fine-Tuning', 'SSO & Audit Logs', 'Dedicated SLA Manager', 'Unlimited Members', 'VAPT & Compliance Reports'],
256
  popular: false,
257
  },
 
279
  };
280
 
281
  return (
282
+ <div className="min-h-screen bg-white dark:bg-gray-900 font-sans text-slate-800 dark:text-white">
283
  {showDemo && <DemoModal onClose={() => setShowDemo(false)} />}
284
 
285
  {/* ==================== NAV ==================== */}
286
+ <nav className="sticky top-0 z-50 bg-white/90 backdrop-blur-md border-b border-gray-100 shadow-sm">
287
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
288
+ <div className="flex items-center justify-between h-20">
289
+
290
+ {/* Logo */}
291
+ <div
292
+ className="flex items-center gap-3 cursor-pointer"
293
+ onClick={() => navigate('/')}
294
+ >
295
+ <img src="/favicon.png" alt="logo" className="w-10 h-10" />
296
+ <span className="font-black italic text-emerald-900 text-2xl">
297
+ HELPDESK.AI
298
+ </span>
299
+ </div>
300
+
301
+ {/* Desktop Nav */}
302
+ <div className="hidden lg:flex items-center gap-10">
303
+
304
+ <button
305
+ className="p-2 rounded-lg hover:bg-gray-100 transition"
306
+ >
307
+ <Moon className="w-6 h-6 text-gray-600" />
308
+ </button>
309
+
310
+ <a
311
+ href="#features"
312
+ className="font-semibold text-gray-600 hover:text-emerald-700 transition"
313
+ >
314
+ Features
315
+ </a>
316
+
317
+ <a
318
+ href="#how-it-works"
319
+ className="font-semibold text-gray-600 hover:text-emerald-700 transition"
320
+ >
321
+ How It Works
322
+ </a>
323
+
324
+ <a
325
+ href="#pricing"
326
+ className="font-semibold text-gray-600 hover:text-emerald-700 transition"
327
+ >
328
+ Pricing
329
+ </a>
330
+ </div>
331
+
332
+ {/* Desktop Actions */}
333
+ <div className="hidden lg:flex items-center gap-4">
334
+
335
+ <button
336
+ onClick={() => navigate('/login')}
337
+ className="font-semibold text-gray-700 hover:text-emerald-700"
338
+ >
339
+ Sign In
340
+ </button>
341
+
342
+ <button
343
+ onClick={() => setShowDemo(true)}
344
+ className="flex items-center gap-2 px-6 py-3 border border-emerald-200 rounded-xl text-emerald-700 font-semibold hover:bg-emerald-50 transition"
345
+ >
346
+ <Play size={16} fill="currentColor" />
347
+ Watch Demo
348
+ </button>
349
+
350
+ <button
351
+ onClick={() => navigate('/admin-signup')}
352
+ className="px-6 py-3 bg-emerald-900 hover:bg-emerald-800 text-white rounded-xl font-bold shadow-lg"
353
+ >
354
+ Get Started Free
355
+ </button>
356
+
357
+ </div>
358
+
359
+ {/* Mobile Buttons */}
360
+ <div className="flex lg:hidden items-center gap-2">
361
+
362
+ <button className="p-2">
363
+ <Moon className="w-5 h-5 text-gray-600" />
364
+ </button>
365
+
366
+ <button
367
+ onClick={() => setIsMenuOpen(!isMenuOpen)}
368
+ className="p-2 border border-gray-200 rounded-lg"
369
+ >
370
+ {isMenuOpen ? (
371
+ <X className="w-6 h-6" />
372
+ ) : (
373
+ <Menu className="w-6 h-6" />
374
+ )}
375
+ </button>
376
+
377
+ </div>
378
+
379
+ </div>
380
+ </div>
381
+
382
+ {/* Mobile Menu */}
383
+ {isMenuOpen && (
384
+ <div className="lg:hidden absolute top-full left-0 w-full border-t border-gray-100 bg-white shadow-xl dark:bg-gray-900 dark:border-gray-800">
385
+ <div className="flex flex-col px-4 py-5 gap-4">
386
+
387
+ <a
388
+ href="#features"
389
+ onClick={() => setIsMenuOpen(false)}
390
+ className="font-medium text-gray-700"
391
+ >
392
+ Features
393
+ </a>
394
+
395
+ <a
396
+ href="#how-it-works"
397
+ onClick={() => setIsMenuOpen(false)}
398
+ className="font-medium text-gray-700"
399
+ >
400
+ How It Works
401
+ </a>
402
+
403
+ <a
404
+ href="#pricing"
405
+ onClick={() => setIsMenuOpen(false)}
406
+ className="font-medium text-gray-700"
407
+ >
408
+ Pricing
409
+ </a>
410
+
411
+ <hr />
412
+
413
+ <button
414
+ onClick={() => navigate('/login')}
415
+ className="text-left font-medium"
416
+ >
417
+ Sign In
418
+ </button>
419
+
420
+ <button
421
+ onClick={() => setShowDemo(true)}
422
+ className="flex items-center gap-2 font-medium"
423
+ >
424
+ <Play size={16} />
425
+ Watch Demo
426
+ </button>
427
+
428
+ <button
429
+ onClick={() => navigate('/admin-signup')}
430
+ className="bg-emerald-900 text-white py-3 rounded-xl font-semibold"
431
+ >
432
+ Get Started Free
433
+ </button>
434
+
435
+ </div>
436
+ </div>
437
+ )}
438
+ </nav>
439
 
440
  {/* ==================== HERO ==================== */}
441
  <section className="relative pt-12 md:pt-20 pb-20 md:pb-32 overflow-hidden">
 
459
  <div className="flex flex-col sm:flex-row items-center justify-center gap-4 mb-6">
460
  <button
461
  onClick={() => navigate('/admin-signup')}
462
+ className={`group w-full sm:w-auto px-8 py-4 bg-emerald-900 text-white rounded-xl font-bold ${CTA_PRIMARY_GLOW} hover:bg-emerald-800 ${CTA_TRANSITION} ${CTA_SCALE} flex items-center justify-center gap-2 text-base`}
463
  >
464
+ Get Started Free <ArrowRight className={`w-5 h-5 ${CTA_ICON_SHIFT}`} />
465
  </button>
466
  <button
467
  onClick={() => setShowDemo(true)}
468
+ className={`group w-full sm:w-auto px-8 py-4 bg-white text-gray-700 border border-gray-200 rounded-xl font-semibold hover:border-emerald-500 hover:text-emerald-700 ${CTA_TRANSITION} ${CTA_SCALE} flex items-center justify-center gap-2 text-base`}
469
  >
470
+ <Play className={`w-4 h-4 fill-gray-500 ${CTA_PLAY_SHIFT}`} /> Watch a Demo
471
  </button>
472
  </div>
473
 
 
504
  <div className="mb-4">
505
  <h3 className="text-sm font-bold text-gray-800 mb-1">Subject: Wifi down again in Lab 3??</h3>
506
  <p className="text-sm text-gray-600 leading-relaxed">
507
+ Hey support, the wifi in <span className="bg-yellow-200 dark:bg-yellow-500/30 dark:text-yellow-200 text-yellow-900 px-1 rounded font-medium">downstairs lab 3</span> is acting up again.
508
  Can't connect at all. Class starts in 20 mins, need this fixed ASAP!<br /><br />
509
  Thanks,<br />Sarah
510
  </p>
 
735
  <div className="inline-flex items-center gap-2 px-3 py-1 bg-emerald-500/10 text-emerald-400 rounded-full text-[10px] md:text-xs font-bold uppercase tracking-widest border border-emerald-500/20 mb-4 md:mb-6">
736
  The Journey
737
  </div>
738
+ <h2 className="text-3xl sm:text-4xl md:text-6xl font-extrabold text-white tracking-tight leading-[0.95] mb-8 md:mb-12">
739
  From Chaos <br />
740
  to <span className="text-emerald-500">Clarity.</span>
741
  </h2>
 
746
  key={idx}
747
  onMouseEnter={() => setActiveStep(idx)}
748
  onClick={() => setActiveStep(idx)}
749
+ className={`group cursor-pointer p-6 rounded-3xl transition-all duration-300 border ${activeStep === idx
750
  ? 'bg-white/10 border-white/20 shadow-2xl shadow-black/20'
751
  : 'bg-transparent border-transparent hover:bg-white/5 opacity-40 hover:opacity-100'
752
  }`}
753
  >
754
  <div className="flex items-start gap-4 md:gap-6">
755
+ <div className={`shrink-0 w-10 md:w-12 h-10 md:h-12 rounded-xl md:rounded-2xl flex items-center justify-center font-bold text-lg md:text-xl transition-all duration-300 ${activeStep === idx ? 'bg-emerald-500 text-white shadow-lg shadow-emerald-500/20 scale-105' : 'bg-white/10 text-white/40'}`}>
756
  {step.num}
757
  </div>
758
  <div>
759
+ <h3 className={`text-xl font-semibold tracking-tight transition-colors duration-300 ${activeStep === idx ? 'text-white' : 'text-white/60'}`}>
760
  {step.title}
761
  </h3>
762
+ <p
763
+ aria-hidden={activeStep !== idx}
764
+ className={`text-white/60 text-sm mt-3 leading-relaxed max-w-sm overflow-hidden transition-all duration-300 ${activeStep === idx ? 'opacity-100 max-h-24' : 'opacity-0 max-h-0'}`}
765
+ >
766
+ {step.desc}
767
+ </p>
 
 
 
768
  </div>
769
  </div>
770
  </div>
 
775
  {/* Right: Visual Display */}
776
  <div className="w-full md:w-1/2 h-[350px] md:h-[500px] relative">
777
  <div className="absolute inset-0 bg-gradient-to-br from-emerald-500/10 to-transparent rounded-[32px] md:rounded-[40px] border border-white/5 backdrop-blur-3xl overflow-hidden p-6 md:p-12 flex items-center justify-center">
778
+ <AnimatePresence mode="sync">
779
  <motion.div
780
  key={activeStep}
781
+ initial={{ opacity: 0, scale: 0.98, y: 10 }}
782
  animate={{ opacity: 1, scale: 1, y: 0 }}
783
+ exit={{ opacity: 0, scale: 1.01, y: -8 }}
784
+ transition={{ duration: 0.28, ease: 'easeOut' }}
785
+ className="w-full h-full flex flex-col items-center justify-center will-change-transform"
786
  >
787
  <div className="mb-6 inline-flex items-center gap-2 px-4 py-1.5 bg-white/5 text-white/50 rounded-full text-[10px] font-black uppercase tracking-[0.2em]">
788
  <div className={`w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse`} />
 
846
  <button
847
  onClick={() => handlePricingClick(name)}
848
  disabled={isRedirecting && name === 'Growth'}
849
+ className={`w-full flex items-center justify-center gap-2 py-3 rounded-xl font-semibold mb-8 text-sm ${ctaStyle} ${isRedirecting && name === 'Growth' ? 'opacity-80 cursor-not-allowed' : ''}`}
850
  >
851
  {isRedirecting && name === 'Growth' ? (
852
  <>
 
889
  <div className="flex flex-col sm:flex-row items-center justify-center gap-4">
890
  <button
891
  onClick={() => navigate('/admin-signup')}
892
+ className={`group w-full sm:w-auto px-8 py-4 bg-white text-emerald-900 font-bold rounded-xl hover:bg-green-50 ${CTA_TRANSITION} ${CTA_SCALE} shadow-xl`}
893
  >
894
  Get Started Free
895
  </button>
896
  <button
897
  onClick={() => setShowDemo(true)}
898
+ className={`group w-full sm:w-auto px-8 py-4 border border-white/30 text-white font-semibold rounded-xl hover:bg-white/10 ${CTA_TRANSITION} ${CTA_SCALE} flex items-center justify-center gap-2`}
899
  >
900
+ <Play className={`w-4 h-4 fill-white ${CTA_PLAY_SHIFT}`} /> Watch Demo
901
  </button>
902
  </div>
903
  <div className="mt-8">
 
949
  {
950
  heading: 'Resources',
951
  links: [
952
+ { label: 'Documentation', href: '#' },
953
+ { label: 'API Reference', href: '#' },
954
  { label: 'Changelog', href: '/changelog' },
955
+ { label: 'Status Page', href: '#' },
956
  ]
957
  },
958
  {
959
  heading: 'Company',
960
  links: [
961
+ { label: 'About Us', href: '/about-us' },
962
+ { label: 'Careers', href: '#' },
963
  { label: 'Privacy Policy', href: '/privacy' },
964
  { label: 'Terms of Service', href: '/terms' },
965
  ]
 
970
  { label: 'Security Overview', href: '/security' },
971
  { label: 'Privacy Policy', href: '/privacy' },
972
  { label: 'Terms of Service', href: '/terms' },
973
+ { label: 'Cookie Policy', href: '#' },
974
  ]
975
  },
976
  ].map(({ heading, links }) => (
Frontend/src/pages/Login.jsx CHANGED
@@ -7,15 +7,17 @@ import { Eye, EyeOff, BrainCircuit, ArrowRight, Loader2, ArrowLeft } from "lucid
7
 
8
  function Login() {
9
  const [email, setEmail] = useState("");
 
10
  const [password, setPassword] = useState("");
11
  const [showPassword, setShowPassword] = useState(false);
12
  const [error, setError] = useState("");
 
13
 
14
  const [isMagicLink, setIsMagicLink] = useState(false);
15
  const [magicLinkSent, setMagicLinkSent] = useState(false);
16
 
17
  const navigate = useNavigate();
18
- const { login, signInWithMagicLink, loading, user, profile } = useAuthStore();
19
 
20
  // Auto-redirect if already logged in
21
  useEffect(() => {
@@ -41,6 +43,7 @@ function Login() {
41
  }
42
 
43
  setError("");
 
44
 
45
  try {
46
  const { profile } = await login(email, password);
@@ -55,7 +58,7 @@ function Login() {
55
 
56
  if (profile.status === "rejected") {
57
  navigate("/not-approved");
58
- return; // Navigation will happen, but just return to prevent further execution
59
  }
60
 
61
  if (profile.role === "master_admin" && profile.status === "active") {
@@ -74,6 +77,8 @@ function Login() {
74
  errMsg = "Network Error: Failed to fetch. This usually happens if your browser's ad-blocker (like Brave Shields, uBlock Origin, etc.) is blocking Supabase requests. Please try disabling your ad-blocker for this site and refresh!";
75
  }
76
  setError(errMsg);
 
 
77
  }
78
  };
79
 
@@ -85,6 +90,7 @@ function Login() {
85
  }
86
 
87
  setError("");
 
88
  try {
89
  await signInWithMagicLink(email);
90
  setMagicLinkSent(true);
@@ -95,6 +101,16 @@ function Login() {
95
  errMsg = "Network Error: Failed to fetch. This usually happens if your browser's ad-blocker (like Brave Shields, uBlock Origin, etc.) is blocking Supabase requests. Please try disabling your ad-blocker for this site and refresh!";
96
  }
97
  setError(errMsg);
 
 
 
 
 
 
 
 
 
 
98
  }
99
  };
100
 
@@ -107,10 +123,11 @@ function Login() {
107
  <div
108
  className="hidden lg:flex w-1/2 items-center justify-center p-12 relative overflow-hidden"
109
  style={{
110
- background: 'linear-gradient(160deg, #f0fdf4 0%, #dcfce7 60%, #bbf7d0 100%)',
 
 
111
  }}
112
  >
113
- {/* Radial glow */}
114
  <div
115
  className="absolute top-0 left-0 w-[600px] h-[600px] rounded-full pointer-events-none"
116
  style={{
@@ -119,7 +136,6 @@ function Login() {
119
  />
120
 
121
  <div className="relative z-10 max-w-lg">
122
- {/* Logo / Icon */}
123
  <div
124
  className="p-3 rounded-2xl w-fit mb-8"
125
  style={{ background: 'rgba(34,160,69,0.08)', border: '1px solid #d1fae5' }}
@@ -127,13 +143,12 @@ function Login() {
127
  <BrainCircuit className="w-10 h-10" style={{ color: '#16a34a' }} />
128
  </div>
129
 
130
- {/* Headline */}
131
  <h1
132
  style={{
133
  fontFamily: "'Syne', sans-serif",
134
  fontSize: '48px',
135
  fontWeight: 800,
136
- color: '#0f1f12',
137
  letterSpacing: '-0.03em',
138
  lineHeight: 1.1,
139
  marginBottom: '24px',
@@ -143,12 +158,10 @@ function Login() {
143
  <span style={{ color: '#16a34a' }}>IT Support</span>
144
  </h1>
145
 
146
- {/* Subtext */}
147
- <p style={{ color: '#374151', fontSize: '16px', lineHeight: 1.7, marginBottom: '32px' }}>
148
  Join thousands of IT teams using HelpDesk.ai to categorize, route, and resolve tickets instantly.
149
  </p>
150
 
151
- {/* System Status Badge */}
152
  <div
153
  style={{
154
  background: '#ffffff',
@@ -180,9 +193,8 @@ function Login() {
180
  {/* ── Right Panel ── */}
181
  <div
182
  className="flex w-full lg:w-1/2 items-center justify-center p-6 relative"
183
- style={{ background: '#ffffff', borderLeft: '1px solid #f0fdf4' }}
184
  >
185
- {/* Back Button */}
186
  <Link
187
  to="/"
188
  className="absolute top-8 left-8 flex items-center gap-2 transition-all group"
@@ -197,14 +209,13 @@ function Login() {
197
  </Link>
198
 
199
  <div className="w-full max-w-md mt-8 lg:mt-0" style={{ padding: '32px' }}>
200
- {/* Header */}
201
  <div className="text-center" style={{ marginBottom: '40px' }}>
202
  <h2
203
  style={{
204
  fontFamily: "'Syne', sans-serif",
205
  fontSize: '28px',
206
  fontWeight: 800,
207
- color: '#0f1f12',
208
  letterSpacing: '-0.02em',
209
  marginBottom: '8px',
210
  }}
@@ -214,8 +225,6 @@ function Login() {
214
  <p style={{ color: '#6b7280', fontSize: '14px' }}>Please sign in to continue</p>
215
  </div>
216
 
217
- {/* Role Toggle Removed */}
218
-
219
  {error && (
220
  <div className="mb-6 flex items-start gap-3" style={{ background: '#fef2f2', border: '1px solid #fee2e2', borderRadius: '12px', padding: '14px 16px' }}>
221
  <div className="rounded-full p-1 mt-0.5" style={{ background: '#fee2e2' }}>
@@ -242,7 +251,6 @@ function Login() {
242
  </div>
243
  ) : (
244
  <form onSubmit={currentSubmitHandler} className="space-y-5">
245
- {/* Email Field */}
246
  <div>
247
  <label
248
  className="block mb-2"
@@ -271,7 +279,6 @@ function Login() {
271
  />
272
  </div>
273
 
274
- {/* Password Field */}
275
  {!isMagicLink && (
276
  <motion.div initial={{ opacity: 0, height: 0 }} animate={{ opacity: 1, height: "auto" }} exit={{ opacity: 0, height: 0 }}>
277
  <div className="flex justify-between items-center mb-2">
@@ -323,10 +330,9 @@ function Login() {
323
  </motion.div>
324
  )}
325
 
326
- {/* Sign In Button */}
327
  <button
328
  type="submit"
329
- disabled={loading}
330
  className="w-full flex items-center justify-center gap-2 active:scale-[0.98] disabled:opacity-70 disabled:cursor-not-allowed"
331
  style={{
332
  background: 'linear-gradient(135deg, #16a34a, #22c55e)',
@@ -343,8 +349,47 @@ function Login() {
343
  onMouseEnter={(e) => { e.currentTarget.style.transform = 'translateY(-1px)'; e.currentTarget.style.boxShadow = '0 6px 24px rgba(34,160,69,0.35)'; }}
344
  onMouseLeave={(e) => { e.currentTarget.style.transform = 'translateY(0)'; e.currentTarget.style.boxShadow = '0 4px 20px rgba(34,160,69,0.3)'; }}
345
  >
346
- {loading && <Loader2 className="w-5 h-5 animate-spin" />}
347
- {!loading && (isMagicLink ? "Send Magic Link" : "Sign In")}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
348
  </button>
349
 
350
  {/* Divider */}
@@ -354,7 +399,6 @@ function Login() {
354
  <div className="flex-grow" style={{ borderTop: '1px solid #e5e7eb' }}></div>
355
  </div>
356
 
357
- {/* Magic Link Toggle */}
358
  <button
359
  type="button"
360
  onClick={() => { setIsMagicLink(!isMagicLink); setError(""); }}
@@ -376,7 +420,6 @@ function Login() {
376
  {isMagicLink ? "Sign in with Password" : "Sign in with Magic Link"}
377
  </button>
378
 
379
- {/* Create Account */}
380
  <p className="text-center" style={{ fontSize: '14px', color: '#6b7280', marginTop: '32px' }}>
381
  Don't have an account?{" "}
382
  <Link to="/signup" className="hover:underline transition-all" style={{ color: '#16a34a', fontWeight: 600 }}>
@@ -391,4 +434,4 @@ function Login() {
391
  );
392
  }
393
 
394
- export default Login;
 
7
 
8
  function Login() {
9
  const [email, setEmail] = useState("");
10
+ const isDark = document.documentElement.classList.contains('dark');
11
  const [password, setPassword] = useState("");
12
  const [showPassword, setShowPassword] = useState(false);
13
  const [error, setError] = useState("");
14
+ const [isSubmitting, setIsSubmitting] = useState(false);
15
 
16
  const [isMagicLink, setIsMagicLink] = useState(false);
17
  const [magicLinkSent, setMagicLinkSent] = useState(false);
18
 
19
  const navigate = useNavigate();
20
+ const { login, signInWithMagicLink, user, profile } = useAuthStore();
21
 
22
  // Auto-redirect if already logged in
23
  useEffect(() => {
 
43
  }
44
 
45
  setError("");
46
+ setIsSubmitting(true);
47
 
48
  try {
49
  const { profile } = await login(email, password);
 
58
 
59
  if (profile.status === "rejected") {
60
  navigate("/not-approved");
61
+ return;
62
  }
63
 
64
  if (profile.role === "master_admin" && profile.status === "active") {
 
77
  errMsg = "Network Error: Failed to fetch. This usually happens if your browser's ad-blocker (like Brave Shields, uBlock Origin, etc.) is blocking Supabase requests. Please try disabling your ad-blocker for this site and refresh!";
78
  }
79
  setError(errMsg);
80
+ } finally {
81
+ setIsSubmitting(false);
82
  }
83
  };
84
 
 
90
  }
91
 
92
  setError("");
93
+ setIsSubmitting(true);
94
  try {
95
  await signInWithMagicLink(email);
96
  setMagicLinkSent(true);
 
101
  errMsg = "Network Error: Failed to fetch. This usually happens if your browser's ad-blocker (like Brave Shields, uBlock Origin, etc.) is blocking Supabase requests. Please try disabling your ad-blocker for this site and refresh!";
102
  }
103
  setError(errMsg);
104
+ } finally {
105
+ setIsSubmitting(false);
106
+ }
107
+ };
108
+
109
+ const handleGoogleLogin = async () => {
110
+ try {
111
+ await loginWithGoogle();
112
+ } catch (err) {
113
+ console.error("Google login failed", err.message);
114
  }
115
  };
116
 
 
123
  <div
124
  className="hidden lg:flex w-1/2 items-center justify-center p-12 relative overflow-hidden"
125
  style={{
126
+ background: document.documentElement.classList.contains('dark')
127
+ ? 'linear-gradient(160deg, #0f2a1a 0%, #1a3a25 60%, #0d2b1a 100%)'
128
+ : 'linear-gradient(160deg, #f0fdf4 0%, #dcfce7 60%, #bbf7d0 100%)',
129
  }}
130
  >
 
131
  <div
132
  className="absolute top-0 left-0 w-[600px] h-[600px] rounded-full pointer-events-none"
133
  style={{
 
136
  />
137
 
138
  <div className="relative z-10 max-w-lg">
 
139
  <div
140
  className="p-3 rounded-2xl w-fit mb-8"
141
  style={{ background: 'rgba(34,160,69,0.08)', border: '1px solid #d1fae5' }}
 
143
  <BrainCircuit className="w-10 h-10" style={{ color: '#16a34a' }} />
144
  </div>
145
 
 
146
  <h1
147
  style={{
148
  fontFamily: "'Syne', sans-serif",
149
  fontSize: '48px',
150
  fontWeight: 800,
151
+ color: isDark ? '#ffffff' : '#0f1f12',
152
  letterSpacing: '-0.03em',
153
  lineHeight: 1.1,
154
  marginBottom: '24px',
 
158
  <span style={{ color: '#16a34a' }}>IT Support</span>
159
  </h1>
160
 
161
+ <p style={{ color: isDark ? '#d1fae5' : '#374151', fontSize: '16px', lineHeight: 1.7, marginBottom: '32px' }}>
 
162
  Join thousands of IT teams using HelpDesk.ai to categorize, route, and resolve tickets instantly.
163
  </p>
164
 
 
165
  <div
166
  style={{
167
  background: '#ffffff',
 
193
  {/* ── Right Panel ── */}
194
  <div
195
  className="flex w-full lg:w-1/2 items-center justify-center p-6 relative"
196
+ style={{ background: document.documentElement.classList.contains('dark') ? '#111827' : '#ffffff', borderLeft: '1px solid #f0fdf4' }}
197
  >
 
198
  <Link
199
  to="/"
200
  className="absolute top-8 left-8 flex items-center gap-2 transition-all group"
 
209
  </Link>
210
 
211
  <div className="w-full max-w-md mt-8 lg:mt-0" style={{ padding: '32px' }}>
 
212
  <div className="text-center" style={{ marginBottom: '40px' }}>
213
  <h2
214
  style={{
215
  fontFamily: "'Syne', sans-serif",
216
  fontSize: '28px',
217
  fontWeight: 800,
218
+ color: isDark ? '#ffffff' : '#0f1f12',
219
  letterSpacing: '-0.02em',
220
  marginBottom: '8px',
221
  }}
 
225
  <p style={{ color: '#6b7280', fontSize: '14px' }}>Please sign in to continue</p>
226
  </div>
227
 
 
 
228
  {error && (
229
  <div className="mb-6 flex items-start gap-3" style={{ background: '#fef2f2', border: '1px solid #fee2e2', borderRadius: '12px', padding: '14px 16px' }}>
230
  <div className="rounded-full p-1 mt-0.5" style={{ background: '#fee2e2' }}>
 
251
  </div>
252
  ) : (
253
  <form onSubmit={currentSubmitHandler} className="space-y-5">
 
254
  <div>
255
  <label
256
  className="block mb-2"
 
279
  />
280
  </div>
281
 
 
282
  {!isMagicLink && (
283
  <motion.div initial={{ opacity: 0, height: 0 }} animate={{ opacity: 1, height: "auto" }} exit={{ opacity: 0, height: 0 }}>
284
  <div className="flex justify-between items-center mb-2">
 
330
  </motion.div>
331
  )}
332
 
 
333
  <button
334
  type="submit"
335
+ disabled={isSubmitting}
336
  className="w-full flex items-center justify-center gap-2 active:scale-[0.98] disabled:opacity-70 disabled:cursor-not-allowed"
337
  style={{
338
  background: 'linear-gradient(135deg, #16a34a, #22c55e)',
 
349
  onMouseEnter={(e) => { e.currentTarget.style.transform = 'translateY(-1px)'; e.currentTarget.style.boxShadow = '0 6px 24px rgba(34,160,69,0.35)'; }}
350
  onMouseLeave={(e) => { e.currentTarget.style.transform = 'translateY(0)'; e.currentTarget.style.boxShadow = '0 4px 20px rgba(34,160,69,0.3)'; }}
351
  >
352
+ {isSubmitting && <Loader2 className="w-5 h-5 animate-spin" />}
353
+ {!isSubmitting && (isMagicLink ? "Send Magic Link" : "Sign In")}
354
+ </button>
355
+
356
+ {/* Google Button */}
357
+ <button
358
+ type="button"
359
+ disabled={loading}
360
+ onClick={handleGoogleLogin}
361
+ className="w-full flex items-center justify-center gap-3 transition-all active:scale-[0.98] disabled:opacity-70 disabled:cursor-not-allowed"
362
+ style={{
363
+ background: isDark ? '#1f2937' : '#ffffff',
364
+ border: isDark ? '1.5px solid #374151' : '1.5px solid #e5e7eb',
365
+ color: isDark ? '#f9fafb' : '#374151',
366
+ borderRadius: '12px',
367
+ padding: '13px',
368
+ fontWeight: 500,
369
+ fontSize: '15px',
370
+ cursor: 'pointer',
371
+ transition: 'all 0.2s',
372
+ }}
373
+ onMouseEnter={(e) => {
374
+ if (!loading) {
375
+ e.currentTarget.style.background = isDark ? '#374151' : '#f9fafb';
376
+ e.currentTarget.style.borderColor = isDark ? '#4b5563' : '#d1d5db';
377
+ }
378
+ }}
379
+ onMouseLeave={(e) => {
380
+ if (!loading) {
381
+ e.currentTarget.style.background = isDark ? '#1f2937' : '#ffffff';
382
+ e.currentTarget.style.borderColor = isDark ? '#374151' : '#e5e7eb';
383
+ }
384
+ }}
385
+ >
386
+ <svg className="w-5 h-5 shrink-0" viewBox="0 0 24 24" fill="currentColor">
387
+ <path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" fill="#4285F4"/>
388
+ <path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853"/>
389
+ <path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.06H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.94l2.85-2.22.81-.63z" fill="#FBBC05"/>
390
+ <path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.06l3.66 2.84c.87-2.6 3.3-4.52 6.16-4.52z" fill="#EA4335"/>
391
+ </svg>
392
+ <span>Continue with Google</span>
393
  </button>
394
 
395
  {/* Divider */}
 
399
  <div className="flex-grow" style={{ borderTop: '1px solid #e5e7eb' }}></div>
400
  </div>
401
 
 
402
  <button
403
  type="button"
404
  onClick={() => { setIsMagicLink(!isMagicLink); setError(""); }}
 
420
  {isMagicLink ? "Sign in with Password" : "Sign in with Magic Link"}
421
  </button>
422
 
 
423
  <p className="text-center" style={{ fontSize: '14px', color: '#6b7280', marginTop: '32px' }}>
424
  Don't have an account?{" "}
425
  <Link to="/signup" className="hover:underline transition-all" style={{ color: '#16a34a', fontWeight: 600 }}>
 
434
  );
435
  }
436
 
437
+ export default Login;
Frontend/src/pages/Signup.jsx CHANGED
@@ -3,6 +3,7 @@ import { useNavigate, Link } from "react-router-dom";
3
  import useAuthStore from "../store/authStore";
4
  import { supabase } from "../lib/supabaseClient";
5
  import { Eye, EyeOff, BrainCircuit, ArrowRight, Loader2, CheckCircle2, ChevronDown, Search, Building2, ArrowLeft } from "lucide-react";
 
6
 
7
  function Signup() {
8
  const [email, setEmail] = useState("");
@@ -27,6 +28,10 @@ function Signup() {
27
  const dropdownRef = useRef(null);
28
  const navigate = useNavigate();
29
  const { signup, user, profile } = useAuthStore();
 
 
 
 
30
 
31
  // Fetch and subscribe to companies
32
  useEffect(() => {
@@ -103,15 +108,6 @@ function Signup() {
103
  e.preventDefault();
104
  setError("");
105
 
106
- // Password complexity validator — mirrors Supabase's policy
107
- const validatePassword = (pw) => {
108
- if (pw.length < 8) return 'Password must be at least 8 characters long.';
109
- if (!/[a-z]/.test(pw)) return 'Password must contain at least one lowercase letter (a-z).';
110
- if (!/[A-Z]/.test(pw)) return 'Password must contain at least one uppercase letter (A-Z).';
111
- if (!/[0-9]/.test(pw)) return 'Password must contain at least one number (0-9).';
112
- return null;
113
- };
114
-
115
  if (!email || !password || !confirmPassword || !fullName) {
116
  setError("All fields are required.");
117
  return;
@@ -122,14 +118,13 @@ function Signup() {
122
  return;
123
  }
124
 
125
- const pwError = validatePassword(password);
126
- if (pwError) {
127
- setError(pwError);
128
  return;
129
  }
130
 
131
- if (password !== confirmPassword) {
132
- setError("Passwords do not match.");
133
  return;
134
  }
135
 
@@ -325,28 +320,16 @@ function Signup() {
325
  <div className="relative">
326
  <label className="block mb-2" style={labelStyle}>Password</label>
327
  <div className="relative">
328
- <input type={showPassword ? "text" : "password"} placeholder="Min 8 chars" style={{ ...inputStyle, paddingRight: '44px' }} onFocus={inputFocus} onBlur={inputBlur}
329
  value={password} onChange={(e) => { setPassword(e.target.value); setError(""); }} />
330
  <button type="button" onClick={() => setShowPassword(!showPassword)} className="absolute right-3 top-1/2 -translate-y-1/2" style={{ color: '#9ca3af', background: 'none', border: 'none', cursor: 'pointer' }}>
331
  {showPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
332
  </button>
333
  </div>
334
- {/* Live password requirement checklist */}
335
  {password && (
336
- <div className="grid grid-cols-2 gap-x-2 gap-y-0.5 mt-2">
337
- {[
338
- { label: '8+ characters', ok: password.length >= 8 },
339
- { label: 'Uppercase (A-Z)', ok: /[A-Z]/.test(password) },
340
- { label: 'Lowercase (a-z)', ok: /[a-z]/.test(password) },
341
- { label: 'Number (0-9)', ok: /[0-9]/.test(password) },
342
- ].map(({ label, ok }) => (
343
- <span key={label} className={`text-[10px] font-semibold flex items-center gap-1 transition-colors ${
344
- ok ? 'text-emerald-600' : 'text-red-400'
345
- }`}>
346
- <span>{ok ? '✓' : '○'}</span> {label}
347
- </span>
348
- ))}
349
- </div>
350
  )}
351
  </div>
352
  <div className="relative">
@@ -358,6 +341,11 @@ function Signup() {
358
  {showConfirmPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
359
  </button>
360
  </div>
 
 
 
 
 
361
  </div>
362
  </div>
363
 
 
3
  import useAuthStore from "../store/authStore";
4
  import { supabase } from "../lib/supabaseClient";
5
  import { Eye, EyeOff, BrainCircuit, ArrowRight, Loader2, CheckCircle2, ChevronDown, Search, Building2, ArrowLeft } from "lucide-react";
6
+ import { getPasswordValidation, getPasswordValidationMessage } from "../utils/passwordValidation";
7
 
8
  function Signup() {
9
  const [email, setEmail] = useState("");
 
28
  const dropdownRef = useRef(null);
29
  const navigate = useNavigate();
30
  const { signup, user, profile } = useAuthStore();
31
+ const passwordRules = { minLength: 6 };
32
+ const passwordChecks = getPasswordValidation(password, passwordRules);
33
+ const passwordWarning = getPasswordValidationMessage(passwordChecks, passwordRules);
34
+ const confirmPasswordWarning = confirmPassword && password !== confirmPassword ? "Passwords do not match." : "";
35
 
36
  // Fetch and subscribe to companies
37
  useEffect(() => {
 
108
  e.preventDefault();
109
  setError("");
110
 
 
 
 
 
 
 
 
 
 
111
  if (!email || !password || !confirmPassword || !fullName) {
112
  setError("All fields are required.");
113
  return;
 
118
  return;
119
  }
120
 
121
+ if (passwordWarning) {
122
+ setError(passwordWarning);
 
123
  return;
124
  }
125
 
126
+ if (confirmPasswordWarning) {
127
+ setError(confirmPasswordWarning);
128
  return;
129
  }
130
 
 
320
  <div className="relative">
321
  <label className="block mb-2" style={labelStyle}>Password</label>
322
  <div className="relative">
323
+ <input type={showPassword ? "text" : "password"} placeholder="Min 6 chars" style={{ ...inputStyle, paddingRight: '44px' }} onFocus={inputFocus} onBlur={inputBlur}
324
  value={password} onChange={(e) => { setPassword(e.target.value); setError(""); }} />
325
  <button type="button" onClick={() => setShowPassword(!showPassword)} className="absolute right-3 top-1/2 -translate-y-1/2" style={{ color: '#9ca3af', background: 'none', border: 'none', cursor: 'pointer' }}>
326
  {showPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
327
  </button>
328
  </div>
 
329
  {password && (
330
+ <p aria-live="polite" className={`mt-2 text-[11px] font-semibold ${passwordWarning ? "text-red-600" : "text-emerald-700"}`}>
331
+ {passwordWarning || "Password looks good."}
332
+ </p>
 
 
 
 
 
 
 
 
 
 
 
333
  )}
334
  </div>
335
  <div className="relative">
 
341
  {showConfirmPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
342
  </button>
343
  </div>
344
+ {confirmPasswordWarning && (
345
+ <p aria-live="polite" className="mt-2 text-[11px] font-semibold text-red-600">
346
+ {confirmPasswordWarning}
347
+ </p>
348
+ )}
349
  </div>
350
  </div>
351
 
Frontend/src/services/aiAssistant.js CHANGED
@@ -1,5 +1,4 @@
1
- import { GoogleGenerativeAI } from "@google/generative-ai";
2
- import { API_CONFIG } from "../config";
3
 
4
  // ============================================================
5
  // MULTI-API FAILOVER CONFIGURATION
@@ -8,45 +7,30 @@ import { API_CONFIG } from "../config";
8
  // ============================================================
9
 
10
  const buildConfigList = () => {
11
- const env = import.meta.env;
12
  const configs = [];
13
 
14
  // Priority 1: Native Gemini — try modern flash models
15
- const geminiKeys = [
16
- env.VITE_GEMINI_API_KEY_1, env.VITE_GEMINI_API_KEY_2,
17
- env.VITE_GEMINI_API_KEY_3, env.VITE_GEMINI_API_KEY_4
18
- ].filter(Boolean);
19
- // Dynamically retrieve configured model slugs or fallback to stable defaults
20
- const geminiModels = (env.VITE_AI_GEMINI_MODELS || 'gemini-2.5-flash,gemini-2.5-flash-lite,gemini-2.0-flash').split(',');
21
-
22
- geminiKeys.forEach(key => {
23
- geminiModels.forEach(model => {
24
- configs.push({ provider: 'gemini', key, model: model.trim() });
25
- });
 
 
 
 
26
  });
27
 
28
- // Priority 2: OpenRouterupdated model slugs
29
- const openrouterKeys = [
30
- env.VITE_OPENROUTER_API_KEY_1, env.VITE_OPENROUTER_API_KEY_2,
31
- env.VITE_OPENROUTER_API_KEY_3, env.VITE_OPENROUTER_API_KEY_4,
32
- ].filter(Boolean);
33
- const openrouterModels = (env.VITE_AI_OPENROUTER_MODELS || 'meta-llama/llama-3.2-3b-instruct:free,microsoft/phi-3-mini-128k-instruct:free,mistralai/mistral-7b-instruct:free,google/gemma-2-9b-it:free').split(',');
34
-
35
- openrouterKeys.forEach((key, idx) => {
36
- const primaryModel = openrouterModels[idx % openrouterModels.length].trim();
37
- const secondaryModel = openrouterModels[(idx + 1) % openrouterModels.length].trim();
38
- configs.push({ provider: 'openrouter', key, model: primaryModel });
39
- configs.push({ provider: 'openrouter', key, model: secondaryModel });
40
- });
41
-
42
- // Priority 3: Groq — use stable models
43
- const groqKeys = [
44
- env.VITE_GROQ_API_KEY_1, env.VITE_GROQ_API_KEY_2, env.VITE_GROQ_API_KEY_3
45
- ].filter(Boolean);
46
- const groqModels = (env.VITE_AI_GROQ_MODELS || 'llama-3.1-8b-instant,mixtral-8x7b-32768,gemma2-9b-it').split(',');
47
-
48
- groqKeys.forEach((key, idx) => {
49
- configs.push({ provider: 'groq', key, model: groqModels[idx % groqModels.length].trim() });
50
  });
51
 
52
  return configs;
@@ -57,11 +41,8 @@ const buildConfigList = () => {
57
  // PROVIDER HANDLERS
58
  // ============================================================
59
 
60
- const callGemini = async (config, promptText, history, image) => {
61
- const genAI = new GoogleGenerativeAI(config.key);
62
- const model = genAI.getGenerativeModel({ model: config.model });
63
-
64
- let formattedHistory = history.map(msg => {
65
  const parts = [{ text: msg.text || "" }];
66
  if (msg.image) {
67
  const [mime, data] = msg.image.split(';base64,');
@@ -70,72 +51,110 @@ const callGemini = async (config, promptText, history, image) => {
70
  return { role: msg.role === 'bot' ? 'model' : 'user', parts };
71
  });
72
 
73
- // Gemini requires history to start with 'user' role
74
- const firstUserIdx = formattedHistory.findIndex(h => h.role === 'user');
75
- if (firstUserIdx > 0) formattedHistory = formattedHistory.slice(firstUserIdx);
76
- else if (firstUserIdx === -1) formattedHistory = [];
77
-
78
- const chat = model.startChat({ history: formattedHistory, generationConfig: { maxOutputTokens: 2048 } });
79
-
80
  const messageParts = [{ text: promptText }];
81
  if (image) {
82
  const [mime, data] = image.split(';base64,');
83
  messageParts.push({ inlineData: { mimeType: mime.split(':')[1] || 'image/png', data } });
84
  }
85
 
86
- const result = await chat.sendMessage(messageParts);
87
- return result.response.text();
 
88
  };
89
 
90
- const callOpenAICompat = async (config, promptText, history, image, baseUrl, extraHeaders = {}) => {
91
- const messages = history.map(msg => ({
92
- role: msg.role === 'bot' ? 'assistant' : 'user',
93
- content: msg.text || ""
94
- }));
 
 
 
 
 
 
 
 
 
95
 
96
  const userContent = image
97
  ? [{ type: "text", text: promptText }, { type: "image_url", image_url: { url: image } }]
98
  : promptText;
99
 
100
  messages.push({ role: "user", content: userContent });
 
 
101
 
102
- const response = await fetch(`${baseUrl}/chat/completions`, {
103
- method: 'POST',
104
- headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${config.key}`, ...extraHeaders },
105
- body: JSON.stringify({ model: config.model, messages, max_tokens: 2048 })
106
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
 
108
- if (!response.ok) {
109
- const err = new Error(`HTTP ${response.status}`);
110
- err.status = response.status;
111
- throw err;
112
  }
113
- const data = await response.json();
114
- return data.choices?.[0]?.message?.content || "No response received.";
 
 
 
 
 
115
  };
116
 
117
  // Core failover runner — shared by both exported functions
118
  const runWithFailover = async (promptText, history, image) => {
119
  const configList = buildConfigList();
120
- if (configList.length === 0) throw new Error("No AI API keys configured in .env");
121
 
122
  for (let i = 0; i < configList.length; i++) {
123
  const config = configList[i];
124
  console.log(`[AI Failover] Trying ${i + 1}/${configList.length}: ${config.provider} (${config.model})`);
125
 
126
  try {
127
- if (config.provider === 'gemini') {
128
- return await callGemini(config, promptText, history, image);
129
- } else if (config.provider === 'openrouter') {
130
- return await callOpenAICompat(config, promptText, history, image,
131
- 'https://openrouter.ai/api/v1',
132
- { 'HTTP-Referer': API_CONFIG.FRONTEND_URL, 'X-Title': 'AI Helpdesk' }
133
- );
134
- } else if (config.provider === 'groq') {
135
- return await callOpenAICompat(config, promptText, history, null, // Groq = text only
136
- 'https://api.groq.com/openai/v1'
137
- );
138
- }
139
  } catch (error) {
140
  const isRateLimit = error.status === 429
141
  || error.message?.includes('429')
@@ -159,6 +178,12 @@ const localFallbackSummary = (issueText) => {
159
  return { summary, image_description: '' };
160
  };
161
 
 
 
 
 
 
 
162
 
163
  // ============================================================
164
  // EXPORT 1: askAI — Used by the chat troubleshooting assistant
@@ -231,11 +256,15 @@ User Issue: "${issueText}"${imageNote}${imageInstruction}`;
231
  subcategory: parsed.subcategory,
232
  priority: parsed.priority,
233
  assigned_team: parsed.assigned_team,
234
- confidence: parsed.confidence || 0.9
 
235
  };
236
  } catch (err) {
237
  // All providers failed — use smart local fallback so ticket flow never breaks
238
  console.warn('[analyzeTicketWithAI] All providers exhausted, using local fallback:', err.message);
239
- return localFallbackSummary(issueText);
 
 
 
240
  }
241
  };
 
1
+ import { supabase } from "../lib/supabaseClient";
 
2
 
3
  // ============================================================
4
  // MULTI-API FAILOVER CONFIGURATION
 
7
  // ============================================================
8
 
9
  const buildConfigList = () => {
 
10
  const configs = [];
11
 
12
  // Priority 1: Native Gemini — try modern flash models
13
+ configs.push(
14
+ { provider: 'gemini', model: 'gemini-2.5-flash' },
15
+ { provider: 'gemini', model: 'gemini-2.5-flash-lite' },
16
+ { provider: 'gemini', model: 'gemini-2.0-flash' }
17
+ );
18
+
19
+ // Priority 2: OpenRouter — updated model slugs (verified working as of 2025)
20
+ const openrouterModels = [
21
+ 'meta-llama/llama-3.2-3b-instruct:free',
22
+ 'microsoft/phi-3-mini-128k-instruct:free',
23
+ 'mistralai/mistral-7b-instruct:free',
24
+ 'google/gemma-2-9b-it:free',
25
+ ];
26
+ openrouterModels.forEach((model) => {
27
+ configs.push({ provider: 'openrouter', model });
28
  });
29
 
30
+ // Priority 3: Groquse stable, currently-available models
31
+ const groqModels = ['llama-3.1-8b-instant', 'mixtral-8x7b-32768', 'gemma2-9b-it'];
32
+ groqModels.forEach((model) => {
33
+ configs.push({ provider: 'groq', model });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  });
35
 
36
  return configs;
 
41
  // PROVIDER HANDLERS
42
  // ============================================================
43
 
44
+ const buildGeminiMessages = (promptText, history, image) => {
45
+ const formattedHistory = history.map(msg => {
 
 
 
46
  const parts = [{ text: msg.text || "" }];
47
  if (msg.image) {
48
  const [mime, data] = msg.image.split(';base64,');
 
51
  return { role: msg.role === 'bot' ? 'model' : 'user', parts };
52
  });
53
 
 
 
 
 
 
 
 
54
  const messageParts = [{ text: promptText }];
55
  if (image) {
56
  const [mime, data] = image.split(';base64,');
57
  messageParts.push({ inlineData: { mimeType: mime.split(':')[1] || 'image/png', data } });
58
  }
59
 
60
+ return formattedHistory.length > 0
61
+ ? [...formattedHistory, { role: 'user', parts: messageParts }]
62
+ : [{ role: 'user', parts: messageParts }];
63
  };
64
 
65
+ const buildOpenAICompatMessages = (promptText, history, image) => {
66
+ const messages = history.map(msg => {
67
+ const content = msg.image
68
+ ? [
69
+ { type: 'text', text: msg.text || '' },
70
+ { type: 'image_url', image_url: { url: msg.image } }
71
+ ]
72
+ : msg.text || '';
73
+
74
+ return {
75
+ role: msg.role === 'bot' ? 'assistant' : 'user',
76
+ content,
77
+ };
78
+ });
79
 
80
  const userContent = image
81
  ? [{ type: "text", text: promptText }, { type: "image_url", image_url: { url: image } }]
82
  : promptText;
83
 
84
  messages.push({ role: "user", content: userContent });
85
+ return messages;
86
+ };
87
 
88
+ const extractResponseText = (data) => {
89
+ if (typeof data === 'string') return data;
90
+ if (!data || typeof data !== 'object') return '';
91
+
92
+ const openAiContent = data.choices?.[0]?.message?.content;
93
+ if (typeof openAiContent === 'string') return openAiContent;
94
+ if (Array.isArray(openAiContent)) {
95
+ return openAiContent
96
+ .map(part => part?.text || part?.content || '')
97
+ .filter(Boolean)
98
+ .join('');
99
+ }
100
+
101
+ const geminiParts = data.candidates?.[0]?.content?.parts;
102
+ if (Array.isArray(geminiParts)) {
103
+ return geminiParts
104
+ .map(part => part?.text || '')
105
+ .filter(Boolean)
106
+ .join('');
107
+ }
108
+
109
+ if (typeof data.candidates?.[0]?.content === 'string') {
110
+ return data.candidates[0].content;
111
+ }
112
+
113
+ if (typeof data.text === 'string') return data.text;
114
+
115
+ return '';
116
+ };
117
+
118
+ const callProxy = async (config, promptText, history, image) => {
119
+ const body = config.provider === 'gemini'
120
+ ? {
121
+ provider: config.provider,
122
+ model: config.model,
123
+ messages: buildGeminiMessages(promptText, history, image),
124
+ }
125
+ : {
126
+ provider: config.provider,
127
+ model: config.model,
128
+ messages: buildOpenAICompatMessages(promptText, history, config.provider === 'groq' ? null : image),
129
+ };
130
+
131
+ const { data, error } = await supabase.functions.invoke('ai-proxy', { body });
132
 
133
+ if (error) {
134
+ const invokeError = new Error(error.message || 'AI proxy request failed');
135
+ invokeError.status = error.status || error?.context?.status;
136
+ throw invokeError;
137
  }
138
+
139
+ const responseText = extractResponseText(data);
140
+ if (!responseText) {
141
+ throw new Error(`No response received from ${config.provider}`);
142
+ }
143
+
144
+ return responseText;
145
  };
146
 
147
  // Core failover runner — shared by both exported functions
148
  const runWithFailover = async (promptText, history, image) => {
149
  const configList = buildConfigList();
150
+ if (configList.length === 0) throw new Error("No AI providers configured");
151
 
152
  for (let i = 0; i < configList.length; i++) {
153
  const config = configList[i];
154
  console.log(`[AI Failover] Trying ${i + 1}/${configList.length}: ${config.provider} (${config.model})`);
155
 
156
  try {
157
+ return await callProxy(config, promptText, history, image);
 
 
 
 
 
 
 
 
 
 
 
158
  } catch (error) {
159
  const isRateLimit = error.status === 429
160
  || error.message?.includes('429')
 
178
  return { summary, image_description: '' };
179
  };
180
 
181
+ const getSlaBreachAt = (priority = 'Medium') => {
182
+ const hoursMap = { Critical: 2, High: 8, Medium: 24, Low: 72 };
183
+ const slaHours = hoursMap[priority] || 24;
184
+ return new Date(Date.now() + slaHours * 60 * 60 * 1000).toISOString();
185
+ };
186
+
187
 
188
  // ============================================================
189
  // EXPORT 1: askAI — Used by the chat troubleshooting assistant
 
256
  subcategory: parsed.subcategory,
257
  priority: parsed.priority,
258
  assigned_team: parsed.assigned_team,
259
+ confidence: parsed.confidence || 0.9,
260
+ sla_breach_at: getSlaBreachAt(parsed.priority)
261
  };
262
  } catch (err) {
263
  // All providers failed — use smart local fallback so ticket flow never breaks
264
  console.warn('[analyzeTicketWithAI] All providers exhausted, using local fallback:', err.message);
265
+ return {
266
+ ...localFallbackSummary(issueText),
267
+ sla_breach_at: getSlaBreachAt()
268
+ };
269
  }
270
  };
Frontend/src/services/api.js CHANGED
@@ -2,11 +2,17 @@ import axios from 'axios';
2
  import { MOCK_TICKETS } from './mockData';
3
  import { API_CONFIG } from '../config';
4
 
5
- const USE_MOCK = true;
6
  const API_BASE_URL = API_CONFIG.BACKEND_URL;
7
 
8
  const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
9
 
 
 
 
 
 
 
10
  // Safe helper to get data from storage or default
11
  const getStorage = (key, defaultData) => {
12
  try {
@@ -32,48 +38,81 @@ const setStorage = (key, data) => {
32
  }
33
  };
34
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  export const api = {
36
  // Login and Signup have been fully migrated to Supabase via authStore.js
37
  // Ensure that no component tries to use api.login or api.signup anymore.
38
 
39
-
40
  getTickets: async () => {
41
  if (USE_MOCK) {
42
  await delay(500);
43
  return getStorage('tickets', MOCK_TICKETS);
44
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
  },
46
 
47
  createTicket: async (ticketData) => {
48
  if (USE_MOCK) {
49
  await delay(800);
50
- const tickets = getStorage('tickets', MOCK_TICKETS);
51
- const newTicket = {
52
- ticket_id: "TCKT-" + Math.floor(Math.random() * 10000),
53
- status: 'Open',
54
- createdAt: new Date().toISOString(),
55
- ...ticketData,
56
- messages: [
57
- {
58
- sender: 'user',
59
- message: ticketData.description || ticketData.summary || '',
60
- timestamp: new Date().toISOString()
61
- }
62
- ]
63
- };
64
- tickets.unshift(newTicket); // Add to beginning
65
- setStorage('tickets', tickets);
66
- return { data: newTicket };
67
  }
68
  },
69
 
70
  predictTicket: async (issueText, imageBase64 = "") => {
71
  try {
 
72
  // ALWAYS call the real backend for prediction if possible
73
  const response = await axios.post(`${API_BASE_URL}/ai/analyze_ticket`, {
74
  text: issueText,
75
  image_base64: imageBase64,
76
- image_text: ""
 
77
  });
78
 
79
  const result = response.data;
@@ -95,7 +134,10 @@ export const api = {
95
  reasoning: result.reasoning,
96
  decision_factors: result.decision_factors,
97
  image_description: result.image_description,
98
- ocr_text: result.ocr_text
 
 
 
99
  }
100
  };
101
  } catch (error) {
@@ -112,7 +154,10 @@ export const api = {
112
  routing_confidence: 0.5,
113
  duplicate_probability: 0.0,
114
  summary: issueText.substring(0, 50) + "...",
115
- entities: []
 
 
 
116
  }
117
  };
118
  }
 
2
  import { MOCK_TICKETS } from './mockData';
3
  import { API_CONFIG } from '../config';
4
 
5
+ const USE_MOCK = API_CONFIG.USE_MOCK;
6
  const API_BASE_URL = API_CONFIG.BACKEND_URL;
7
 
8
  const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
9
 
10
+ const getSlaBreachAt = (priority = 'Low') => {
11
+ const hoursMap = { Critical: 2, High: 8, Medium: 24, Low: 72 };
12
+ const slaHours = hoursMap[priority] || 72;
13
+ return new Date(Date.now() + slaHours * 60 * 60 * 1000).toISOString();
14
+ };
15
+
16
  // Safe helper to get data from storage or default
17
  const getStorage = (key, defaultData) => {
18
  try {
 
38
  }
39
  };
40
 
41
+ // Shared mock logic for createTicket
42
+ const createTicketMock = (ticketData) => {
43
+ const tickets = getStorage('tickets', MOCK_TICKETS);
44
+ const newTicket = {
45
+ ticket_id: "TCKT-" + Math.floor(Math.random() * 10000),
46
+ status: 'Open',
47
+ createdAt: new Date().toISOString(),
48
+ ...ticketData,
49
+ messages: [
50
+ {
51
+ sender: 'user',
52
+ message: ticketData.description || ticketData.summary || '',
53
+ timestamp: new Date().toISOString()
54
+ }
55
+ ]
56
+ };
57
+ tickets.unshift(newTicket); // Add to beginning
58
+ setStorage('tickets', tickets);
59
+ return { data: newTicket };
60
+ };
61
+
62
  export const api = {
63
  // Login and Signup have been fully migrated to Supabase via authStore.js
64
  // Ensure that no component tries to use api.login or api.signup anymore.
65
 
 
66
  getTickets: async () => {
67
  if (USE_MOCK) {
68
  await delay(500);
69
  return getStorage('tickets', MOCK_TICKETS);
70
  }
71
+ try {
72
+ const response = await axios.get(`${API_BASE_URL}/tickets`);
73
+ const data = response?.data;
74
+
75
+ // Normalize to the mock shape: an array of tickets
76
+ if (Array.isArray(data)) return data;
77
+ if (data && Array.isArray(data.data)) return data.data;
78
+ if (data && Array.isArray(data.tickets)) return data.tickets;
79
+
80
+ return data;
81
+ } catch (error) {
82
+ console.error("Backend unavailable, falling back to mock:", error);
83
+ await delay(500);
84
+ return getStorage('tickets', MOCK_TICKETS);
85
+ }
86
  },
87
 
88
  createTicket: async (ticketData) => {
89
  if (USE_MOCK) {
90
  await delay(800);
91
+ return createTicketMock(ticketData);
92
+ }
93
+ try {
94
+ const response = await axios.post(`${API_BASE_URL}/tickets/save`, ticketData);
95
+ const created = response?.data;
96
+
97
+ // Normalize to mock shape: { data: <createdTicket> }
98
+ if (created && created.data) return created;
99
+ return { data: created };
100
+ } catch (error) {
101
+ console.error("Backend unavailable, falling back to mock:", error);
102
+ await delay(800);
103
+ return createTicketMock(ticketData);
 
 
 
 
104
  }
105
  },
106
 
107
  predictTicket: async (issueText, imageBase64 = "") => {
108
  try {
109
+ const currentUser = JSON.parse(sessionStorage.getItem("currentUser") || "{}");
110
  // ALWAYS call the real backend for prediction if possible
111
  const response = await axios.post(`${API_BASE_URL}/ai/analyze_ticket`, {
112
  text: issueText,
113
  image_base64: imageBase64,
114
+ image_text: "",
115
+ company_id: currentUser.company_id || currentUser.companyId || null
116
  });
117
 
118
  const result = response.data;
 
134
  reasoning: result.reasoning,
135
  decision_factors: result.decision_factors,
136
  image_description: result.image_description,
137
+ ocr_text: result.ocr_text,
138
+ is_potential_duplicate: result.is_potential_duplicate || false,
139
+ parent_ticket_id: result.parent_ticket_id || result.duplicate_ticket?.duplicate_ticket_id || null,
140
+ sla_breach_at: result.sla_breach_at || getSlaBreachAt(result.priority)
141
  }
142
  };
143
  } catch (error) {
 
154
  routing_confidence: 0.5,
155
  duplicate_probability: 0.0,
156
  summary: issueText.substring(0, 50) + "...",
157
+ entities: [],
158
+ is_potential_duplicate: false,
159
+ parent_ticket_id: null,
160
+ sla_breach_at: getSlaBreachAt("Medium")
161
  }
162
  };
163
  }
Frontend/src/store/authStore.js CHANGED
@@ -1,8 +1,63 @@
1
  import { create } from 'zustand';
2
  import { persist } from 'zustand/middleware';
3
  import { supabase } from '../lib/supabaseClient';
 
4
  import useTicketStore from './ticketStore';
5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  const useAuthStore = create(
7
  persist(
8
  (set, get) => ({
@@ -10,6 +65,7 @@ const useAuthStore = create(
10
  user: null,
11
  profile: null,
12
  loading: false,
 
13
 
14
  // --- SUPABASE AUTH METHODS ---
15
 
@@ -18,38 +74,25 @@ const useAuthStore = create(
18
  if (!user) return null;
19
 
20
  const metadata = user.user_metadata || {};
21
- const currentProfile = get().profile;
22
-
23
- // 1. Resolve FROM METADATA or PERSISTED state
24
- // Priority 1: If we have a persisted session for THIS user and it's active, keep it
25
- // to prevent temporary lobbies during refresh/tab switching.
26
- if (currentProfile && currentProfile.id === user.id && currentProfile.status === 'active') {
27
- console.log("Active profile retained from state.");
28
- // Background fetch to ensure session is still valid/synced
29
- get()._syncProfile(user.id);
30
- return currentProfile;
31
- }
32
 
33
- // Priority 2: Use Auth Metadata (Instant fallback)
34
- const isMasterAdmin = user.email === 'masteradmin@helpdesk.ai';
 
 
 
 
35
 
36
  const instantProfile = {
37
  id: user.id,
38
  email: user.email,
39
- full_name: isMasterAdmin ? 'Master Admin' : (metadata.full_name || 'User'),
40
- role: isMasterAdmin ? 'master_admin' : (metadata.role || 'user'),
41
- status: isMasterAdmin ? 'active' : 'pending_email_verification',
42
  company: metadata.company || ''
43
  };
44
 
45
- // 2. Sync with Database First before setting a fallback
46
- // This prevents flashes of 'pending_email_verification' when returning from magic links
47
- const dbProfile = await get()._syncProfile(user.id);
48
- if (dbProfile) {
49
- return dbProfile;
50
- }
51
-
52
- console.log("Falling back to instant profile resolved from metadata:", instantProfile.role);
53
  set({ profile: instantProfile });
54
  return instantProfile;
55
  },
@@ -79,31 +122,49 @@ const useAuthStore = create(
79
  },
80
 
81
  getCurrentUser: async () => {
82
- try {
83
- const { data: { user }, error } = await supabase.auth.getUser();
84
- if (error) throw error;
 
 
 
 
 
 
 
 
 
 
 
 
 
85
 
86
- if (user) {
87
- set({ user });
88
- // Don't 'await' here because we want 'loading: false' ASAP
89
- get().getProfile(user);
90
- } else {
 
 
 
 
91
  set({ user: null, profile: null });
 
 
 
 
92
  }
93
- return user;
94
- // eslint-disable-next-line no-unused-vars
95
- } catch (error) {
96
- set({ user: null, profile: null });
97
- return null;
98
- } finally {
99
- set({ loading: false });
100
- }
101
  },
102
 
103
  login: async (email, password) => {
104
  set({ loading: true });
105
  console.log("Attempting login for:", email);
106
  try {
 
 
107
  const { data, error } = await supabase.auth.signInWithPassword({
108
  email,
109
  password,
@@ -134,6 +195,26 @@ const useAuthStore = create(
134
  }
135
  },
136
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
137
  signInWithMagicLink: async (email) => {
138
  set({ loading: true });
139
  console.log("Attempting magic link / OTP login for:", email);
@@ -192,6 +273,14 @@ const useAuthStore = create(
192
  console.log("Starting signup for:", email);
193
 
194
  try {
 
 
 
 
 
 
 
 
195
  // 1. Auth Signup with Metadata
196
  console.log("Step 1: Auth.signUp...");
197
  const { data, error } = await supabase.auth.signUp({
@@ -232,6 +321,15 @@ const useAuthStore = create(
232
  logout: async () => {
233
  set({ loading: true });
234
  try {
 
 
 
 
 
 
 
 
 
235
  const { error } = await supabase.auth.signOut();
236
  if (error) throw error;
237
  set({ user: null, profile: null });
@@ -279,25 +377,31 @@ const useAuthStore = create(
279
 
280
  supabase.auth.onAuthStateChange(async (event, session) => {
281
  console.log("Auth state change:", event);
282
- if (session?.user) {
283
- set({ user: session.user });
284
- get().getProfile(session.user);
285
- } else {
 
 
 
 
 
286
  set({ user: null, profile: null });
 
 
287
  }
288
- set({ loading: false });
289
  });
290
  }
291
  }),
292
  {
293
  name: 'auth-storage',
294
  partialize: (state) => ({
295
- // We keep profile persisted for quick UI transitions,
296
- // but session is handled by Supabase cookie/localStorage
297
- profile: state.profile
298
  }),
299
  }
300
  )
301
  );
302
 
303
  export default useAuthStore;
 
 
1
  import { create } from 'zustand';
2
  import { persist } from 'zustand/middleware';
3
  import { supabase } from '../lib/supabaseClient';
4
+ import { API_CONFIG } from '../config';
5
  import useTicketStore from './ticketStore';
6
 
7
+ const BACKEND_URL = API_CONFIG.BACKEND_URL;
8
+
9
+ const verifyServerCookieSession = async () => {
10
+ try {
11
+ const controller = new AbortController();
12
+ const timeout = setTimeout(() => controller.abort(), 5000);
13
+ const res = await fetch(`${BACKEND_URL}/auth/me`, {
14
+ method: 'GET',
15
+ credentials: 'include',
16
+ headers: { Accept: 'application/json' },
17
+ signal: controller.signal,
18
+ });
19
+ clearTimeout(timeout);
20
+ if (!res.ok) return null;
21
+ const body = await res.json();
22
+ return body?.user || null;
23
+ } catch (e) {
24
+ console.warn('Server cookie session check failed:', e?.message || e);
25
+ return null;
26
+ }
27
+ };
28
+
29
+ const mirrorBackendAuth = async (path, payload) => {
30
+ try {
31
+ const controller = new AbortController();
32
+ const timeout = setTimeout(() => controller.abort(), 5000);
33
+ await fetch(`${BACKEND_URL}${path}`, {
34
+ method: 'POST',
35
+ credentials: 'include',
36
+ headers: { 'Content-Type': 'application/json' },
37
+ body: JSON.stringify(payload),
38
+ signal: controller.signal,
39
+ });
40
+ clearTimeout(timeout);
41
+ } catch (e) {
42
+ console.warn(`Backend auth ${path} failed:`, e?.message || e);
43
+ }
44
+ };
45
+
46
+ let currentUserPromise = null;
47
+
48
+ const getProfileCache = (profile) => {
49
+ if (!profile?.id) return null;
50
+
51
+ return {
52
+ id: profile.id,
53
+ email: profile.email,
54
+ full_name: profile.full_name,
55
+ company: profile.company,
56
+ company_id: profile.company_id,
57
+ profile_picture: profile.profile_picture,
58
+ };
59
+ };
60
+
61
  const useAuthStore = create(
62
  persist(
63
  (set, get) => ({
 
65
  user: null,
66
  profile: null,
67
  loading: false,
68
+ isCheckingSession: true,
69
 
70
  // --- SUPABASE AUTH METHODS ---
71
 
 
74
  if (!user) return null;
75
 
76
  const metadata = user.user_metadata || {};
77
+ set({ profile: null });
 
 
 
 
 
 
 
 
 
 
78
 
79
+ // Always resolve authorization fields from the database. Local storage and
80
+ // user_metadata are client-controlled surfaces and must not grant roles.
81
+ const dbProfile = await get()._syncProfile(user.id);
82
+ if (dbProfile) {
83
+ return dbProfile;
84
+ }
85
 
86
  const instantProfile = {
87
  id: user.id,
88
  email: user.email,
89
+ full_name: metadata.full_name || 'User',
90
+ role: 'user',
91
+ status: 'pending_email_verification',
92
  company: metadata.company || ''
93
  };
94
 
95
+ console.log("Falling back to non-authoritative profile:", instantProfile.role);
 
 
 
 
 
 
 
96
  set({ profile: instantProfile });
97
  return instantProfile;
98
  },
 
122
  },
123
 
124
  getCurrentUser: async () => {
125
+ if (currentUserPromise) {
126
+ return currentUserPromise;
127
+ }
128
+
129
+ currentUserPromise = (async () => {
130
+ try {
131
+ set({ isCheckingSession: true });
132
+ const cookieUser = await verifyServerCookieSession();
133
+ if (cookieUser) {
134
+ set({ user: cookieUser });
135
+ await get().getProfile(cookieUser);
136
+ return cookieUser;
137
+ }
138
+
139
+ const { data: { user }, error } = await supabase.auth.getUser();
140
+ if (error) throw error;
141
 
142
+ if (user) {
143
+ set({ user });
144
+ await get().getProfile(user);
145
+ } else {
146
+ set({ user: null, profile: null });
147
+ }
148
+ return user;
149
+ // eslint-disable-next-line no-unused-vars
150
+ } catch (error) {
151
  set({ user: null, profile: null });
152
+ return null;
153
+ } finally {
154
+ set({ loading: false, isCheckingSession: false });
155
+ currentUserPromise = null;
156
  }
157
+ })();
158
+
159
+ return currentUserPromise;
 
 
 
 
 
160
  },
161
 
162
  login: async (email, password) => {
163
  set({ loading: true });
164
  console.log("Attempting login for:", email);
165
  try {
166
+ await mirrorBackendAuth('/auth/login', { email, password });
167
+
168
  const { data, error } = await supabase.auth.signInWithPassword({
169
  email,
170
  password,
 
195
  }
196
  },
197
 
198
+ loginWithGoogle: async () => {
199
+ const { error } =
200
+ await supabase.auth.signInWithOAuth({
201
+ provider: 'google',
202
+ options: {
203
+ redirectTo:
204
+ `${window.location.origin}/auth/callback`
205
+ }
206
+ });
207
+
208
+ if (error) {
209
+ console.error(
210
+ "Google OAuth error:",
211
+ error.message
212
+ );
213
+
214
+ throw error;
215
+ }
216
+ },
217
+
218
  signInWithMagicLink: async (email) => {
219
  set({ loading: true });
220
  console.log("Attempting magic link / OTP login for:", email);
 
273
  console.log("Starting signup for:", email);
274
 
275
  try {
276
+ await mirrorBackendAuth('/auth/signup', {
277
+ email,
278
+ password,
279
+ full_name: fullName,
280
+ role,
281
+ company,
282
+ });
283
+
284
  // 1. Auth Signup with Metadata
285
  console.log("Step 1: Auth.signUp...");
286
  const { data, error } = await supabase.auth.signUp({
 
321
  logout: async () => {
322
  set({ loading: true });
323
  try {
324
+ try {
325
+ await fetch(`${BACKEND_URL}/auth/logout`, {
326
+ method: 'POST',
327
+ credentials: 'include',
328
+ });
329
+ } catch (e) {
330
+ console.warn('Backend cookie logout failed:', e?.message || e);
331
+ }
332
+
333
  const { error } = await supabase.auth.signOut();
334
  if (error) throw error;
335
  set({ user: null, profile: null });
 
377
 
378
  supabase.auth.onAuthStateChange(async (event, session) => {
379
  console.log("Auth state change:", event);
380
+ try {
381
+ if (session?.user) {
382
+ set({ user: session.user, loading: true, isCheckingSession: true });
383
+ await get().getProfile(session.user);
384
+ } else {
385
+ set({ user: null, profile: null });
386
+ }
387
+ } catch (e) {
388
+ console.warn("Auth state change error:", e?.message || e);
389
  set({ user: null, profile: null });
390
+ } finally {
391
+ set({ loading: false, isCheckingSession: false });
392
  }
 
393
  });
394
  }
395
  }),
396
  {
397
  name: 'auth-storage',
398
  partialize: (state) => ({
399
+ // Cache display-only profile fields. Role/status must come from the DB.
400
+ profile: getProfileCache(state.profile)
 
401
  }),
402
  }
403
  )
404
  );
405
 
406
  export default useAuthStore;
407
+
Frontend/src/store/ticketStore.js CHANGED
@@ -3,14 +3,19 @@ import { persist } from 'zustand/middleware';
3
 
4
  const useTicketStore = create(
5
  persist(
6
- (set) => ({
7
  aiTicket: null,
8
  activeTicket: null,
9
  autoResolvedTickets: [], // For analytics
10
  tickets: [], // Global queue for admins
11
  notifications: [], // User notifications
 
 
12
  setAITicket: (data) => set({ aiTicket: data }),
13
  setActiveTicket: (ticket) => set({ activeTicket: ticket }),
 
 
 
14
  addAutoResolvedTicket: (record) => set((state) => ({
15
  autoResolvedTickets: [...state.autoResolvedTickets, record]
16
  })),
@@ -30,27 +35,68 @@ const useTicketStore = create(
30
  tickets: [...state.tickets, ticket]
31
  };
32
  }),
33
- updateTicket: (ticketId, updates) => set((state) => {
34
- // eslint-disable-next-line no-unused-vars
35
- const existingTicket = state.tickets.find(t => t.ticket_id === ticketId);
36
- const updatedTickets = state.tickets.map(t => t.ticket_id === ticketId ? { ...t, ...updates } : t);
37
- const shouldUpdateActive = state.activeTicket?.ticket_id === ticketId;
38
-
39
-
40
 
 
 
 
 
 
41
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  return {
43
  tickets: updatedTickets,
44
  activeTicket: shouldUpdateActive ? { ...state.activeTicket, ...updates } : state.activeTicket
45
  };
46
  }),
47
 
48
- removeTicket: (ticketId) => set((state) => ({
49
- tickets: state.tickets.filter(t => t.ticket_id !== ticketId),
50
- activeTicket: state.activeTicket?.ticket_id === ticketId
51
- ? null
52
- : state.activeTicket
53
- })),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  appendMessage: (ticketId, message) => set((state) => {
55
  const updatedTickets = state.tickets.map(t =>
56
  t.ticket_id === ticketId
 
3
 
4
  const useTicketStore = create(
5
  persist(
6
+ (set, get) => ({
7
  aiTicket: null,
8
  activeTicket: null,
9
  autoResolvedTickets: [], // For analytics
10
  tickets: [], // Global queue for admins
11
  notifications: [], // User notifications
12
+ wsConnected: false, // WebSocket connection status
13
+
14
  setAITicket: (data) => set({ aiTicket: data }),
15
  setActiveTicket: (ticket) => set({ activeTicket: ticket }),
16
+
17
+ setWsConnected: (connected) => set({ wsConnected: connected }),
18
+
19
  addAutoResolvedTicket: (record) => set((state) => ({
20
  autoResolvedTickets: [...state.autoResolvedTickets, record]
21
  })),
 
35
  tickets: [...state.tickets, ticket]
36
  };
37
  }),
38
+ upsertTicket: (ticket) => set((state) => {
39
+ const ticketId = ticket?.id ?? ticket?.ticket_id;
40
+ if (!ticketId) return state;
 
 
 
 
41
 
42
+ const exists = state.tickets.some(t => (t.id ?? t.ticket_id) === ticketId);
43
+ const tickets = exists
44
+ ? state.tickets.map(t => (t.id ?? t.ticket_id) === ticketId ? { ...t, ...ticket } : t)
45
+ : [ticket, ...state.tickets];
46
+ const shouldUpdateActive = (state.activeTicket?.id ?? state.activeTicket?.ticket_id) === ticketId;
47
 
48
+ return {
49
+ tickets,
50
+ activeTicket: shouldUpdateActive ? { ...state.activeTicket, ...ticket } : state.activeTicket
51
+ };
52
+ }),
53
+ removeTicket: (ticketId) => set((state) => ({
54
+ tickets: state.tickets.filter(t => (t.id ?? t.ticket_id) !== ticketId),
55
+ activeTicket: (state.activeTicket?.id ?? state.activeTicket?.ticket_id) === ticketId
56
+ ? null
57
+ : state.activeTicket
58
+ })),
59
+ updateTicket: (ticketId, updates) => set((state) => {
60
+ const existingTicket = state.tickets.find(t => (t.id ?? t.ticket_id) === ticketId);
61
+ if (!existingTicket) return state;
62
+ const updatedTickets = state.tickets.map(t => (t.id ?? t.ticket_id) === ticketId ? { ...t, ...updates } : t);
63
+ const shouldUpdateActive = (state.activeTicket?.id ?? state.activeTicket?.ticket_id) === ticketId;
64
  return {
65
  tickets: updatedTickets,
66
  activeTicket: shouldUpdateActive ? { ...state.activeTicket, ...updates } : state.activeTicket
67
  };
68
  }),
69
 
70
+ /**
71
+ * Route an incoming WebSocket message to the correct store action.
72
+ *
73
+ * Call this from the component that owns the WebSocket connection
74
+ * (e.g. AdminDashboard) whenever a message arrives.
75
+ */
76
+ handleWsMessage: (msg) => {
77
+ if (!msg || !msg.type) return;
78
+
79
+ const { type, event, ticket, ticket_id } = msg;
80
+
81
+ switch (type) {
82
+ case "ticket_update": {
83
+ if (!ticket) break;
84
+ if (event === "created") {
85
+ // Avoid duplicates — use upsert
86
+ get().upsertTicket(ticket);
87
+ } else if (event === "updated") {
88
+ get().upsertTicket(ticket);
89
+ } else if (event === "deleted") {
90
+ get().removeTicket(ticket_id);
91
+ }
92
+ break;
93
+ }
94
+ default:
95
+ // Ignore unknown message types (e.g. heartbeat)
96
+ break;
97
+ }
98
+ },
99
+
100
  appendMessage: (ticketId, message) => set((state) => {
101
  const updatedTickets = state.tickets.map(t =>
102
  t.ticket_id === ticketId
Frontend/src/user/components/QuickActions.jsx CHANGED
@@ -7,6 +7,7 @@ const actions = [
7
  title: "Network Issues",
8
  description: "Connectivity problems, VPN access, and slow internet.",
9
  category: "Network",
 
10
  icon: Network,
11
  iconBg: '#EDFAF3',
12
  iconColor: '#16a34a',
@@ -15,6 +16,7 @@ const actions = [
15
  title: "Software Problems",
16
  description: "Application crashes, license issues, and installations.",
17
  category: "Software",
 
18
  icon: Laptop,
19
  iconBg: '#EEF2FF',
20
  iconColor: '#4f46e5',
@@ -23,6 +25,7 @@ const actions = [
23
  title: "Access Requests",
24
  description: "Permission changes, new account setup, and MFA.",
25
  category: "Access",
 
26
  icon: ShieldCheck,
27
  iconBg: '#F5F0FF',
28
  iconColor: '#7c3aed',
@@ -33,8 +36,8 @@ const QuickActions = () => {
33
  const navigate = useNavigate();
34
  const [hoveredIdx, setHoveredIdx] = useState(null);
35
 
36
- const handleActionClick = (category) => {
37
- navigate('/create-ticket', { state: { prefilledCategory: category } });
38
  };
39
 
40
  return (
@@ -42,7 +45,7 @@ const QuickActions = () => {
42
  {actions.map((action, index) => (
43
  <div
44
  key={index}
45
- onClick={() => handleActionClick(action.category)}
46
  onMouseEnter={() => setHoveredIdx(index)}
47
  onMouseLeave={() => setHoveredIdx(null)}
48
  style={{
 
7
  title: "Network Issues",
8
  description: "Connectivity problems, VPN access, and slow internet.",
9
  category: "Network",
10
+ templateId: "vpn-connectivity",
11
  icon: Network,
12
  iconBg: '#EDFAF3',
13
  iconColor: '#16a34a',
 
16
  title: "Software Problems",
17
  description: "Application crashes, license issues, and installations.",
18
  category: "Software",
19
+ templateId: "software-installation",
20
  icon: Laptop,
21
  iconBg: '#EEF2FF',
22
  iconColor: '#4f46e5',
 
25
  title: "Access Requests",
26
  description: "Permission changes, new account setup, and MFA.",
27
  category: "Access",
28
+ templateId: "password-reset",
29
  icon: ShieldCheck,
30
  iconBg: '#F5F0FF',
31
  iconColor: '#7c3aed',
 
36
  const navigate = useNavigate();
37
  const [hoveredIdx, setHoveredIdx] = useState(null);
38
 
39
+ const handleActionClick = (action) => {
40
+ navigate('/create-ticket', { state: { templateId: action.templateId, prefilledCategory: action.category } });
41
  };
42
 
43
  return (
 
45
  {actions.map((action, index) => (
46
  <div
47
  key={index}
48
+ onClick={() => handleActionClick(action)}
49
  onMouseEnter={() => setHoveredIdx(index)}
50
  onMouseLeave={() => setHoveredIdx(null)}
51
  style={{
Frontend/src/user/components/RecentTickets.jsx CHANGED
@@ -150,6 +150,11 @@ const RecentTickets = () => {
150
  <p style={{ fontSize: '14px', fontWeight: 500, color: '#111827', margin: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', maxWidth: '320px' }}>
151
  {ticket.summary || ticket.subject || ticket.description || "No description provided"}
152
  </p>
 
 
 
 
 
153
  </td>
154
  <td style={{ padding: '16px 28px' }}>
155
  {getStatusBadge(ticket.status)}
 
150
  <p style={{ fontSize: '14px', fontWeight: 500, color: '#111827', margin: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', maxWidth: '320px' }}>
151
  {ticket.summary || ticket.subject || ticket.description || "No description provided"}
152
  </p>
153
+ {ticket?.metadata?.translation?.translated && (
154
+ <p style={{ fontSize: '11px', color: '#0369a1', margin: '4px 0 0' }}>
155
+ Translated from {ticket.metadata.translation.source_language_name || ticket.metadata.translation.source_language || 'Unknown'}
156
+ </p>
157
+ )}
158
  </td>
159
  <td style={{ padding: '16px 28px' }}>
160
  {getStatusBadge(ticket.status)}
Frontend/src/user/components/TopNav.jsx CHANGED
@@ -1,5 +1,5 @@
1
- import React, { useState } from 'react';
2
- import { Bell, Box, CheckCircle2, MessageSquare, Menu, X, LogOut, User as UserIcon, BookOpen } from 'lucide-react';
3
  import { Link, useNavigate } from 'react-router-dom';
4
  import { Avatar, AvatarFallback, AvatarImage } from "../../components/ui/avatar";
5
  import { Button } from "../../components/ui/button";
@@ -8,15 +8,32 @@ import { Popover, PopoverContent, PopoverTrigger } from "../../components/ui/pop
8
  import useTicketStore from "../../store/ticketStore";
9
  // removed useNotificationStore
10
  import NotificationPopover from "./NotificationPopover";
11
-
12
  import useAuthStore from "../../store/authStore";
13
 
14
  const TopNav = () => {
15
  const navigate = useNavigate();
16
-
17
-
18
  const { profile, logout } = useAuthStore();
19
  const [isMenuOpen, setIsMenuOpen] = useState(false);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
 
21
  const initials = profile?.full_name ? profile.full_name[0].toUpperCase() : (profile?.email ? profile.email[0].toUpperCase() : 'U');
22
 
@@ -25,29 +42,35 @@ const TopNav = () => {
25
  navigate('/login');
26
  };
27
 
28
-
29
-
30
  return (
31
- <header className="w-full bg-white border-b border-gray-200 sticky top-0 z-50">
32
  <div className="max-w-[1100px] mx-auto px-4 md:px-6 h-16 flex items-center justify-between">
33
  {/* Left: Logo */}
34
  <div className="flex items-center gap-3 cursor-pointer" onClick={() => navigate('/dashboard')}>
35
  <div className="flex items-center justify-center overflow-hidden">
36
  <img src="/favicon.png" alt="HELPDESK.AI Logo" className="w-7 h-7 object-contain" />
37
  </div>
38
- <h1 className="text-xl font-black tracking-tighter text-gray-900 italic">HELPDESK.AI</h1>
39
  </div>
40
 
41
  {/* Center: Navigation Links */}
42
  <nav className="hidden md:flex items-center gap-8">
43
- <Link className="text-sm font-semibold text-gray-900 hover:text-emerald-600 transition-colors" to="/dashboard">Dashboard</Link>
44
- <Link className="text-sm font-semibold text-gray-500 hover:text-gray-900 transition-colors" to="/my-tickets">My Tickets</Link>
45
- <Link className="text-sm font-semibold text-gray-500 hover:text-gray-900 transition-colors" to="/help">Help</Link>
46
- <Link className="text-sm font-semibold text-gray-500 hover:text-gray-900 transition-colors" to="/docs">Documentation</Link>
47
  </nav>
48
 
49
  {/* Right: Profile */}
50
  <div className="flex items-center gap-3">
 
 
 
 
 
 
 
 
 
51
  <NotificationPopover />
52
  <div className="hidden md:block">
53
  <Avatar
@@ -60,7 +83,7 @@ const TopNav = () => {
60
  </div>
61
  <button
62
  onClick={() => setIsMenuOpen(!isMenuOpen)}
63
- className="md:hidden p-2 text-gray-600 focus:outline-none"
64
  >
65
  {isMenuOpen ? <X size={24} /> : <Menu size={24} />}
66
  </button>
@@ -69,54 +92,35 @@ const TopNav = () => {
69
 
70
  {/* Mobile Menu Overlay */}
71
  {isMenuOpen && (
72
- <div className="md:hidden bg-white border-t border-gray-100 absolute w-full shadow-2xl z-50 animate-in fade-in slide-in-from-top-2 duration-200">
73
  <div className="px-6 py-8 space-y-6">
74
- <div className="flex items-center gap-4 border-b border-gray-50 pb-6">
75
  <Avatar className="size-12 border border-gray-100">
76
  <AvatarImage src={profile?.profile_picture} />
77
  <AvatarFallback className="bg-emerald-50 text-emerald-700 font-black">{initials}</AvatarFallback>
78
  </Avatar>
79
  <div>
80
- <p className="font-bold text-gray-900">{profile?.full_name}</p>
81
  <p className="text-xs text-gray-400 font-medium">{profile?.email}</p>
82
  </div>
83
  </div>
84
 
85
  <div className="space-y-4">
86
- <Link
87
- to="/dashboard"
88
- onClick={() => setIsMenuOpen(false)}
89
- className="flex items-center gap-3 text-lg font-bold text-gray-700 hover:text-emerald-700 transition-colors"
90
- >
91
  <Box size={20} className="text-gray-400" /> Dashboard
92
  </Link>
93
- <Link
94
- to="/my-tickets"
95
- onClick={() => setIsMenuOpen(false)}
96
- className="flex items-center gap-3 text-lg font-bold text-gray-700 hover:text-emerald-700 transition-colors"
97
- >
98
  <MessageSquare size={20} className="text-gray-400" /> My Tickets
99
  </Link>
100
- <Link
101
- to="/profile"
102
- onClick={() => setIsMenuOpen(false)}
103
- className="flex items-center gap-3 text-lg font-bold text-gray-700 hover:text-emerald-700 transition-colors"
104
- >
105
  <UserIcon size={20} className="text-gray-400" /> My Profile
106
  </Link>
107
- <Link
108
- to="/docs"
109
- onClick={() => setIsMenuOpen(false)}
110
- className="flex items-center gap-3 text-lg font-bold text-gray-700 hover:text-emerald-700 transition-colors"
111
- >
112
- <BookOpen size={20} className="text-gray-400" /> Documentation
113
- </Link>
114
  </div>
115
 
116
- <div className="pt-6 border-t border-gray-50">
117
  <button
118
  onClick={handleLogout}
119
- className="w-full py-4 bg-gray-50 rounded-2xl flex items-center justify-center gap-2 text-red-600 font-bold active:scale-95 transition-all"
120
  >
121
  <LogOut size={18} /> Sign Out
122
  </button>
@@ -128,4 +132,4 @@ const TopNav = () => {
128
  );
129
  };
130
 
131
- export default TopNav;
 
1
+ import React, { useState, useEffect } from 'react';
2
+ import { Bell, Box, CheckCircle2, MessageSquare, Menu, X, LogOut, User as UserIcon, Moon, Sun } from 'lucide-react';
3
  import { Link, useNavigate } from 'react-router-dom';
4
  import { Avatar, AvatarFallback, AvatarImage } from "../../components/ui/avatar";
5
  import { Button } from "../../components/ui/button";
 
8
  import useTicketStore from "../../store/ticketStore";
9
  // removed useNotificationStore
10
  import NotificationPopover from "./NotificationPopover";
 
11
  import useAuthStore from "../../store/authStore";
12
 
13
  const TopNav = () => {
14
  const navigate = useNavigate();
 
 
15
  const { profile, logout } = useAuthStore();
16
  const [isMenuOpen, setIsMenuOpen] = useState(false);
17
+ const [isDark, setIsDark] = useState(false);
18
+
19
+ useEffect(() => {
20
+ const saved = localStorage.getItem('theme');
21
+ if (saved === 'dark') {
22
+ document.documentElement.classList.add('dark');
23
+ setIsDark(true);
24
+ }
25
+ }, []);
26
+
27
+ const toggleDark = () => {
28
+ if (isDark) {
29
+ document.documentElement.classList.remove('dark');
30
+ localStorage.setItem('theme', 'light');
31
+ } else {
32
+ document.documentElement.classList.add('dark');
33
+ localStorage.setItem('theme', 'dark');
34
+ }
35
+ setIsDark(!isDark);
36
+ };
37
 
38
  const initials = profile?.full_name ? profile.full_name[0].toUpperCase() : (profile?.email ? profile.email[0].toUpperCase() : 'U');
39
 
 
42
  navigate('/login');
43
  };
44
 
 
 
45
  return (
46
+ <header className="w-full bg-white dark:bg-gray-900 border-b border-gray-200 dark:border-gray-700 sticky top-0 z-50">
47
  <div className="max-w-[1100px] mx-auto px-4 md:px-6 h-16 flex items-center justify-between">
48
  {/* Left: Logo */}
49
  <div className="flex items-center gap-3 cursor-pointer" onClick={() => navigate('/dashboard')}>
50
  <div className="flex items-center justify-center overflow-hidden">
51
  <img src="/favicon.png" alt="HELPDESK.AI Logo" className="w-7 h-7 object-contain" />
52
  </div>
53
+ <h1 className="text-xl font-black tracking-tighter text-gray-900 dark:text-white italic">HELPDESK.AI</h1>
54
  </div>
55
 
56
  {/* Center: Navigation Links */}
57
  <nav className="hidden md:flex items-center gap-8">
58
+ <Link className="text-sm font-semibold text-gray-900 dark:text-gray-100 hover:text-emerald-600 transition-colors" to="/dashboard">Dashboard</Link>
59
+ <Link className="text-sm font-semibold text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white transition-colors" to="/my-tickets">My Tickets</Link>
60
+ <Link className="text-sm font-semibold text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white transition-colors" to="/help">Help</Link>
 
61
  </nav>
62
 
63
  {/* Right: Profile */}
64
  <div className="flex items-center gap-3">
65
+ {/* Dark Mode Toggle */}
66
+ <button
67
+ onClick={toggleDark}
68
+ className="p-2 rounded-full hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
69
+ aria-label="Toggle dark mode"
70
+ >
71
+ {isDark ? <Sun size={20} className="text-yellow-400" /> : <Moon size={20} className="text-gray-600" />}
72
+ </button>
73
+
74
  <NotificationPopover />
75
  <div className="hidden md:block">
76
  <Avatar
 
83
  </div>
84
  <button
85
  onClick={() => setIsMenuOpen(!isMenuOpen)}
86
+ className="md:hidden p-2 text-gray-600 dark:text-gray-300 focus:outline-none"
87
  >
88
  {isMenuOpen ? <X size={24} /> : <Menu size={24} />}
89
  </button>
 
92
 
93
  {/* Mobile Menu Overlay */}
94
  {isMenuOpen && (
95
+ <div className="md:hidden bg-white dark:bg-gray-900 border-t border-gray-100 dark:border-gray-700 absolute w-full shadow-2xl z-50 animate-in fade-in slide-in-from-top-2 duration-200">
96
  <div className="px-6 py-8 space-y-6">
97
+ <div className="flex items-center gap-4 border-b border-gray-50 dark:border-gray-700 pb-6">
98
  <Avatar className="size-12 border border-gray-100">
99
  <AvatarImage src={profile?.profile_picture} />
100
  <AvatarFallback className="bg-emerald-50 text-emerald-700 font-black">{initials}</AvatarFallback>
101
  </Avatar>
102
  <div>
103
+ <p className="font-bold text-gray-900 dark:text-white">{profile?.full_name}</p>
104
  <p className="text-xs text-gray-400 font-medium">{profile?.email}</p>
105
  </div>
106
  </div>
107
 
108
  <div className="space-y-4">
109
+ <Link to="/dashboard" onClick={() => setIsMenuOpen(false)} className="flex items-center gap-3 text-lg font-bold text-gray-700 dark:text-gray-200 hover:text-emerald-700 transition-colors">
 
 
 
 
110
  <Box size={20} className="text-gray-400" /> Dashboard
111
  </Link>
112
+ <Link to="/my-tickets" onClick={() => setIsMenuOpen(false)} className="flex items-center gap-3 text-lg font-bold text-gray-700 dark:text-gray-200 hover:text-emerald-700 transition-colors">
 
 
 
 
113
  <MessageSquare size={20} className="text-gray-400" /> My Tickets
114
  </Link>
115
+ <Link to="/profile" onClick={() => setIsMenuOpen(false)} className="flex items-center gap-3 text-lg font-bold text-gray-700 dark:text-gray-200 hover:text-emerald-700 transition-colors">
 
 
 
 
116
  <UserIcon size={20} className="text-gray-400" /> My Profile
117
  </Link>
 
 
 
 
 
 
 
118
  </div>
119
 
120
+ <div className="pt-6 border-t border-gray-50 dark:border-gray-700">
121
  <button
122
  onClick={handleLogout}
123
+ className="w-full py-4 bg-gray-50 dark:bg-gray-800 rounded-2xl flex items-center justify-center gap-2 text-red-600 font-bold active:scale-95 transition-all"
124
  >
125
  <LogOut size={18} /> Sign Out
126
  </button>
 
132
  );
133
  };
134
 
135
+ export default TopNav;
Frontend/src/user/pages/AIProcessing.jsx CHANGED
@@ -24,7 +24,7 @@ const steps = [
24
  const AIProcessing = () => {
25
  const navigate = useNavigate();
26
  const location = useLocation();
27
- const { text, image_text, image_base64 } = location.state || {};
28
  const setAITicket = useTicketStore((state) => state.setAITicket);
29
  const { settings } = useAdminStore();
30
  const { user, profile } = useAuthStore();
@@ -112,9 +112,15 @@ const AIProcessing = () => {
112
  profile?.company ||
113
  user?.user_metadata?.company ||
114
  "System",
 
115
  image_url: uploadedImageUrl,
116
  confidence_threshold: settings.aiConfidenceThreshold,
117
- duplicate_sensitivity: settings.duplicateSensitivity
 
 
 
 
 
118
  };
119
 
120
  const response = await fetch(
@@ -270,10 +276,10 @@ const AIProcessing = () => {
270
  const aiTicketObject = {
271
  ...finalTicket,
272
  status: 'analyzing',
273
- originalIssue: text,
 
274
  capturedFileBase64: image_base64,
275
- ocrText: image_text,
276
- image_url: uploadedImageUrl || finalTicket.image_url
277
  };
278
 
279
  setAITicket(aiTicketObject);
@@ -343,10 +349,10 @@ const AIProcessing = () => {
343
 
344
  ocr_text: image_text || "",
345
  highlights: [],
346
- originalIssue: text,
 
347
  capturedFileBase64: image_base64,
348
- ocrText: image_text,
349
- image_url: uploadedImageUrl
350
  };
351
 
352
  setAITicket(fallbackTicket);
 
24
  const AIProcessing = () => {
25
  const navigate = useNavigate();
26
  const location = useLocation();
27
+ const { text, image_text, image_base64, template_id, template_used, user_modified, ticket_title, original_text, original_language } = location.state || {};
28
  const setAITicket = useTicketStore((state) => state.setAITicket);
29
  const { settings } = useAdminStore();
30
  const { user, profile } = useAuthStore();
 
112
  profile?.company ||
113
  user?.user_metadata?.company ||
114
  "System",
115
+ company_id: profile?.company_id || null,
116
  image_url: uploadedImageUrl,
117
  confidence_threshold: settings.aiConfidenceThreshold,
118
+ duplicate_sensitivity: settings.duplicateSensitivity,
119
+ // Smart Template metadata (backend can use for improved routing)
120
+ template_id: template_id || null,
121
+ template_used: template_used || false,
122
+ user_modified: user_modified || false,
123
+ ticket_title: ticket_title || null,
124
  };
125
 
126
  const response = await fetch(
 
276
  const aiTicketObject = {
277
  ...finalTicket,
278
  status: 'analyzing',
279
+ originalIssue: original_text || text,
280
+ originalLanguage: original_language || 'en',
281
  capturedFileBase64: image_base64,
282
+ ocrText: image_text
 
283
  };
284
 
285
  setAITicket(aiTicketObject);
 
349
 
350
  ocr_text: image_text || "",
351
  highlights: [],
352
+ originalIssue: original_text || text,
353
+ originalLanguage: original_language || 'en',
354
  capturedFileBase64: image_base64,
355
+ ocrText: image_text
 
356
  };
357
 
358
  setAITicket(fallbackTicket);
Frontend/src/user/pages/AutoResolveChat.jsx CHANGED
@@ -19,6 +19,7 @@ const AutoResolveChat = () => {
19
  const { aiTicket } = useTicketStore();
20
  const navigate = useNavigate();
21
  const [messages, setMessages] = useState([]);
 
22
  const [isThinking, setIsThinking] = useState(false);
23
  const [isFinal, setIsFinal] = useState(false);
24
  const [inputText, setInputText] = useState('');
@@ -77,7 +78,7 @@ const AutoResolveChat = () => {
77
  }
78
 
79
  if (newSteps.length >= 2) {
80
- // Steps parsed successfully; welcome message sent below
81
  } else {
82
  const sentences = response
83
  .replace(/\*\*/g, '')
@@ -87,7 +88,12 @@ const AutoResolveChat = () => {
87
  .slice(0, 4);
88
 
89
  if (sentences.length >= 2) {
90
- // Plan parsed but not rendered; welcome message sent below
 
 
 
 
 
91
  } else {
92
  throw new Error("Could not parse steps from AI response.");
93
  }
@@ -197,6 +203,12 @@ const AutoResolveChat = () => {
197
  else recognition.start();
198
  };
199
 
 
 
 
 
 
 
200
  if (!aiTicket) return null;
201
 
202
  return (
@@ -249,6 +261,48 @@ const AutoResolveChat = () => {
249
  </div>
250
  </div>
251
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
252
  {/* Chat Messages */}
253
  <div
254
  ref={scrollRef}
 
19
  const { aiTicket } = useTicketStore();
20
  const navigate = useNavigate();
21
  const [messages, setMessages] = useState([]);
22
+ const [steps, setSteps] = useState([]);
23
  const [isThinking, setIsThinking] = useState(false);
24
  const [isFinal, setIsFinal] = useState(false);
25
  const [inputText, setInputText] = useState('');
 
78
  }
79
 
80
  if (newSteps.length >= 2) {
81
+ setSteps(newSteps);
82
  } else {
83
  const sentences = response
84
  .replace(/\*\*/g, '')
 
88
  .slice(0, 4);
89
 
90
  if (sentences.length >= 2) {
91
+ const sentenceSteps = sentences.map((s, i) => ({
92
+ id: i + 1,
93
+ task: s,
94
+ completed: false
95
+ }));
96
+ setSteps(sentenceSteps);
97
  } else {
98
  throw new Error("Could not parse steps from AI response.");
99
  }
 
203
  else recognition.start();
204
  };
205
 
206
+ const toggleStep = (stepId) => {
207
+ setSteps(prev => prev.map(step =>
208
+ step.id === stepId ? { ...step, completed: !step.completed } : step
209
+ ));
210
+ };
211
+
212
  if (!aiTicket) return null;
213
 
214
  return (
 
261
  </div>
262
  </div>
263
 
264
+ {/* Troubleshooting Steps */}
265
+ {steps.length > 0 && (
266
+ <div className="px-10 py-6 border-b border-white/40 bg-emerald-50/30">
267
+ <div className="flex items-center gap-2 mb-3">
268
+ <ListChecks size={16} className="text-emerald-600" />
269
+ <h3 className="text-[10px] font-black text-emerald-700 uppercase tracking-[0.2em]">
270
+ Troubleshooting Plan
271
+ </h3>
272
+ </div>
273
+ <div className="space-y-2">
274
+ {steps.map((step) => (
275
+ <button
276
+ key={step.id}
277
+ onClick={() => toggleStep(step.id)}
278
+ className={`w-full text-left flex items-center gap-3 p-3 rounded-xl border transition-all duration-300 ${
279
+ step.completed
280
+ ? 'bg-emerald-100 border-emerald-200 opacity-60'
281
+ : 'bg-white border-slate-100 hover:border-emerald-200 hover:bg-emerald-50/50'
282
+ }`}
283
+ >
284
+ <div className={`w-6 h-6 rounded-full flex items-center justify-center shrink-0 transition-all duration-300 ${
285
+ step.completed
286
+ ? 'bg-emerald-500 text-white'
287
+ : 'bg-slate-100 text-slate-400'
288
+ }`}>
289
+ {step.completed ? (
290
+ <CheckCircle2 size={14} />
291
+ ) : (
292
+ <span className="text-[10px] font-black">{step.id}</span>
293
+ )}
294
+ </div>
295
+ <span className={`text-[13px] font-bold leading-snug transition-all duration-300 ${
296
+ step.completed ? 'text-slate-400 line-through' : 'text-slate-700'
297
+ }`}>
298
+ {step.task}
299
+ </span>
300
+ </button>
301
+ ))}
302
+ </div>
303
+ </div>
304
+ )}
305
+
306
  {/* Chat Messages */}
307
  <div
308
  ref={scrollRef}
Frontend/src/user/pages/CreateTicket.jsx CHANGED
@@ -1,5 +1,5 @@
1
  import React, { useState, useRef, useEffect } from 'react';
2
- import { useNavigate } from 'react-router-dom';
3
  import {
4
  Upload,
5
  X,
@@ -21,11 +21,14 @@ import { motion, AnimatePresence } from 'framer-motion';
21
  import { Button } from "../../components/ui/button";
22
  import { Card, CardHeader, CardTitle, CardDescription, CardContent } from "../../components/ui/card";
23
  import { Textarea } from "../../components/ui/textarea";
24
- import Tesseract from 'tesseract.js';
25
  import { translateText, SUPPORTED_LANGUAGES } from '../../services/translationService';
 
 
 
26
 
27
  const CreateTicket = () => {
28
  const [issue, setIssue] = useState('');
 
29
  const [file, setFile] = useState(null);
30
  const [imagePreview, setImagePreview] = useState(null);
31
  const [isLoading, setIsLoading] = useState(false);
@@ -33,8 +36,10 @@ const CreateTicket = () => {
33
  const [extractedOCR, setExtractedOCR] = useState('');
34
  const [isOcrLoading, setIsOcrLoading] = useState(false);
35
  const [isListening, setIsListening] = useState(false);
 
36
  const fileInputRef = useRef(null);
37
  const navigate = useNavigate();
 
38
  const MAX_CHARS = 1000;
39
  const supportsSpeech = 'SpeechRecognition' in window || 'webkitSpeechRecognition' in window;
40
  const [selectedLanguage, setSelectedLanguage] = useState('en');
@@ -42,6 +47,13 @@ const CreateTicket = () => {
42
  const [isLangOpen, setIsLangOpen] = useState(false);
43
  const langRef = useRef(null);
44
 
 
 
 
 
 
 
 
45
  // Voice UI states
46
  const [showVoiceModal, setShowVoiceModal] = useState(false);
47
  const [voiceTranscript, setVoiceTranscript] = useState('');
@@ -58,12 +70,25 @@ const CreateTicket = () => {
58
 
59
  useEffect(() => {
60
  return () => {
 
61
  if (recognitionRef.current) recognitionRef.current.stop();
62
  if (audioContextRef.current) audioContextRef.current.close();
63
  if (animationFrameRef.current) cancelAnimationFrame(animationFrameRef.current);
64
  };
65
  }, []);
66
 
 
 
 
 
 
 
 
 
 
 
 
 
67
  // Close language dropdown on outside click
68
  useEffect(() => {
69
  const handleClickOutside = (event) => {
@@ -85,6 +110,7 @@ const CreateTicket = () => {
85
  const processOCR = async (imageFile) => {
86
  setIsOcrLoading(true);
87
  try {
 
88
  const { data: { text } } = await Tesseract.recognize(imageFile, 'eng');
89
  setExtractedOCR(text.trim());
90
  } catch (err) {
@@ -165,13 +191,22 @@ const CreateTicket = () => {
165
  console.error("Speech Recognition Error:", event.error);
166
  if (event.error !== 'no-speech') {
167
  setError(`Microphone error: ${event.error}`);
 
168
  }
169
  };
170
 
171
  recognition.onend = () => {
172
- // Only stop visualizer if we actually intended to stop
173
- if (!isListening) {
174
- if (animationFrameRef.current) cancelAnimationFrame(animationFrameRef.current);
 
 
 
 
 
 
 
 
175
  }
176
  };
177
 
@@ -179,6 +214,7 @@ const CreateTicket = () => {
179
  recognition.start();
180
 
181
  setIsListening(true);
 
182
  setShowVoiceModal(true);
183
  setVoiceTranscript('');
184
  setInterimVoice('');
@@ -192,6 +228,7 @@ const CreateTicket = () => {
192
 
193
  const stopListening = () => {
194
  setIsListening(false);
 
195
  if (recognitionRef.current) {
196
  recognitionRef.current.stop();
197
  recognitionRef.current = null;
@@ -263,11 +300,68 @@ const CreateTicket = () => {
263
  }
264
  };
265
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
266
  const handleAnalyze = async (e) => {
267
  e.preventDefault();
268
- if (!issue.trim()) {
269
- setError('Please describe your issue first.');
270
- return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
271
  }
272
 
273
  if (file && !isOcrLoading && !extractedOCR.trim()) {
@@ -279,15 +373,20 @@ const CreateTicket = () => {
279
  setError('');
280
 
281
  try {
282
- let textToSubmit = issue;
283
 
284
  // Translate to English if a different language is selected
285
  if (selectedLanguage !== 'en') {
286
  setIsTranslating(true);
287
- textToSubmit = await translateText(issue, selectedLanguage, 'en');
288
  setIsTranslating(false);
289
  }
290
 
 
 
 
 
 
291
  let imageBase64 = "";
292
  let extractedOCRText = extractedOCR;
293
  if (file) {
@@ -304,10 +403,15 @@ const CreateTicket = () => {
304
  navigate('/ai-processing', {
305
  state: {
306
  text: textToSubmit,
307
- original_text: issue,
308
  original_language: selectedLanguage,
309
  image_base64: imageBase64,
310
- image_text: extractedOCRText
 
 
 
 
 
311
  }
312
  });
313
 
@@ -346,88 +450,133 @@ const CreateTicket = () => {
346
 
347
  <CardContent className="p-8 pt-2 flex-grow flex flex-col">
348
  <form onSubmit={handleAnalyze} className="space-y-6 flex-grow flex flex-col">
349
- {/* Description Textarea */}
350
- <div className="space-y-2 flex-grow flex flex-col relative">
351
- <div className="flex items-center justify-between">
352
- <label className="text-sm font-bold text-gray-700">Describe your issue</label>
353
- <span className={`text-xs font-semibold ${issue.length >= MAX_CHARS ? 'text-red-500' : 'text-gray-400'}`}>
354
- {issue.length} / {MAX_CHARS}
355
- </span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
356
  </div>
 
 
 
 
 
 
 
 
 
357
 
358
- {/* Premium Language Selector */}
359
- <div className="flex items-center gap-2">
360
- <label className="text-xs font-bold text-gray-500 uppercase tracking-wider shrink-0">Language:</label>
361
- <div className="relative flex-1" ref={langRef}>
362
- <button
363
- type="button"
364
- onClick={() => setIsLangOpen(!isLangOpen)}
365
- className="w-full bg-gray-50 border border-gray-100 rounded-xl px-4 py-2.5 text-sm font-bold text-gray-700 flex items-center justify-between hover:bg-white hover:border-emerald-200 transition-all shadow-sm group"
366
- >
367
- <span className="flex items-center gap-2">
368
- <Globe size={14} className="text-emerald-500" />
369
- {SUPPORTED_LANGUAGES.find(l => l.code === selectedLanguage)?.label}
370
- </span>
371
- <motion.div
372
- animate={{ rotate: isLangOpen ? 180 : 0 }}
373
- className="text-gray-400 group-hover:text-emerald-500 transition-colors"
374
  >
375
- <ChevronDown size={16} />
376
- </motion.div>
377
- </button>
378
-
379
- <AnimatePresence>
380
- {isLangOpen && (
381
  <motion.div
382
- initial={{ opacity: 0, y: 10, scale: 0.95 }}
383
- animate={{ opacity: 1, y: 5, scale: 1 }}
384
- exit={{ opacity: 0, y: 10, scale: 0.95 }}
385
- className="absolute z-50 top-full left-0 right-0 bg-white border border-gray-100 rounded-2xl shadow-2xl shadow-emerald-900/10 p-2 overflow-hidden"
386
  >
387
- <div className="max-h-[220px] overflow-y-auto custom-scrollbar space-y-1">
388
- {SUPPORTED_LANGUAGES.map(lang => (
389
- <button
390
- key={lang.code}
391
- type="button"
392
- onClick={() => {
393
- setSelectedLanguage(lang.code);
394
- setIsLangOpen(false);
395
- }}
396
- className={`w-full text-left px-4 py-2.5 rounded-xl text-sm font-bold transition-all flex items-center justify-between
397
- ${selectedLanguage === lang.code
398
- ? 'bg-emerald-50 text-emerald-700'
399
- : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'
400
- }`}
401
- >
402
- {lang.label}
403
- {selectedLanguage === lang.code && <CheckCircle2 size={14} className="text-emerald-500" />}
404
- </button>
405
- ))}
406
- </div>
407
  </motion.div>
408
- )}
409
- </AnimatePresence>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
410
  </div>
411
- {selectedLanguage !== 'en' && (
412
- <motion.span
413
- initial={{ opacity: 0, x: -10 }}
414
- animate={{ opacity: 1, x: 0 }}
415
- className="text-[10px] bg-emerald-500 text-white px-2.5 py-1 rounded-full font-black uppercase tracking-widest shadow-lg shadow-emerald-200"
416
- >
417
- Translating
418
- </motion.span>
419
- )}
420
- </div>
421
- <div className="relative flex-grow flex flex-col">
422
- <Textarea
423
- value={issue}
424
- onChange={(e) => setIssue(e.target.value.substring(0, MAX_CHARS))}
425
- placeholder="Describe your problem. Example: VPN not connecting error 789"
426
- className="min-h-[160px] flex-grow rounded-2xl border-gray-100 bg-gray-50/50 focus:bg-white transition-all text-base p-4 resize-none"
427
- disabled={isLoading}
428
- />
429
  </div>
430
- </div>
431
 
432
  {/* Premium Voice Visualizer */}
433
  {supportsSpeech && (
@@ -571,7 +720,7 @@ const CreateTicket = () => {
571
  {/* Primary Submit Button */}
572
  <Button
573
  type="submit"
574
- disabled={isLoading || isOcrLoading || isTranslating || !issue.trim()}
575
  className="w-full h-14 bg-emerald-500 hover:bg-emerald-600 text-white font-bold text-lg rounded-2xl flex items-center justify-center gap-2 transition-all border-none shadow-emerald-200/50 shadow-lg hover:shadow-xl active:scale-[0.98] disabled:opacity-50"
576
  >
577
  {(isLoading || isTranslating) ? (
 
1
  import React, { useState, useRef, useEffect } from 'react';
2
+ import { useNavigate, useLocation } from 'react-router-dom';
3
  import {
4
  Upload,
5
  X,
 
21
  import { Button } from "../../components/ui/button";
22
  import { Card, CardHeader, CardTitle, CardDescription, CardContent } from "../../components/ui/card";
23
  import { Textarea } from "../../components/ui/textarea";
 
24
  import { translateText, SUPPORTED_LANGUAGES } from '../../services/translationService';
25
+ import TemplateSelector from '../components/TemplateSelector';
26
+ import TemplateForm from '../components/TemplateForm';
27
+ import TICKET_TEMPLATES, { serializeFieldsToText, getEmptyFormValues } from '../../data/ticketTemplates';
28
 
29
  const CreateTicket = () => {
30
  const [issue, setIssue] = useState('');
31
+ const [ticketTitle, setTicketTitle] = useState('');
32
  const [file, setFile] = useState(null);
33
  const [imagePreview, setImagePreview] = useState(null);
34
  const [isLoading, setIsLoading] = useState(false);
 
36
  const [extractedOCR, setExtractedOCR] = useState('');
37
  const [isOcrLoading, setIsOcrLoading] = useState(false);
38
  const [isListening, setIsListening] = useState(false);
39
+ const isListeningRef = useRef(false);
40
  const fileInputRef = useRef(null);
41
  const navigate = useNavigate();
42
+ const location = useLocation();
43
  const MAX_CHARS = 1000;
44
  const supportsSpeech = 'SpeechRecognition' in window || 'webkitSpeechRecognition' in window;
45
  const [selectedLanguage, setSelectedLanguage] = useState('en');
 
47
  const [isLangOpen, setIsLangOpen] = useState(false);
48
  const langRef = useRef(null);
49
 
50
+ // ── Smart Template state (v2: two-step highlight → activate flow) ──
51
+ const [highlightedTemplateId, setHighlightedTemplateId] = useState(null); // Card highlighted (preview)
52
+ const [activatedTemplateId, setActivatedTemplateId] = useState(null); // Template committed (form shown)
53
+ const [formValues, setFormValues] = useState({}); // Dynamic form field values
54
+ const [templateUsed, setTemplateUsed] = useState(false);
55
+ const [userModified, setUserModified] = useState(false);
56
+
57
  // Voice UI states
58
  const [showVoiceModal, setShowVoiceModal] = useState(false);
59
  const [voiceTranscript, setVoiceTranscript] = useState('');
 
70
 
71
  useEffect(() => {
72
  return () => {
73
+ isListeningRef.current = false;
74
  if (recognitionRef.current) recognitionRef.current.stop();
75
  if (audioContextRef.current) audioContextRef.current.close();
76
  if (animationFrameRef.current) cancelAnimationFrame(animationFrameRef.current);
77
  };
78
  }, []);
79
 
80
+ // Pre-select template if navigated from QuickActions or Dashboard with a templateId
81
+ // v2: Now highlights the template so user sees the preview first
82
+ useEffect(() => {
83
+ const incomingTemplateId = location.state?.templateId;
84
+ if (incomingTemplateId) {
85
+ const template = TICKET_TEMPLATES.find((t) => t.id === incomingTemplateId);
86
+ if (template) {
87
+ handleHighlightTemplate(template);
88
+ }
89
+ }
90
+ }, []);
91
+
92
  // Close language dropdown on outside click
93
  useEffect(() => {
94
  const handleClickOutside = (event) => {
 
110
  const processOCR = async (imageFile) => {
111
  setIsOcrLoading(true);
112
  try {
113
+ const { default: Tesseract } = await import('tesseract.js');
114
  const { data: { text } } = await Tesseract.recognize(imageFile, 'eng');
115
  setExtractedOCR(text.trim());
116
  } catch (err) {
 
191
  console.error("Speech Recognition Error:", event.error);
192
  if (event.error !== 'no-speech') {
193
  setError(`Microphone error: ${event.error}`);
194
+ stopListening();
195
  }
196
  };
197
 
198
  recognition.onend = () => {
199
+ if (isListeningRef.current) {
200
+ try {
201
+ recognitionRef.current?.start();
202
+ } catch {
203
+ // start() may throw if already running — safely ignore
204
+ }
205
+ } else {
206
+ if (animationFrameRef.current) {
207
+ cancelAnimationFrame(animationFrameRef.current);
208
+ animationFrameRef.current = null;
209
+ }
210
  }
211
  };
212
 
 
214
  recognition.start();
215
 
216
  setIsListening(true);
217
+ isListeningRef.current = true;
218
  setShowVoiceModal(true);
219
  setVoiceTranscript('');
220
  setInterimVoice('');
 
228
 
229
  const stopListening = () => {
230
  setIsListening(false);
231
+ isListeningRef.current = false;
232
  if (recognitionRef.current) {
233
  recognitionRef.current.stop();
234
  recognitionRef.current = null;
 
300
  }
301
  };
302
 
303
+ // ── Smart Template handlers (v2: highlight → activate → dismiss) ──
304
+
305
+ /** Step 1: Highlight a template card (shows preview, does NOT apply) */
306
+ const handleHighlightTemplate = (template) => {
307
+ setHighlightedTemplateId(template ? template.id : null);
308
+ setError('');
309
+ };
310
+
311
+ /** Step 2: User explicitly confirms — activate template and show dynamic form */
312
+ const handleActivateTemplate = (template) => {
313
+ setActivatedTemplateId(template.id);
314
+ setHighlightedTemplateId(null);
315
+ setTicketTitle(template.title);
316
+ setFormValues(getEmptyFormValues(template.fields));
317
+ setIssue(''); // Clear manual textarea content
318
+ setTemplateUsed(true);
319
+ setUserModified(false);
320
+ setError('');
321
+ };
322
+
323
+ /** Dismiss: clear template and restore manual mode */
324
+ const handleDismissTemplate = () => {
325
+ setActivatedTemplateId(null);
326
+ setHighlightedTemplateId(null);
327
+ setTicketTitle('');
328
+ setIssue('');
329
+ setFormValues({});
330
+ setTemplateUsed(false);
331
+ setUserModified(false);
332
+ };
333
+
334
+ /** Handle individual field changes in the dynamic form */
335
+ const handleFormFieldChange = (key, value) => {
336
+ setFormValues((prev) => ({ ...prev, [key]: value }));
337
+ if (templateUsed) setUserModified(true);
338
+ };
339
+
340
  const handleAnalyze = async (e) => {
341
  e.preventDefault();
342
+
343
+ // ── v2: Determine description text based on active mode ──
344
+ const activeTemplate = TICKET_TEMPLATES.find((t) => t.id === activatedTemplateId);
345
+ let descriptionText = issue; // Default: manual textarea content
346
+
347
+ if (activeTemplate) {
348
+ // Template mode: serialize structured form fields to text
349
+ descriptionText = serializeFieldsToText(activeTemplate.fields, formValues);
350
+
351
+ // Validate required fields
352
+ const missingFields = activeTemplate.fields
353
+ .filter((f) => f.required && !formValues[f.key] && formValues[f.key] !== false)
354
+ .map((f) => f.label);
355
+ if (missingFields.length > 0) {
356
+ setError(`Please fill in required fields: ${missingFields.join(', ')}`);
357
+ return;
358
+ }
359
+ } else {
360
+ // Manual mode: validate textarea
361
+ if (!issue.trim()) {
362
+ setError('Please describe your issue first.');
363
+ return;
364
+ }
365
  }
366
 
367
  if (file && !isOcrLoading && !extractedOCR.trim()) {
 
373
  setError('');
374
 
375
  try {
376
+ let textToSubmit = descriptionText;
377
 
378
  // Translate to English if a different language is selected
379
  if (selectedLanguage !== 'en') {
380
  setIsTranslating(true);
381
+ textToSubmit = await translateText(descriptionText, selectedLanguage, 'en');
382
  setIsTranslating(false);
383
  }
384
 
385
+ // If a title was provided, prepend it to the text for richer AI context
386
+ if (ticketTitle.trim()) {
387
+ textToSubmit = `${ticketTitle.trim()}\n\n${textToSubmit}`;
388
+ }
389
+
390
  let imageBase64 = "";
391
  let extractedOCRText = extractedOCR;
392
  if (file) {
 
403
  navigate('/ai-processing', {
404
  state: {
405
  text: textToSubmit,
406
+ original_text: descriptionText,
407
  original_language: selectedLanguage,
408
  image_base64: imageBase64,
409
+ image_text: extractedOCRText,
410
+ // Smart Template metadata
411
+ template_id: activatedTemplateId || null,
412
+ template_used: templateUsed,
413
+ user_modified: userModified,
414
+ ticket_title: ticketTitle.trim() || null,
415
  }
416
  });
417
 
 
450
 
451
  <CardContent className="p-8 pt-2 flex-grow flex flex-col">
452
  <form onSubmit={handleAnalyze} className="space-y-6 flex-grow flex flex-col">
453
+
454
+ {/* ── Smart Ticket Templates (v2: two-step selection) ── */}
455
+ <TemplateSelector
456
+ selectedTemplateId={highlightedTemplateId}
457
+ activatedTemplateId={activatedTemplateId}
458
+ onHighlightTemplate={handleHighlightTemplate}
459
+ onActivateTemplate={handleActivateTemplate}
460
+ onDismissTemplate={handleDismissTemplate}
461
+ hasExistingContent={!!(issue.trim() || ticketTitle.trim())}
462
+ />
463
+
464
+ {/* Title Field */}
465
+ <div className="space-y-2">
466
+ <label className="text-sm font-bold text-gray-700">Title</label>
467
+ <input
468
+ type="text"
469
+ value={ticketTitle}
470
+ onChange={(e) => {
471
+ setTicketTitle(e.target.value);
472
+ if (templateUsed) setUserModified(true);
473
+ }}
474
+ placeholder="Brief summary of your issue"
475
+ className="w-full rounded-2xl border border-gray-100 bg-gray-50/50 focus:bg-white focus:border-emerald-300 focus:ring-2 focus:ring-emerald-100 transition-all text-base p-4 outline-none"
476
+ disabled={isLoading}
477
+ maxLength={200}
478
+ />
479
+ </div>
480
+
481
+ {/* ── v2: Conditional rendering — TemplateForm OR manual textarea ── */}
482
+ {activatedTemplateId ? (
483
+ /* Template mode: show structured dynamic form */
484
+ <div className="space-y-2">
485
+ <label className="text-sm font-bold text-gray-700">Fill in the details</label>
486
+ <TemplateForm
487
+ fields={TICKET_TEMPLATES.find(t => t.id === activatedTemplateId)?.fields || []}
488
+ values={formValues}
489
+ onChange={handleFormFieldChange}
490
+ disabled={isLoading}
491
+ />
492
  </div>
493
+ ) : (
494
+ /* Manual mode: original textarea with language selector */
495
+ <div className="space-y-2 flex-grow flex flex-col relative">
496
+ <div className="flex items-center justify-between">
497
+ <label className="text-sm font-bold text-gray-700">Describe your issue</label>
498
+ <span className={`text-xs font-semibold ${issue.length >= MAX_CHARS ? 'text-red-500' : 'text-gray-400'}`}>
499
+ {issue.length} / {MAX_CHARS}
500
+ </span>
501
+ </div>
502
 
503
+ {/* Premium Language Selector */}
504
+ <div className="flex items-center gap-2">
505
+ <label className="text-xs font-bold text-gray-500 uppercase tracking-wider shrink-0">Language:</label>
506
+ <div className="relative flex-1" ref={langRef}>
507
+ <button
508
+ type="button"
509
+ onClick={() => setIsLangOpen(!isLangOpen)}
510
+ className="w-full bg-gray-50 border border-gray-100 rounded-xl px-4 py-2.5 text-sm font-bold text-gray-700 flex items-center justify-between hover:bg-white hover:border-emerald-200 transition-all shadow-sm group"
 
 
 
 
 
 
 
 
511
  >
512
+ <span className="flex items-center gap-2">
513
+ <Globe size={14} className="text-emerald-500" />
514
+ {SUPPORTED_LANGUAGES.find(l => l.code === selectedLanguage)?.label}
515
+ </span>
 
 
516
  <motion.div
517
+ animate={{ rotate: isLangOpen ? 180 : 0 }}
518
+ className="text-gray-400 group-hover:text-emerald-500 transition-colors"
 
 
519
  >
520
+ <ChevronDown size={16} />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
521
  </motion.div>
522
+ </button>
523
+
524
+ <AnimatePresence>
525
+ {isLangOpen && (
526
+ <motion.div
527
+ initial={{ opacity: 0, y: 10, scale: 0.95 }}
528
+ animate={{ opacity: 1, y: 5, scale: 1 }}
529
+ exit={{ opacity: 0, y: 10, scale: 0.95 }}
530
+ className="absolute z-50 top-full left-0 right-0 bg-white border border-gray-100 rounded-2xl shadow-2xl shadow-emerald-900/10 p-2 overflow-hidden"
531
+ >
532
+ <div className="max-h-[220px] overflow-y-auto custom-scrollbar space-y-1">
533
+ {SUPPORTED_LANGUAGES.map(lang => (
534
+ <button
535
+ key={lang.code}
536
+ type="button"
537
+ onClick={() => {
538
+ setSelectedLanguage(lang.code);
539
+ setIsLangOpen(false);
540
+ }}
541
+ className={`w-full text-left px-4 py-2.5 rounded-xl text-sm font-bold transition-all flex items-center justify-between
542
+ ${selectedLanguage === lang.code
543
+ ? 'bg-emerald-50 text-emerald-700'
544
+ : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'
545
+ }`}
546
+ >
547
+ {lang.label}
548
+ {selectedLanguage === lang.code && <CheckCircle2 size={14} className="text-emerald-500" />}
549
+ </button>
550
+ ))}
551
+ </div>
552
+ </motion.div>
553
+ )}
554
+ </AnimatePresence>
555
+ </div>
556
+ {selectedLanguage !== 'en' && (
557
+ <motion.span
558
+ initial={{ opacity: 0, x: -10 }}
559
+ animate={{ opacity: 1, x: 0 }}
560
+ className="text-[10px] bg-emerald-500 text-white px-2.5 py-1 rounded-full font-black uppercase tracking-widest shadow-lg shadow-emerald-200"
561
+ >
562
+ Translating
563
+ </motion.span>
564
+ )}
565
+ </div>
566
+ <div className="relative flex-grow flex flex-col">
567
+ <Textarea
568
+ value={issue}
569
+ onChange={(e) => {
570
+ setIssue(e.target.value.substring(0, MAX_CHARS));
571
+ if (templateUsed) setUserModified(true);
572
+ }}
573
+ placeholder="Describe your problem. Example: VPN not connecting error 789"
574
+ className="min-h-[160px] flex-grow rounded-2xl border-gray-100 bg-gray-50/50 focus:bg-white transition-all text-base p-4 resize-none"
575
+ disabled={isLoading}
576
+ />
577
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
578
  </div>
579
+ )}
580
 
581
  {/* Premium Voice Visualizer */}
582
  {supportsSpeech && (
 
720
  {/* Primary Submit Button */}
721
  <Button
722
  type="submit"
723
+ disabled={isLoading || isOcrLoading || isTranslating || (!activatedTemplateId && !issue.trim())}
724
  className="w-full h-14 bg-emerald-500 hover:bg-emerald-600 text-white font-bold text-lg rounded-2xl flex items-center justify-center gap-2 transition-all border-none shadow-emerald-200/50 shadow-lg hover:shadow-xl active:scale-[0.98] disabled:opacity-50"
725
  >
726
  {(isLoading || isTranslating) ? (
Frontend/src/user/pages/Dashboard.jsx CHANGED
@@ -42,9 +42,10 @@ const Dashboard = () => {
42
  <RecentTickets />
43
  </section>
44
 
 
45
  <footer className="mt-8 text-center">
46
  <p className="text-xs text-gray-400 font-medium tracking-tight">
47
- &copy; {new Date().getFullYear()} {profile?.company || 'Emerald Helpdesk'}. All systems operational.
48
  </p>
49
  </footer>
50
  </div>
 
42
  <RecentTickets />
43
  </section>
44
 
45
+ {/* Footer Info */}
46
  <footer className="mt-8 text-center">
47
  <p className="text-xs text-gray-400 font-medium tracking-tight">
48
+ &copy; 2026 Emerald Helpdesk. All systems operational.
49
  </p>
50
  </footer>
51
  </div>
Frontend/src/user/pages/DuplicateDetection.jsx CHANGED
@@ -3,11 +3,17 @@ import { useNavigate } from 'react-router-dom';
3
  import {
4
  FileText, Database, Zap,
5
  CheckCircle2, AlertTriangle, ArrowRight,
6
- Lightbulb, SearchX, TicketCheck, Search
7
  } from 'lucide-react';
8
  import useTicketStore from "../../store/ticketStore";
9
  import { API_CONFIG } from "../../config";
10
- import { supabase } from "../../lib/supabaseClient";
 
 
 
 
 
 
11
 
12
  // ─── Shimmer skeleton ─────────────────────────────────────────────────────────
13
  const Shimmer = ({ className = '' }) => (
@@ -34,33 +40,6 @@ const DuplicateDetection = () => {
34
  const [isLoading, setIsLoading] = useState(true);
35
  const [activeStep, setActiveStep] = useState(0);
36
  const [countdown, setCountdown] = useState(2);
37
- const [totalCases, setTotalCases] = useState('10,000+');
38
-
39
- // ─── Animated Step Pipeline (Dynamic) ───────────────────────────────────────────
40
- const pipelineSteps = [
41
- { icon: FileText, label: 'Your Issue', desc: 'Captured & analysed' },
42
- { icon: Database, label: 'Case History', desc: `Scanned ${totalCases} cases` },
43
- { icon: Zap, label: 'Match Found', desc: 'Similarity calculated' },
44
- ];
45
-
46
- // Fetch dynamic case count from Supabase database
47
- useEffect(() => {
48
- const fetchCaseCount = async () => {
49
- try {
50
- if (supabase) {
51
- const { count, error } = await supabase
52
- .from('tickets')
53
- .select('id', { count: 'exact', head: true });
54
- if (!error && count != null) {
55
- setTotalCases(count.toLocaleString());
56
- }
57
- }
58
- } catch (err) {
59
- console.warn("[DuplicateDetection] Failed to fetch live case count fallback to default:", err);
60
- }
61
- };
62
- fetchCaseCount();
63
- }, []);
64
 
65
  // Loading delay + step animation
66
  useEffect(() => {
@@ -75,6 +54,7 @@ const DuplicateDetection = () => {
75
  }, [isLoading, aiTicket, navigate]);
76
 
77
  const isDuplicate = aiTicket?.duplicate_ticket?.is_duplicate === true;
 
78
 
79
  // Auto-redirect when no duplicate
80
  useEffect(() => {
@@ -208,8 +188,45 @@ const DuplicateDetection = () => {
208
  {/* ── Match / No-match result card ─────────────────────── */}
209
  {isDuplicate ? (
210
  <div className="bg-white rounded-3xl border border-slate-100 shadow-xl shadow-slate-200/40 overflow-hidden">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
211
  {/* Card header */}
212
- <div className="px-8 py-6 bg-amber-50 border-b border-amber-100 flex items-start gap-4">
213
  <div className="p-2.5 bg-amber-100 rounded-xl shrink-0">
214
  <AlertTriangle className="w-5 h-5 text-amber-600" />
215
  </div>
 
3
  import {
4
  FileText, Database, Zap,
5
  CheckCircle2, AlertTriangle, ArrowRight,
6
+ Lightbulb, SearchX, TicketCheck, Search, Bell, Link2
7
  } from 'lucide-react';
8
  import useTicketStore from "../../store/ticketStore";
9
  import { API_CONFIG } from "../../config";
10
+
11
+ // ─── Animated Step Pipeline ───────────────────────────────────────────────────
12
+ const pipelineSteps = [
13
+ { icon: FileText, label: 'Your Issue', desc: 'Captured & analysed' },
14
+ { icon: Database, label: 'Case History', desc: 'Scanned 10 000+ cases' },
15
+ { icon: Zap, label: 'Match Found', desc: 'Similarity calculated' },
16
+ ];
17
 
18
  // ─── Shimmer skeleton ─────────────────────────────────────────────────────────
19
  const Shimmer = ({ className = '' }) => (
 
40
  const [isLoading, setIsLoading] = useState(true);
41
  const [activeStep, setActiveStep] = useState(0);
42
  const [countdown, setCountdown] = useState(2);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
 
44
  // Loading delay + step animation
45
  useEffect(() => {
 
54
  }, [isLoading, aiTicket, navigate]);
55
 
56
  const isDuplicate = aiTicket?.duplicate_ticket?.is_duplicate === true;
57
+ const duplicateParentTicketId = aiTicket?.parent_ticket_id || aiTicket?.duplicate_ticket?.parent_ticket_id || aiTicket?.duplicate_ticket?.duplicate_ticket_id;
58
 
59
  // Auto-redirect when no duplicate
60
  useEffect(() => {
 
188
  {/* ── Match / No-match result card ─────────────────────── */}
189
  {isDuplicate ? (
190
  <div className="bg-white rounded-3xl border border-slate-100 shadow-xl shadow-slate-200/40 overflow-hidden">
191
+ <div className="px-8 pt-8">
192
+ <div className="relative overflow-hidden rounded-3xl border border-amber-200 bg-gradient-to-br from-amber-50 via-white to-orange-50 p-6 shadow-[0_20px_60px_-24px_rgba(217,119,6,0.45)]">
193
+ <div className="absolute -right-10 -top-10 h-32 w-32 rounded-full bg-amber-300/20 blur-3xl" />
194
+ <div className="relative flex items-start gap-4">
195
+ <div className="rounded-2xl bg-amber-100 p-3 shadow-sm">
196
+ <Bell className="h-6 w-6 text-amber-700" />
197
+ </div>
198
+ <div className="flex-1">
199
+ <p className="text-[11px] font-black uppercase tracking-[0.22em] text-amber-700">Potential Duplicate</p>
200
+ <h2 className="mt-1 text-xl font-black text-slate-900 tracking-tight">
201
+ A similar issue was recently reported by your teammate.
202
+ </h2>
203
+ <p className="mt-2 max-w-2xl text-sm font-medium leading-6 text-slate-600">
204
+ Would you like to subscribe to updates on the existing ticket instead of creating a duplicate? You can open the parent ticket now or continue anyway if this is a new incident.
205
+ </p>
206
+ <div className="mt-5 flex flex-col gap-3 sm:flex-row">
207
+ {duplicateParentTicketId ? (
208
+ <button
209
+ onClick={() => navigate(`/ticket/${duplicateParentTicketId}`)}
210
+ className="inline-flex items-center justify-center gap-2 rounded-2xl bg-slate-900 px-5 py-3 text-sm font-black uppercase tracking-widest text-white transition-transform hover:scale-[1.01]"
211
+ >
212
+ <Link2 className="h-4 w-4" />
213
+ Open Existing Ticket
214
+ </button>
215
+ ) : null}
216
+ <button
217
+ onClick={handleCreateTicket}
218
+ className="inline-flex items-center justify-center gap-2 rounded-2xl border border-amber-200 bg-white px-5 py-3 text-sm font-black uppercase tracking-widest text-amber-900 transition-transform hover:scale-[1.01]"
219
+ >
220
+ Create Anyway
221
+ </button>
222
+ </div>
223
+ </div>
224
+ </div>
225
+ </div>
226
+ </div>
227
+
228
  {/* Card header */}
229
+ <div className="px-8 py-6 bg-amber-50 border-b border-amber-100 flex items-start gap-4 mt-8">
230
  <div className="p-2.5 bg-amber-100 rounded-xl shrink-0">
231
  <AlertTriangle className="w-5 h-5 text-amber-600" />
232
  </div>
Frontend/src/user/pages/MyTickets.jsx CHANGED
@@ -120,6 +120,14 @@ function MyTickets() {
120
  return 'text-gray-600';
121
  };
122
 
 
 
 
 
 
 
 
 
123
  return (
124
  <main className="flex-1 max-w-[1200px] w-full mx-auto px-6 py-10 flex flex-col gap-8">
125
  {/* Header section */}
@@ -317,6 +325,11 @@ function MyTickets() {
317
  <p className="text-sm font-semibold text-gray-900 truncate group-hover:text-emerald-700 transition-colors">
318
  {ticket.summary || ticket.subject || ticket.description || "No subject"}
319
  </p>
 
 
 
 
 
320
  </td>
321
  <td className="px-6 py-4">
322
  <span className="text-sm font-medium text-gray-600 bg-gray-100 px-2.5 py-1 rounded-md">
 
120
  return 'text-gray-600';
121
  };
122
 
123
+ const getTranslationInfo = (ticket) => {
124
+ const t = ticket?.metadata?.translation;
125
+ if (!t?.translated) return null;
126
+ return {
127
+ sourceLanguageName: t.source_language_name || t.source_language || 'Unknown',
128
+ };
129
+ };
130
+
131
  return (
132
  <main className="flex-1 max-w-[1200px] w-full mx-auto px-6 py-10 flex flex-col gap-8">
133
  {/* Header section */}
 
325
  <p className="text-sm font-semibold text-gray-900 truncate group-hover:text-emerald-700 transition-colors">
326
  {ticket.summary || ticket.subject || ticket.description || "No subject"}
327
  </p>
328
+ {getTranslationInfo(ticket) && (
329
+ <p className="text-[10px] text-slate-500 mt-1">
330
+ Translated from {getTranslationInfo(ticket).sourceLanguageName}
331
+ </p>
332
+ )}
333
  </td>
334
  <td className="px-6 py-4">
335
  <span className="text-sm font-medium text-gray-600 bg-gray-100 px-2.5 py-1 rounded-md">
Frontend/src/user/pages/TicketDetail.jsx CHANGED
@@ -22,6 +22,7 @@ const TicketDetail = () => {
22
  const [isReopening, setIsReopening] = useState(false);
23
  const [showCsat, setShowCsat] = useState(false);
24
  const [csatHasBeenDismissed, setCsatHasBeenDismissed] = useState(false);
 
25
 
26
  useEffect(() => {
27
  window.scrollTo(0, 0);
@@ -118,10 +119,14 @@ const TicketDetail = () => {
118
  }
119
 
120
  // Safely parse arrays and formats
121
- const entities = ticket.metadata?.entities || ticket.entities || [];
122
  const solutionSteps = Array.isArray(ticket.solution_steps) ? ticket.solution_steps : [];
123
  const isAutoResolved = ticket.auto_resolve === true;
124
  const confidenceScore = ticket.metadata?.confidence ?? ticket.routing_confidence ?? 0.92;
 
 
 
 
125
 
126
 
127
  const handleReopen = async () => {
@@ -184,6 +189,27 @@ const TicketDetail = () => {
184
 
185
  {/* LEFT SIDE (Main Content) */}
186
  <div className="lg:col-span-2 flex flex-col gap-6">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
187
 
188
  {/* Card 1: Ticket Timeline */}
189
  <Card className="p-6 sm:p-8 rounded-2xl border border-gray-100 shadow-sm bg-white">
 
22
  const [isReopening, setIsReopening] = useState(false);
23
  const [showCsat, setShowCsat] = useState(false);
24
  const [csatHasBeenDismissed, setCsatHasBeenDismissed] = useState(false);
25
+ const [showOriginalText, setShowOriginalText] = useState(false);
26
 
27
  useEffect(() => {
28
  window.scrollTo(0, 0);
 
119
  }
120
 
121
  // Safely parse arrays and formats
122
+ const entities = ticket.entities || [];
123
  const solutionSteps = Array.isArray(ticket.solution_steps) ? ticket.solution_steps : [];
124
  const isAutoResolved = ticket.auto_resolve === true;
125
  const confidenceScore = ticket.metadata?.confidence ?? ticket.routing_confidence ?? 0.92;
126
+ const translationMeta = ticket.metadata?.translation;
127
+ const originalTextMeta = ticket.metadata?.original_text;
128
+ const isTranslated = Boolean(translationMeta?.translated && originalTextMeta?.description);
129
+ const sourceLanguageName = translationMeta?.source_language_name || translationMeta?.source_language || 'Unknown';
130
 
131
 
132
  const handleReopen = async () => {
 
189
 
190
  {/* LEFT SIDE (Main Content) */}
191
  <div className="lg:col-span-2 flex flex-col gap-6">
192
+ {isTranslated && (
193
+ <Card className="p-4 rounded-2xl border border-sky-100 bg-sky-50/70 shadow-sm">
194
+ <div className="flex items-center justify-between gap-3">
195
+ <p className="text-sm font-semibold text-sky-900">
196
+ Translated from {sourceLanguageName}
197
+ </p>
198
+ <button
199
+ type="button"
200
+ onClick={() => setShowOriginalText(prev => !prev)}
201
+ className="text-xs font-bold text-sky-700 hover:text-sky-900"
202
+ >
203
+ {showOriginalText ? "View English" : "View Original"}
204
+ </button>
205
+ </div>
206
+ {showOriginalText && (
207
+ <p className="mt-3 text-sm text-slate-700 bg-white border border-sky-100 rounded-lg px-3 py-2">
208
+ {originalTextMeta?.description}
209
+ </p>
210
+ )}
211
+ </Card>
212
+ )}
213
 
214
  {/* Card 1: Ticket Timeline */}
215
  <Card className="p-6 sm:p-8 rounded-2xl border border-gray-100 shadow-sm bg-white">
Frontend/src/user/pages/TicketTracking.jsx CHANGED
@@ -1,4 +1,4 @@
1
- import React, { useEffect, useState, useRef } from 'react';
2
  import { useNavigate, useLocation } from 'react-router-dom';
3
  import {
4
  Activity, CheckCircle2, ShieldCheck, User,
@@ -20,14 +20,20 @@ const TicketTracking = () => {
20
  const [error, setError] = useState(null);
21
  const [createdTicket, setCreatedTicket] = useState(null);
22
  const hasCreated = useRef(false);
 
 
 
 
 
 
 
 
23
  useEffect(() => {
24
  if (!aiTicket) {
25
  navigate('/create-ticket');
26
  return;
27
  }
28
 
29
- const resolutionSteps = location.state?.resolutionSteps || [];
30
-
31
  const finalizeTracking = async () => {
32
  if (hasCreated.current) return;
33
  hasCreated.current = true;
@@ -42,6 +48,7 @@ const TicketTracking = () => {
42
  user_id: user?.id,
43
  subject: aiTicket.summary,
44
  description: aiTicket.originalIssue || aiTicket.summary,
 
45
  category: aiTicket.category,
46
  subcategory: aiTicket.subcategory,
47
  priority: aiTicket.priority,
@@ -49,11 +56,13 @@ const TicketTracking = () => {
49
  status: status,
50
  auto_resolve: isAutoResolved,
51
  is_duplicate: aiTicket.duplicate_ticket?.is_duplicate || false,
 
 
52
  confidence: aiTicket.confidence,
53
  image_url: aiTicket.image_url || null,
54
- company: profile?.company || "System",
55
  company_id: profile?.company_id || null,
56
- sla_breach_at: aiTicket.sla_breach_at,
57
  metadata: {
58
  confidence: aiTicket.confidence,
59
  entities: aiTicket.entities,
@@ -91,7 +100,7 @@ const TicketTracking = () => {
91
  };
92
 
93
  finalizeTracking();
94
- }, [aiTicket, addTicket, navigate, user, profile?.company, location.state]);
95
 
96
  if (!aiTicket) return null;
97
 
 
1
+ import React, { useEffect, useState, useRef, useMemo } from 'react';
2
  import { useNavigate, useLocation } from 'react-router-dom';
3
  import {
4
  Activity, CheckCircle2, ShieldCheck, User,
 
20
  const [error, setError] = useState(null);
21
  const [createdTicket, setCreatedTicket] = useState(null);
22
  const hasCreated = useRef(false);
23
+ const resolutionSteps = useMemo(() => location.state?.resolutionSteps || [], [location.state?.resolutionSteps]);
24
+
25
+ const getSlaBreachAt = (priority = 'Medium') => {
26
+ const hoursMap = { Critical: 2, High: 8, Medium: 24, Low: 72 };
27
+ const slaHours = hoursMap[priority] || 24;
28
+ return new Date(Date.now() + slaHours * 60 * 60 * 1000).toISOString();
29
+ };
30
+
31
  useEffect(() => {
32
  if (!aiTicket) {
33
  navigate('/create-ticket');
34
  return;
35
  }
36
 
 
 
37
  const finalizeTracking = async () => {
38
  if (hasCreated.current) return;
39
  hasCreated.current = true;
 
48
  user_id: user?.id,
49
  subject: aiTicket.summary,
50
  description: aiTicket.originalIssue || aiTicket.summary,
51
+ original_language: aiTicket.originalLanguage || 'en',
52
  category: aiTicket.category,
53
  subcategory: aiTicket.subcategory,
54
  priority: aiTicket.priority,
 
56
  status: status,
57
  auto_resolve: isAutoResolved,
58
  is_duplicate: aiTicket.duplicate_ticket?.is_duplicate || false,
59
+ is_potential_duplicate: aiTicket.is_potential_duplicate || aiTicket.duplicate_ticket?.is_potential_duplicate || false,
60
+ parent_ticket_id: aiTicket.parent_ticket_id || aiTicket.duplicate_ticket?.parent_ticket_id || aiTicket.duplicate_ticket?.duplicate_ticket_id || null,
61
  confidence: aiTicket.confidence,
62
  image_url: aiTicket.image_url || null,
63
+ company: profile?.company || null,
64
  company_id: profile?.company_id || null,
65
+ sla_breach_at: aiTicket.sla_breach_at || getSlaBreachAt(aiTicket.priority),
66
  metadata: {
67
  confidence: aiTicket.confidence,
68
  entities: aiTicket.entities,
 
100
  };
101
 
102
  finalizeTracking();
103
+ }, [aiTicket, addTicket, navigate, user, profile?.company, profile?.company_id, resolutionSteps]);
104
 
105
  if (!aiTicket) return null;
106
 
Frontend/vite.config.js CHANGED
@@ -15,6 +15,6 @@ export default defineConfig({
15
  },
16
  },
17
  build: {
18
- sourcemap: true
19
  }
20
  })
 
15
  },
16
  },
17
  build: {
18
+ sourcemap: 'hidden'
19
  }
20
  })
MobileApp/App.js CHANGED
@@ -1,5 +1,4 @@
1
  import 'react-native-gesture-handler';
2
- import LogRocket from '@logrocket/react-native';
3
  import React, { useEffect, useState } from 'react';
4
  import { NavigationContainer } from '@react-navigation/native';
5
  import { createNativeStackNavigator } from '@react-navigation/native-stack';
@@ -7,8 +6,8 @@ import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
7
  import { StatusBar } from 'expo-status-bar';
8
  import { supabase } from './src/lib/supabase';
9
  import { COLORS } from './src/styles/theme';
10
- import { LayoutDashboard, Ticket, User } from 'lucide-react-native';
11
- import { View, ActivityIndicator } from 'react-native';
12
  import AsyncStorage from '@react-native-async-storage/async-storage';
13
  import { SafeAreaProvider, useSafeAreaInsets } from 'react-native-safe-area-context';
14
 
@@ -34,6 +33,13 @@ import AIProcessingScreen from './src/screens/user/AIProcessingScreen';
34
  import NotificationsScreen from './src/screens/user/NotificationsScreen';
35
  import KnowledgeBaseScreen from './src/screens/user/KnowledgeBaseScreen';
36
 
 
 
 
 
 
 
 
37
  const Stack = createNativeStackNavigator();
38
  const Tab = createBottomTabNavigator();
39
 
@@ -72,6 +78,45 @@ const TabNavigator = () => {
72
  );
73
  };
74
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
  // Inner app that has access to SafeAreaProvider context
76
  const AppContent = () => {
77
  const insets = useSafeAreaInsets();
@@ -79,36 +124,95 @@ const AppContent = () => {
79
  const [loading, setLoading] = useState(true);
80
  const [showOnboarding, setShowOnboarding] = useState(null);
81
  const [userStatus, setUserStatus] = useState(null); // 'active', 'pending_approval', 'rejected'
 
82
 
83
  useEffect(() => {
84
- // Initialize LogRocket
85
- LogRocket.init('ky7sla/helpdeskai');
86
 
87
  const initialize = async () => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
  try {
89
- const { data: { session } } = await supabase.auth.getSession();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
  setSession(session);
91
 
 
92
  if (session?.user) {
93
- // Identify user in LogRocket
94
- LogRocket.identify(session.user.id, {
95
- email: session.user.email,
96
- name: session.user.user_metadata?.full_name || 'User',
97
- });
98
-
99
- const { data } = await supabase
100
  .from('profiles')
101
- .select('status')
102
  .eq('id', session.user.id)
103
  .single();
104
- setUserStatus(data?.status || 'active');
105
- }
106
 
107
- const onboardingDone = await AsyncStorage.getItem('@onboarding_complete');
108
- setShowOnboarding(onboardingDone === null);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
109
  } catch (e) {
110
- console.log('Init error', e);
111
  } finally {
 
 
 
 
 
 
 
 
112
  setLoading(false);
113
  }
114
  };
@@ -118,21 +222,39 @@ const AppContent = () => {
118
  const { data: { subscription } } = supabase.auth.onAuthStateChange(async (_event, session) => {
119
  setSession(session);
120
  if (session?.user) {
121
- // Identify user in LogRocket on auth change
122
- LogRocket.identify(session.user.id, {
123
- email: session.user.email,
124
- name: session.user.user_metadata?.full_name || 'User',
125
- });
126
-
127
- // Fetch profile status for routing
128
- const { data } = await supabase
129
- .from('profiles')
130
- .select('status')
131
- .eq('id', session.user.id)
132
- .single();
133
- setUserStatus(data?.status || 'active');
 
 
 
 
 
 
 
 
 
 
 
 
 
134
  } else {
135
  setUserStatus(null);
 
 
 
 
 
136
  }
137
  });
138
 
@@ -150,14 +272,56 @@ const AppContent = () => {
150
  schema: 'public',
151
  table: 'profiles',
152
  filter: `id=eq.${session.user.id}`,
153
- }, (payload) => {
154
- setUserStatus(payload.new.status);
 
 
 
 
 
 
 
155
  })
156
  .subscribe();
157
 
158
  return () => supabase.removeChannel(channel);
159
  }, [session?.user?.id]);
160
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
161
  if (loading) {
162
  return (
163
  <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: '#0b1120' }}>
@@ -170,6 +334,7 @@ const AppContent = () => {
170
  const isActive = userStatus === 'active';
171
  const isPending = userStatus === 'pending_approval';
172
  const isRejected = userStatus === 'rejected';
 
173
 
174
  return (
175
  <NotificationProvider topInset={insets.top}>
@@ -191,13 +356,20 @@ const AppContent = () => {
191
  ) : (
192
  // ─── Active user ───
193
  <>
194
- <Stack.Screen name="MainTabs" component={TabNavigator} />
 
 
 
 
195
  <Stack.Screen name="CreateTicket" component={CreateTicketScreen} options={{ animation: 'slide_from_bottom' }} />
196
  <Stack.Screen name="AIProcessing" component={AIProcessingScreen} options={{ animation: 'slide_from_right' }} />
197
  <Stack.Screen name="TicketTracking" component={TicketTrackingScreen} options={{ animation: 'slide_from_right' }} />
198
  <Stack.Screen name="TicketDetail" component={TicketDetailScreen} options={{ animation: 'slide_from_right' }} />
199
  <Stack.Screen name="Notifications" component={NotificationsScreen} options={{ animation: 'slide_from_right' }} />
200
  <Stack.Screen name="KnowledgeBase" component={KnowledgeBaseScreen} options={{ animation: 'slide_from_right' }} />
 
 
 
201
  </>
202
  )}
203
  </Stack.Navigator>
 
1
  import 'react-native-gesture-handler';
 
2
  import React, { useEffect, useState } from 'react';
3
  import { NavigationContainer } from '@react-navigation/native';
4
  import { createNativeStackNavigator } from '@react-navigation/native-stack';
 
6
  import { StatusBar } from 'expo-status-bar';
7
  import { supabase } from './src/lib/supabase';
8
  import { COLORS } from './src/styles/theme';
9
+ import { LayoutDashboard, Ticket, User, Settings, ShieldAlert, Users } from 'lucide-react-native';
10
+ import { View, ActivityIndicator, Linking } from 'react-native';
11
  import AsyncStorage from '@react-native-async-storage/async-storage';
12
  import { SafeAreaProvider, useSafeAreaInsets } from 'react-native-safe-area-context';
13
 
 
33
  import NotificationsScreen from './src/screens/user/NotificationsScreen';
34
  import KnowledgeBaseScreen from './src/screens/user/KnowledgeBaseScreen';
35
 
36
+ // Admin Screens
37
+ import AdminDashboardScreen from './src/screens/admin/AdminDashboardScreen';
38
+ import AdminTicketsScreen from './src/screens/admin/AdminTicketsScreen';
39
+ import AdminTicketDetailScreen from './src/screens/admin/AdminTicketDetailScreen';
40
+ import AdminUsersScreen from './src/screens/admin/AdminUsersScreen';
41
+ import AdminSettingsScreen from './src/screens/admin/AdminSettingsScreen';
42
+
43
  const Stack = createNativeStackNavigator();
44
  const Tab = createBottomTabNavigator();
45
 
 
78
  );
79
  };
80
 
81
+ const AdminTabNavigator = () => {
82
+ const insets = useSafeAreaInsets();
83
+ const tabBarHeight = 60 + insets.bottom;
84
+
85
+ return (
86
+ <Tab.Navigator
87
+ screenOptions={({ route }) => ({
88
+ tabBarIcon: ({ color, size }) => {
89
+ if (route.name === 'AdminDashboard') return <LayoutDashboard size={size} color={color} />;
90
+ if (route.name === 'Tickets') return <Ticket size={size} color={color} />;
91
+ if (route.name === 'Users') return <Users size={size} color={color} />;
92
+ if (route.name === 'Settings') return <Settings size={size} color={color} />;
93
+ if (route.name === 'Profile') return <User size={size} color={color} />;
94
+ },
95
+ tabBarActiveTintColor: COLORS.primary,
96
+ tabBarInactiveTintColor: COLORS.textMuted,
97
+ tabBarLabelStyle: { fontSize: 11, fontWeight: '700' },
98
+ tabBarStyle: {
99
+ height: tabBarHeight,
100
+ paddingBottom: insets.bottom + 8,
101
+ paddingTop: 10,
102
+ backgroundColor: '#ffffff',
103
+ borderTopWidth: 1,
104
+ borderTopColor: '#f0f0f0',
105
+ elevation: 0,
106
+ shadowOpacity: 0,
107
+ },
108
+ headerShown: false,
109
+ })}
110
+ >
111
+ <Tab.Screen name="AdminDashboard" component={AdminDashboardScreen} options={{ title: 'Dashboard' }} />
112
+ <Tab.Screen name="Tickets" component={AdminTicketsScreen} />
113
+ <Tab.Screen name="Users" component={AdminUsersScreen} />
114
+ <Tab.Screen name="Settings" component={AdminSettingsScreen} />
115
+ <Tab.Screen name="Profile" component={ProfileScreen} />
116
+ </Tab.Navigator>
117
+ );
118
+ };
119
+
120
  // Inner app that has access to SafeAreaProvider context
121
  const AppContent = () => {
122
  const insets = useSafeAreaInsets();
 
124
  const [loading, setLoading] = useState(true);
125
  const [showOnboarding, setShowOnboarding] = useState(null);
126
  const [userStatus, setUserStatus] = useState(null); // 'active', 'pending_approval', 'rejected'
127
+ const [userRole, setUserRole] = useState('user'); // 'user', 'admin', 'master_admin'
128
 
129
  useEffect(() => {
130
+ let finished = false;
 
131
 
132
  const initialize = async () => {
133
+ // Safety timeout wrapper: if anything hangs, force mount after 3 seconds
134
+ const timeoutId = setTimeout(async () => {
135
+ if (!finished) {
136
+ console.log('[AuthInit] Timeout reached (3s). Forcing mount fallback.');
137
+ try {
138
+ const onboardingDone = await AsyncStorage.getItem('@onboarding_complete');
139
+ setShowOnboarding(onboardingDone === null);
140
+ } catch (err) {
141
+ setShowOnboarding(false);
142
+ }
143
+ setLoading(false);
144
+ }
145
+ }, 3000);
146
+
147
  try {
148
+ // 1. Instantly load cached status and role from AsyncStorage (stale-while-revalidate pattern)
149
+ const [cachedStatus, cachedRole] = await Promise.all([
150
+ AsyncStorage.getItem('@user_status'),
151
+ AsyncStorage.getItem('@user_role'),
152
+ ]);
153
+
154
+ if (cachedStatus) setUserStatus(cachedStatus);
155
+ if (cachedRole) setUserRole(cachedRole);
156
+
157
+ // 2. Fetch session with a 2.5 second timeout wrapper to prevent slow refreshing from locking the app
158
+ const sessionPromise = supabase.auth.getSession();
159
+ const sessionTimeoutPromise = new Promise((_, reject) =>
160
+ setTimeout(() => reject(new Error('Session fetch timed out')), 2500)
161
+ );
162
+
163
+ const { data: { session } } = await Promise.race([sessionPromise, sessionTimeoutPromise]);
164
  setSession(session);
165
 
166
+ // 3. If session is valid, validate/fetch the user profile
167
  if (session?.user) {
168
+ const profilePromise = supabase
 
 
 
 
 
 
169
  .from('profiles')
170
+ .select('status, role')
171
  .eq('id', session.user.id)
172
  .single();
 
 
173
 
174
+ const profileTimeoutPromise = new Promise((_, reject) =>
175
+ setTimeout(() => reject(new Error('Profile fetch timed out')), 2000)
176
+ );
177
+
178
+ const { data, error } = await Promise.race([profilePromise, profileTimeoutPromise]);
179
+
180
+ if (error) {
181
+ console.log('[AuthInit] Profile fetch error, validating session:', error.message);
182
+ // Verify if session is still validly active (GetUser forces refresh if expired)
183
+ const { data: userData, error: userError } = await supabase.auth.getUser();
184
+ if (userError) {
185
+ console.log('[AuthInit] Token validation failed. Clearing session.');
186
+ setSession(null);
187
+ await AsyncStorage.removeItem('@user_status');
188
+ await AsyncStorage.removeItem('@user_role');
189
+ } else {
190
+ // Valid token but profiles table is offline; retain cached or default to safe user status
191
+ if (!cachedStatus) setUserStatus('active');
192
+ if (!cachedRole) setUserRole('user');
193
+ }
194
+ } else {
195
+ const status = data?.status || 'active';
196
+ const role = data?.role || 'user';
197
+ setUserStatus(status);
198
+ setUserRole(role);
199
+ await Promise.all([
200
+ AsyncStorage.setItem('@user_status', status),
201
+ AsyncStorage.setItem('@user_role', role),
202
+ ]);
203
+ }
204
+ }
205
  } catch (e) {
206
+ console.log('[AuthInit] Exception caught during initialization:', e.message || e);
207
  } finally {
208
+ finished = true;
209
+ clearTimeout(timeoutId);
210
+ try {
211
+ const onboardingDone = await AsyncStorage.getItem('@onboarding_complete');
212
+ setShowOnboarding(onboardingDone === null);
213
+ } catch (err) {
214
+ setShowOnboarding(false);
215
+ }
216
  setLoading(false);
217
  }
218
  };
 
222
  const { data: { subscription } } = supabase.auth.onAuthStateChange(async (_event, session) => {
223
  setSession(session);
224
  if (session?.user) {
225
+ try {
226
+ const { data, error } = await supabase
227
+ .from('profiles')
228
+ .select('status, role')
229
+ .eq('id', session.user.id)
230
+ .single();
231
+
232
+ if (error) {
233
+ console.log('[AuthChange] Profile query failed:', error.message);
234
+ setUserStatus('active');
235
+ setUserRole('user');
236
+ } else {
237
+ const status = data?.status || 'active';
238
+ const role = data?.role || 'user';
239
+ setUserStatus(status);
240
+ setUserRole(role);
241
+ await Promise.all([
242
+ AsyncStorage.setItem('@user_status', status),
243
+ AsyncStorage.setItem('@user_role', role),
244
+ ]);
245
+ }
246
+ } catch (err) {
247
+ console.warn('[AuthChange] Uncaught exception inside handler:', err);
248
+ setUserStatus('active');
249
+ setUserRole('user');
250
+ }
251
  } else {
252
  setUserStatus(null);
253
+ setUserRole('user');
254
+ await Promise.all([
255
+ AsyncStorage.removeItem('@user_status'),
256
+ AsyncStorage.removeItem('@user_role'),
257
+ ]);
258
  }
259
  });
260
 
 
272
  schema: 'public',
273
  table: 'profiles',
274
  filter: `id=eq.${session.user.id}`,
275
+ }, async (payload) => {
276
+ const status = payload.new.status;
277
+ const role = payload.new.role || 'user';
278
+ setUserStatus(status);
279
+ setUserRole(role);
280
+ await Promise.all([
281
+ AsyncStorage.setItem('@user_status', status),
282
+ AsyncStorage.setItem('@user_role', role),
283
+ ]);
284
  })
285
  .subscribe();
286
 
287
  return () => supabase.removeChannel(channel);
288
  }, [session?.user?.id]);
289
 
290
+ // Deep link listener for magic link / email verification callbacks
291
+ useEffect(() => {
292
+ const handleUrl = async ({ url }) => {
293
+ if (!url) return;
294
+ const hashIndex = url.indexOf('#');
295
+ if (hashIndex === -1) return;
296
+ const hash = url.substring(hashIndex + 1);
297
+ const params = {};
298
+ hash.split('&').forEach(pair => {
299
+ const [key, val] = pair.split('=');
300
+ if (key && val) params[key] = decodeURIComponent(val);
301
+ });
302
+ if (params.access_token && params.refresh_token) {
303
+ try {
304
+ const { data, error } = await supabase.auth.setSession({
305
+ access_token: params.access_token,
306
+ refresh_token: params.refresh_token,
307
+ });
308
+ if (error) {
309
+ console.warn('[DeepLink] Failed to set session:', error.message);
310
+ } else {
311
+ console.log('[DeepLink] Session set successfully for:', data.user?.email);
312
+ }
313
+ } catch (e) {
314
+ console.warn('[DeepLink] Error parsing magic link:', e);
315
+ }
316
+ }
317
+ };
318
+
319
+ const subscription = Linking.addEventListener('url', handleUrl);
320
+ Linking.getInitialURL().then(url => url && handleUrl({ url }));
321
+
322
+ return () => subscription.remove();
323
+ }, []);
324
+
325
  if (loading) {
326
  return (
327
  <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: '#0b1120' }}>
 
334
  const isActive = userStatus === 'active';
335
  const isPending = userStatus === 'pending_approval';
336
  const isRejected = userStatus === 'rejected';
337
+ const isAdmin = userRole === 'admin' || userRole === 'master_admin';
338
 
339
  return (
340
  <NotificationProvider topInset={insets.top}>
 
356
  ) : (
357
  // ─── Active user ───
358
  <>
359
+ {isAdmin ? (
360
+ <Stack.Screen name="MainTabs" component={AdminTabNavigator} />
361
+ ) : (
362
+ <Stack.Screen name="MainTabs" component={TabNavigator} />
363
+ )}
364
  <Stack.Screen name="CreateTicket" component={CreateTicketScreen} options={{ animation: 'slide_from_bottom' }} />
365
  <Stack.Screen name="AIProcessing" component={AIProcessingScreen} options={{ animation: 'slide_from_right' }} />
366
  <Stack.Screen name="TicketTracking" component={TicketTrackingScreen} options={{ animation: 'slide_from_right' }} />
367
  <Stack.Screen name="TicketDetail" component={TicketDetailScreen} options={{ animation: 'slide_from_right' }} />
368
  <Stack.Screen name="Notifications" component={NotificationsScreen} options={{ animation: 'slide_from_right' }} />
369
  <Stack.Screen name="KnowledgeBase" component={KnowledgeBaseScreen} options={{ animation: 'slide_from_right' }} />
370
+
371
+ {/* Admin specific screens */}
372
+ <Stack.Screen name="AdminTicketDetail" component={AdminTicketDetailScreen} options={{ animation: 'slide_from_right' }} />
373
  </>
374
  )}
375
  </Stack.Navigator>
MobileApp/app.json CHANGED
@@ -12,13 +12,14 @@
12
  "resizeMode": "contain",
13
  "backgroundColor": "#ffffff"
14
  },
 
15
  "ios": {
16
  "supportsTablet": true
17
  },
18
  "android": {
19
  "package": "com.ritesh.helpdeskai",
20
  "adaptiveIcon": {
21
- "foregroundImage": "./assets/logo_v1.png",
22
  "backgroundColor": "#0b1120"
23
  },
24
  "edgeToEdgeEnabled": true
@@ -27,7 +28,6 @@
27
  "favicon": "./assets/favicon.png"
28
  },
29
  "plugins": [
30
- "@logrocket/react-native",
31
  [
32
  "expo-build-properties",
33
  {
@@ -41,7 +41,9 @@
41
  "policy": "sdkVersion"
42
  },
43
  "updates": {
44
- "url": "https://u.expo.dev/a5830464-58a7-4737-9cc9-e5ecad2e4acf"
 
 
45
  },
46
  "extra": {
47
  "eas": {
 
12
  "resizeMode": "contain",
13
  "backgroundColor": "#ffffff"
14
  },
15
+ "scheme": "helpdeskai",
16
  "ios": {
17
  "supportsTablet": true
18
  },
19
  "android": {
20
  "package": "com.ritesh.helpdeskai",
21
  "adaptiveIcon": {
22
+ "foregroundImage": "./assets/adaptive-icon.png",
23
  "backgroundColor": "#0b1120"
24
  },
25
  "edgeToEdgeEnabled": true
 
28
  "favicon": "./assets/favicon.png"
29
  },
30
  "plugins": [
 
31
  [
32
  "expo-build-properties",
33
  {
 
41
  "policy": "sdkVersion"
42
  },
43
  "updates": {
44
+ "url": "https://u.expo.dev/a5830464-58a7-4737-9cc9-e5ecad2e4acf",
45
+ "checkAutomatically": "ON_LOAD",
46
+ "fallbackToCacheTimeout": 0
47
  },
48
  "extra": {
49
  "eas": {
MobileApp/assets/adaptive-icon.png CHANGED

Git LFS Details

  • SHA256: 1c07a1d56b311c4dd317cc57c2e49f8409d146537bfc9dc1ea68d2e737d0b728
  • Pointer size: 131 Bytes
  • Size of remote file: 443 kB

Git LFS Details

  • SHA256: 3cc868585ddfc07f3d86297fd2f6c8439bf81452fd1a187337fd8d64f44cb2c2
  • Pointer size: 131 Bytes
  • Size of remote file: 122 kB
MobileApp/assets/icon.png CHANGED

Git LFS Details

  • SHA256: 1c07a1d56b311c4dd317cc57c2e49f8409d146537bfc9dc1ea68d2e737d0b728
  • Pointer size: 131 Bytes
  • Size of remote file: 443 kB

Git LFS Details

  • SHA256: 3cc868585ddfc07f3d86297fd2f6c8439bf81452fd1a187337fd8d64f44cb2c2
  • Pointer size: 131 Bytes
  • Size of remote file: 122 kB
MobileApp/assets/logo_v1.png CHANGED

Git LFS Details

  • SHA256: 1c07a1d56b311c4dd317cc57c2e49f8409d146537bfc9dc1ea68d2e737d0b728
  • Pointer size: 131 Bytes
  • Size of remote file: 443 kB

Git LFS Details

  • SHA256: 3cc868585ddfc07f3d86297fd2f6c8439bf81452fd1a187337fd8d64f44cb2c2
  • Pointer size: 131 Bytes
  • Size of remote file: 122 kB
MobileApp/assets/splash-icon.png CHANGED

Git LFS Details

  • SHA256: 1c07a1d56b311c4dd317cc57c2e49f8409d146537bfc9dc1ea68d2e737d0b728
  • Pointer size: 131 Bytes
  • Size of remote file: 443 kB

Git LFS Details

  • SHA256: 3cc868585ddfc07f3d86297fd2f6c8439bf81452fd1a187337fd8d64f44cb2c2
  • Pointer size: 131 Bytes
  • Size of remote file: 122 kB
MobileApp/package-lock.json CHANGED
@@ -8,7 +8,6 @@
8
  "name": "mobileapp_new",
9
  "version": "1.0.0",
10
  "dependencies": {
11
- "@logrocket/react-native": "^2.3.2",
12
  "@react-native-async-storage/async-storage": "2.2.0",
13
  "@react-navigation/bottom-tabs": "^7.15.10",
14
  "@react-navigation/native": "^7.2.2",
@@ -32,6 +31,7 @@
32
  "react-native-screens": "~4.16.0",
33
  "react-native-svg": "15.12.1",
34
  "react-native-url-polyfill": "^3.0.0",
 
35
  "zustand": "^5.0.12"
36
  },
37
  "devDependencies": {
@@ -2736,23 +2736,6 @@
2736
  "@jridgewell/sourcemap-codec": "^1.4.14"
2737
  }
2738
  },
2739
- "node_modules/@logrocket/react-native": {
2740
- "version": "2.3.2",
2741
- "resolved": "https://registry.npmjs.org/@logrocket/react-native/-/react-native-2.3.2.tgz",
2742
- "integrity": "sha512-Fi8H3HCTHj6NFcDGurxz/bXZ2A1fTCj6WVocRNmaqDSSIHL63xP01PyXqLT6xdTakO65Mlw7i37WxojLYuzgmQ==",
2743
- "license": "MIT",
2744
- "peerDependencies": {
2745
- "expo": ">=45.0.0",
2746
- "expo-build-properties": "*",
2747
- "react": "*",
2748
- "react-native": ">=0.60.0-rc.0 <1.0.x"
2749
- },
2750
- "peerDependenciesMeta": {
2751
- "expo": {
2752
- "optional": true
2753
- }
2754
- }
2755
- },
2756
  "node_modules/@react-native-async-storage/async-storage": {
2757
  "version": "2.2.0",
2758
  "resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-2.2.0.tgz",
@@ -8495,6 +8478,32 @@
8495
  "react-native": "*"
8496
  }
8497
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8498
  "node_modules/react-native/node_modules/@react-native/virtualized-lists": {
8499
  "version": "0.81.5",
8500
  "resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.81.5.tgz",
 
8
  "name": "mobileapp_new",
9
  "version": "1.0.0",
10
  "dependencies": {
 
11
  "@react-native-async-storage/async-storage": "2.2.0",
12
  "@react-navigation/bottom-tabs": "^7.15.10",
13
  "@react-navigation/native": "^7.2.2",
 
31
  "react-native-screens": "~4.16.0",
32
  "react-native-svg": "15.12.1",
33
  "react-native-url-polyfill": "^3.0.0",
34
+ "react-native-webview": "^13.15.0",
35
  "zustand": "^5.0.12"
36
  },
37
  "devDependencies": {
 
2736
  "@jridgewell/sourcemap-codec": "^1.4.14"
2737
  }
2738
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2739
  "node_modules/@react-native-async-storage/async-storage": {
2740
  "version": "2.2.0",
2741
  "resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-2.2.0.tgz",
 
8478
  "react-native": "*"
8479
  }
8480
  },
8481
+ "node_modules/react-native-webview": {
8482
+ "version": "13.16.1",
8483
+ "resolved": "https://registry.npmjs.org/react-native-webview/-/react-native-webview-13.16.1.tgz",
8484
+ "integrity": "sha512-If0eHhoEdOYDcHsX+xBFwHMbWBGK1BvGDQDQdVkwtSIXiq1uiqjkpWVP2uQ1as94J0CzvFE9PUNDuhiX0Z6ubw==",
8485
+ "license": "MIT",
8486
+ "dependencies": {
8487
+ "escape-string-regexp": "^4.0.0",
8488
+ "invariant": "2.2.4"
8489
+ },
8490
+ "peerDependencies": {
8491
+ "react": "*",
8492
+ "react-native": "*"
8493
+ }
8494
+ },
8495
+ "node_modules/react-native-webview/node_modules/escape-string-regexp": {
8496
+ "version": "4.0.0",
8497
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
8498
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
8499
+ "license": "MIT",
8500
+ "engines": {
8501
+ "node": ">=10"
8502
+ },
8503
+ "funding": {
8504
+ "url": "https://github.com/sponsors/sindresorhus"
8505
+ }
8506
+ },
8507
  "node_modules/react-native/node_modules/@react-native/virtualized-lists": {
8508
  "version": "0.81.5",
8509
  "resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.81.5.tgz",