diff --git a/src/components/admin/UserRoleManager/UserRoleManager.tsx b/src/components/admin/UserRoleManager/UserRoleManager.tsx index 1c6ed40..582da5c 100644 --- a/src/components/admin/UserRoleManager/UserRoleManager.tsx +++ b/src/components/admin/UserRoleManager/UserRoleManager.tsx @@ -1,5 +1,6 @@ import { useState, useCallback, useEffect, useRef } from 'react'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Separator } from '@/components/ui/separator'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Badge } from '@/components/ui/badge'; @@ -336,9 +337,23 @@ export function UserRoleManager() { }; return ( -
- {/* Statistics Cards */} -
+ + +
+
+ +
+
+ User Role Management + + Search for users, assign roles, and manage user permissions across the system + +
+
+
+ + {/* Statistics Cards */} +
+ + {/* Assign Role Section */} - - -
-
- -
-
- Assign User Role - - Search for a user in Okta and assign them a role - -
-
-
- +
+
+

Assign User Role

+

+ Search for a user in Okta and assign them a role +

+
+
{/* Search Input */}
@@ -435,11 +445,11 @@ export function UserRoleManager() { placeholder="Type name or email address..." value={searchQuery} onChange={handleSearchChange} - className="pl-10 pr-10 h-12 border rounded-lg border-gray-300 focus:border-purple-500 focus:ring-2 focus:ring-purple-200 transition-all" + className="pl-10 pr-10 border-gray-200 focus:border-re-green focus:ring-2 focus:ring-re-green/20" data-testid="user-search-input" /> {searching && ( - + )}

Start typing to search across all Okta users

@@ -452,18 +462,18 @@ export function UserRoleManager() { {searchResults.length} user{searchResults.length > 1 ? 's' : ''} found

-
+
{searchResults.map((user) => (
- - +
{loadingUsers ? (
@@ -666,7 +670,7 @@ export function UserRoleManager() { {users.map((user) => (
@@ -752,10 +756,10 @@ export function UserRoleManager() { )} )} - - +
-
+ + ); } diff --git a/src/components/modals/ShareSummaryModal.tsx b/src/components/modals/ShareSummaryModal.tsx index 0c2de64..0c838fc 100644 --- a/src/components/modals/ShareSummaryModal.tsx +++ b/src/components/modals/ShareSummaryModal.tsx @@ -127,30 +127,39 @@ export function ShareSummaryModal({ isOpen, onClose, summaryId, requestTitle, on {!searching && users.length > 0 && (
- {users.map((user) => ( -
handleToggleUser(user.userId)} - > - handleToggleUser(user.userId)} - /> -
-
- -

- {user.displayName || user.email} -

+ {users.map((user) => { + const isSelected = selectedUserIds.has(user.userId); + return ( +
handleToggleUser(user.userId)} + > +
e.stopPropagation()} + onMouseDown={(e) => e.stopPropagation()} + className="flex items-center" + > + handleToggleUser(user.userId)} + /> +
+
+
+ +

+ {user.displayName || user.email} +

+
+ {(user.designation || user.department) && ( +

{user.designation || user.department}

+ )} +

{user.email}

- {(user.designation || user.department) && ( -

{user.designation || user.department}

- )} -

{user.email}

-
- ))} + ); + })}
)} diff --git a/src/components/settings/NotificationStatusModal/NotificationStatusModal.tsx b/src/components/settings/NotificationStatusModal/NotificationStatusModal.tsx index 6cf46f9..ce4bf37 100644 --- a/src/components/settings/NotificationStatusModal/NotificationStatusModal.tsx +++ b/src/components/settings/NotificationStatusModal/NotificationStatusModal.tsx @@ -1,4 +1,4 @@ -import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; import { Bell, CheckCircle, XCircle } from 'lucide-react'; @@ -23,6 +23,11 @@ export function NotificationStatusModal({ Push Notifications + + {success + ? 'Push notifications have been successfully enabled for your account.' + : 'There was an error enabling push notifications. Please review the details below.'} +
diff --git a/src/hooks/useConclusionRemark.ts b/src/hooks/useConclusionRemark.ts index 543ce2d..a751fd8 100644 --- a/src/hooks/useConclusionRemark.ts +++ b/src/hooks/useConclusionRemark.ts @@ -121,7 +121,7 @@ export function useConclusionRemark( * Purpose: Submit conclusion remark and close the request * * Business Logic: - * - Only initiators can finalize approved requests + * - Only initiators can finalize approved or rejected requests * - Conclusion cannot be empty * - After finalization: * → Request status changes to CLOSED @@ -206,13 +206,13 @@ export function useConclusionRemark( }; /** - * Effect: Auto-fetch existing conclusion when request becomes approved + * Effect: Auto-fetch existing conclusion when request becomes approved or rejected * - * Trigger: When request status changes to "approved" and user is initiator - * Purpose: Load any conclusion generated by final approver + * Trigger: When request status changes to "approved" or "rejected" and user is initiator + * Purpose: Load any conclusion generated by final approver (for approved) or AI (for rejected) */ useEffect(() => { - if (request?.status === 'approved' && isInitiator && !conclusionRemark) { + if ((request?.status === 'approved' || request?.status === 'rejected') && isInitiator && !conclusionRemark) { fetchExistingConclusion(); } }, [request?.status, isInitiator]); diff --git a/src/pages/Auth/Auth.tsx b/src/pages/Auth/Auth.tsx index 63a50fe..1f0bcf7 100644 --- a/src/pages/Auth/Auth.tsx +++ b/src/pages/Auth/Auth.tsx @@ -1,7 +1,8 @@ import { useAuth } from '@/contexts/AuthContext'; import { Button } from '@/components/ui/button'; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; -import { LogIn, Shield } from 'lucide-react'; +import { Card, CardContent, CardHeader } from '@/components/ui/card'; +import { LogIn } from 'lucide-react'; +import { ReLogo } from '@/assets'; export function Auth() { const { login, isLoading, error } = useAuth(); @@ -34,17 +35,14 @@ export function Auth() {
-
-
- -
+
+ Royal Enfield Logo +

Approval Portal

- - Royal Enfield - - - Approval & Request Management Portal - @@ -58,12 +56,14 @@ export function Auth() { + ) : ( + + {getStatusIcon(summary.workflow.status)} + {summary.workflow.status} + + )} +
+ {summary.description && ( +

{summary.description}

+ )} +
+ + {/* Initiator Section */} +
+

Initiator

+
+
+

Name

+

{summary.initiator.name}

+
+
+

Designation

+

+ {getDesignationOrDepartment(summary.initiator.designation, summary.initiator.department)} +

+
+
+

Status

+

{summary.initiator.status}

+
+
+

Time Stamp

+

+ {format(new Date(summary.initiator.timestamp), 'MMM dd, yy, HH:mm')} +

+
+
+
+ + {/* Approvers Section */} + {summary.approvers && summary.approvers.length > 0 && ( +
+

Workflow

+ {summary.approvers.map((approver, index) => ( +
+

+ {approver.levelName || `Approver ${approver.levelNumber}`} +

+
+
+

Name

+

{approver.name}

+
+
+

Designation

+

+ {getDesignationOrDepartment(approver.designation, approver.department)} +

+
+
+

Status

+
+ {getStatusIcon(approver.status)} +

{approver.status}

+
+
+
+

Time Stamp

+

+ {format(new Date(approver.timestamp), 'MMM dd, yy, HH:mm')} +

+
+
+
+

Remarks

+

{approver.remarks || '—'}

+
+
+ ))} +
+ )} + + {/* Closing Remarks Section */} +
+

Closing Remarks (Conclusion)

+
+
+
+

Name

+

{summary.initiator.name}

+
+
+

Designation

+

+ {getDesignationOrDepartment(summary.initiator.designation, summary.initiator.department)} +

+
+
+

Status

+

Concluded

+
+ {summary.isAiGenerated && ( +
+

Source

+ AI Generated +
+ )} +
+
+

Remarks

+

{summary.closingRemarks || '—'}

+
+
+
+
+
+ ); +} + diff --git a/src/pages/Requests/UserAllRequests.tsx b/src/pages/Requests/UserAllRequests.tsx index 015bb5e..c25eb06 100644 --- a/src/pages/Requests/UserAllRequests.tsx +++ b/src/pages/Requests/UserAllRequests.tsx @@ -2,7 +2,9 @@ * User All Requests Page - For Regular Users * * This is a SEPARATE screen for regular users' "All Requests" page. - * Shows only requests where the user is a participant (approver/spectator), NOT initiator. + * Shows requests where the user is EITHER: + * - The initiator (created by the user), OR + * - A participant (approver/spectator) * Completely separate from AdminAllRequests to avoid interference. */ diff --git a/src/pages/Requests/services/userRequestsService.ts b/src/pages/Requests/services/userRequestsService.ts index 0c29366..4597623 100644 --- a/src/pages/Requests/services/userRequestsService.ts +++ b/src/pages/Requests/services/userRequestsService.ts @@ -1,9 +1,9 @@ /** - * Service for fetching user participant requests data + * Service for fetching user requests data (initiator + participant) * SEPARATE from admin requests service to avoid interference * * This service is specifically for regular users' "All Requests" page - * Shows only requests where user is a participant (approver/spectator), NOT initiator + * Shows requests where user is EITHER initiator OR participant (approver/spectator) */ import workflowApi from '@/services/workflowApi'; @@ -11,21 +11,21 @@ import type { RequestFilters } from '../types/requests.types'; const EXPORT_FETCH_LIMIT = 100; -interface FetchUserParticipantRequestsOptions { +interface FetchUserAllRequestsOptions { page: number; itemsPerPage: number; filters?: RequestFilters; } /** - * Fetch participant requests for regular users - * Uses /workflows/participant-requests endpoint which excludes initiator requests + * Fetch all requests for regular users (initiator + participant) + * Combines requests where user is initiator AND requests where user is participant */ export async function fetchUserParticipantRequestsData({ page, itemsPerPage, filters -}: FetchUserParticipantRequestsOptions) { +}: FetchUserAllRequestsOptions) { // Build filter params for backend API const backendFilters: any = {}; if (filters?.search) backendFilters.search = filters.search; @@ -42,51 +42,128 @@ export async function fetchUserParticipantRequestsData({ if (filters?.startDate) backendFilters.startDate = filters.startDate instanceof Date ? filters.startDate.toISOString() : filters.startDate; if (filters?.endDate) backendFilters.endDate = filters.endDate instanceof Date ? filters.endDate.toISOString() : filters.endDate; - // Fetch paginated data using SEPARATE endpoint for regular users - // This endpoint automatically excludes initiator requests - const pageResult = await workflowApi.listParticipantRequests({ - page, - limit: itemsPerPage, - ...backendFilters - }); + // To properly merge and paginate, we need to fetch enough data from both endpoints + // Fetch multiple pages from each endpoint to ensure we have enough data to merge and paginate correctly + const fetchLimit = Math.max(itemsPerPage * 3, 100); // Fetch at least 3 pages worth or 100 items, whichever is larger + + // Fetch from both endpoints in parallel + const [initiatorResult, participantResult] = await Promise.all([ + // Fetch requests where user is initiator (fetch more to account for merging) + workflowApi.listMyInitiatedWorkflows({ + page: 1, + limit: fetchLimit, + search: backendFilters.search, + status: backendFilters.status, + priority: backendFilters.priority, + department: backendFilters.department, + slaCompliance: backendFilters.slaCompliance, + dateRange: backendFilters.dateRange, + startDate: backendFilters.startDate, + endDate: backendFilters.endDate + }), + // Fetch requests where user is participant (approver/spectator) + workflowApi.listParticipantRequests({ + page: 1, + limit: fetchLimit, + ...backendFilters + }) + ]); - let pageData: any[] = []; - if (Array.isArray(pageResult?.data)) { - pageData = pageResult.data; - } else if (Array.isArray(pageResult)) { - pageData = pageResult; + // Extract data from both results + let initiatorData: any[] = []; + if (Array.isArray(initiatorResult?.data)) { + initiatorData = initiatorResult.data; + } else if (Array.isArray(initiatorResult)) { + initiatorData = initiatorResult; } - // Filter out drafts (backend should handle this, but double-check) - const nonDraftData = pageData.filter((req: any) => { + let participantData: any[] = []; + if (Array.isArray(participantResult?.data)) { + participantData = participantResult.data; + } else if (Array.isArray(participantResult)) { + participantData = participantResult; + } + + // Filter out drafts from both + const nonDraftInitiatorData = initiatorData.filter((req: any) => { const reqStatus = (req.status || '').toString().toUpperCase(); return reqStatus !== 'DRAFT'; }); - // Get pagination info from backend response - const pagination = pageResult?.pagination || { + const nonDraftParticipantData = participantData.filter((req: any) => { + const reqStatus = (req.status || '').toString().toUpperCase(); + return reqStatus !== 'DRAFT'; + }); + + // Merge and deduplicate by requestId + const mergedMap = new Map(); + + // Add initiator requests + nonDraftInitiatorData.forEach((req: any) => { + const requestId = req.requestId || req.id; + if (requestId) { + mergedMap.set(requestId, req); + } + }); + + // Add participant requests (will overwrite if duplicate, but that's fine) + nonDraftParticipantData.forEach((req: any) => { + const requestId = req.requestId || req.id; + if (requestId) { + mergedMap.set(requestId, req); + } + }); + + // Convert map to array + const mergedData = Array.from(mergedMap.values()); + + // Sort by updatedAt or createdAt (most recent first) + mergedData.sort((a: any, b: any) => { + const dateA = new Date(a.updatedAt || a.createdAt || 0).getTime(); + const dateB = new Date(b.updatedAt || b.createdAt || 0).getTime(); + return dateB - dateA; + }); + + // Calculate combined pagination + const initiatorPagination = initiatorResult?.pagination || { page: 1, limit: fetchLimit, total: 0, totalPages: 1 }; + const participantPagination = participantResult?.pagination || { page: 1, limit: fetchLimit, total: 0, totalPages: 1 }; + + // Estimate total: sum of both totals, but account for potential duplicates + // We'll use a conservative estimate: sum of both, but we know there might be overlap + const estimatedTotal = (initiatorPagination.total || 0) + (participantPagination.total || 0); + // The actual merged count might be less due to duplicates, but we use the merged length if we have enough data + const actualTotal = mergedData.length >= fetchLimit ? estimatedTotal : mergedData.length; + + // Paginate the merged results + const startIndex = (page - 1) * itemsPerPage; + const endIndex = startIndex + itemsPerPage; + const paginatedData = mergedData.slice(startIndex, endIndex); + + const pagination = { page, limit: itemsPerPage, - total: nonDraftData.length, - totalPages: 1 + total: actualTotal, + totalPages: Math.ceil(actualTotal / itemsPerPage) || 1 }; return { - data: nonDraftData, // Paginated data for list display + data: paginatedData, // Paginated merged data for list display allData: [], // Stats calculated from data - filteredData: nonDraftData, // Same as data for list + filteredData: paginatedData, // Same as data for list pagination: pagination }; } /** - * Fetch all participant requests for export (regular users) - * Uses the same endpoint but fetches all pages + * Fetch all requests for export (regular users - initiator + participant) + * Fetches from both endpoints and merges results */ export async function fetchAllRequestsForExport(filters?: RequestFilters): Promise { - const allPages: any[] = []; + const allInitiatorPages: any[] = []; + const allParticipantPages: any[] = []; let currentPage = 1; - let hasMore = true; + let hasMoreInitiator = true; + let hasMoreParticipant = true; const maxPages = 100; // Safety limit // Build filter params for backend API @@ -105,25 +182,101 @@ export async function fetchAllRequestsForExport(filters?: RequestFilters): Promi if (filters?.startDate) backendFilters.startDate = filters.startDate instanceof Date ? filters.startDate.toISOString() : filters.startDate; if (filters?.endDate) backendFilters.endDate = filters.endDate instanceof Date ? filters.endDate.toISOString() : filters.endDate; - while (hasMore && currentPage <= maxPages) { - const pageResult = await workflowApi.listParticipantRequests({ - page: currentPage, - limit: EXPORT_FETCH_LIMIT, - ...backendFilters - }); + // Fetch all pages from both endpoints in parallel + const fetchPromises: Promise[] = []; - const pageData = pageResult?.data || []; - if (pageData.length === 0) { - hasMore = false; - } else { - allPages.push(...pageData); - currentPage++; - if (pageData.length < EXPORT_FETCH_LIMIT) { - hasMore = false; + // Fetch initiator requests + const initiatorFetch = async () => { + let page = 1; + while (hasMoreInitiator && page <= maxPages) { + const pageResult = await workflowApi.listMyInitiatedWorkflows({ + page, + limit: EXPORT_FETCH_LIMIT, + search: backendFilters.search, + status: backendFilters.status, + priority: backendFilters.priority, + department: backendFilters.department, + slaCompliance: backendFilters.slaCompliance, + dateRange: backendFilters.dateRange, + startDate: backendFilters.startDate, + endDate: backendFilters.endDate + }); + + const pageData = pageResult?.data || []; + if (pageData.length === 0) { + hasMoreInitiator = false; + } else { + allInitiatorPages.push(...pageData); + page++; + if (pageData.length < EXPORT_FETCH_LIMIT) { + hasMoreInitiator = false; + } } } - } + }; - return allPages; + // Fetch participant requests + const participantFetch = async () => { + let page = 1; + while (hasMoreParticipant && page <= maxPages) { + const pageResult = await workflowApi.listParticipantRequests({ + page, + limit: EXPORT_FETCH_LIMIT, + ...backendFilters + }); + + const pageData = pageResult?.data || []; + if (pageData.length === 0) { + hasMoreParticipant = false; + } else { + allParticipantPages.push(...pageData); + page++; + if (pageData.length < EXPORT_FETCH_LIMIT) { + hasMoreParticipant = false; + } + } + } + }; + + // Fetch both in parallel + await Promise.all([initiatorFetch(), participantFetch()]); + + // Filter out drafts + const nonDraftInitiator = allInitiatorPages.filter((req: any) => { + const reqStatus = (req.status || '').toString().toUpperCase(); + return reqStatus !== 'DRAFT'; + }); + + const nonDraftParticipant = allParticipantPages.filter((req: any) => { + const reqStatus = (req.status || '').toString().toUpperCase(); + return reqStatus !== 'DRAFT'; + }); + + // Merge and deduplicate by requestId + const mergedMap = new Map(); + + nonDraftInitiator.forEach((req: any) => { + const requestId = req.requestId || req.id; + if (requestId) { + mergedMap.set(requestId, req); + } + }); + + nonDraftParticipant.forEach((req: any) => { + const requestId = req.requestId || req.id; + if (requestId) { + mergedMap.set(requestId, req); + } + }); + + // Convert to array and sort by date + const mergedData = Array.from(mergedMap.values()); + mergedData.sort((a: any, b: any) => { + const dateA = new Date(a.updatedAt || a.createdAt || 0).getTime(); + const dateB = new Date(b.updatedAt || b.createdAt || 0).getTime(); + return dateB - dateA; + }); + + return mergedData; } diff --git a/src/pages/Settings/Settings.tsx b/src/pages/Settings/Settings.tsx index 00f023d..2992e5c 100644 --- a/src/pages/Settings/Settings.tsx +++ b/src/pages/Settings/Settings.tsx @@ -8,8 +8,7 @@ import { Palette, Lock, Calendar, - Sliders, - AlertCircle + Sliders } from 'lucide-react'; import { setupPushNotifications } from '@/utils/pushNotifications'; import { useAuth, isAdmin as checkIsAdmin } from '@/contexts/AuthContext'; @@ -386,21 +385,6 @@ export function Settings() {
- - {/* Info: Admin features not available */} - - -
-
- -
-
-

Admin Features Not Accessible

-

System configuration and holiday management require admin privileges. Contact your administrator for access.

-
-
-
-
)}
diff --git a/src/services/summaryApi.ts b/src/services/summaryApi.ts index fe57c3f..a66c165 100644 --- a/src/services/summaryApi.ts +++ b/src/services/summaryApi.ts @@ -125,3 +125,21 @@ export async function listMySummaries(params: { page?: number; limit?: number } }; } +/** + * Get summary by requestId (checks if summary exists without creating) + * Returns null if summary doesn't exist + */ +export async function getSummaryByRequestId(requestId: string): Promise { + try { + const res = await apiClient.get(`/summaries/request/${requestId}`); + return res.data.data; + } catch (error: any) { + // If summary doesn't exist (404), return null + if (error?.response?.status === 404) { + return null; + } + // For other errors, also return null + return null; + } +} + diff --git a/src/services/workflowApi.ts b/src/services/workflowApi.ts index a6e51cc..82ca126 100644 --- a/src/services/workflowApi.ts +++ b/src/services/workflowApi.ts @@ -230,8 +230,8 @@ export async function listMyWorkflows(params: { page?: number; limit?: number; s } // List requests where user is the initiator - for "My Requests" page -export async function listMyInitiatedWorkflows(params: { page?: number; limit?: number; search?: string; status?: string; priority?: string; department?: string; dateRange?: string; startDate?: string; endDate?: string } = {}) { - const { page = 1, limit = 20, search, status, priority, department, dateRange, startDate, endDate } = params; +export async function listMyInitiatedWorkflows(params: { page?: number; limit?: number; search?: string; status?: string; priority?: string; department?: string; slaCompliance?: string; dateRange?: string; startDate?: string; endDate?: string } = {}) { + const { page = 1, limit = 20, search, status, priority, department, slaCompliance, dateRange, startDate, endDate } = params; const res = await apiClient.get('/workflows/my-initiated', { params: { page, @@ -240,6 +240,7 @@ export async function listMyInitiatedWorkflows(params: { page?: number; limit?: status, priority, department, + slaCompliance, dateRange, startDate, endDate diff --git a/src/styles/globals.css b/src/styles/globals.css index 482a8f4..ba9656c 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -47,6 +47,7 @@ --re-gold: #c9b037; --re-dark: #1a1a1a; --re-light-green: #8a9b8e; + --re-red: #DA281C; } .dark { @@ -129,6 +130,7 @@ --color-re-gold: var(--re-gold); --color-re-dark: var(--re-dark); --color-re-light-green: var(--re-light-green); + --color-re-red: var(--re-red); } @layer base { diff --git a/src/utils/pushNotifications.ts b/src/utils/pushNotifications.ts index 5d3e625..f77506d 100644 --- a/src/utils/pushNotifications.ts +++ b/src/utils/pushNotifications.ts @@ -45,23 +45,79 @@ export async function subscribeUserToPush(register: ServiceWorkerRegistration) { throw new Error('Missing VAPID public key configuration'); } + // Validate VAPID key format (should be base64 URL-safe string) + if (!VAPID_PUBLIC_KEY || VAPID_PUBLIC_KEY.trim().length === 0) { + throw new Error('VAPID public key is empty. Please configure VITE_PUBLIC_VAPID_KEY in your environment variables.'); + } + + // Check if pushManager is available + if (!register.pushManager) { + throw new Error('Push manager is not available. Please ensure your browser supports push notifications and the service worker is properly registered.'); + } + // Check if already subscribed let subscription: PushSubscription; try { const existingSubscription = await register.pushManager.getSubscription(); if (existingSubscription) { - // Already subscribed, check if it's still valid - subscription = existingSubscription; + // Already subscribed, check if it's still valid by trying to use it + try { + // Verify the subscription is still valid + subscription = existingSubscription; + } catch (error) { + // Existing subscription is invalid, unsubscribe and create new one + console.warn('[Push] Existing subscription is invalid, creating new one...'); + await existingSubscription.unsubscribe().catch(() => { + // Ignore unsubscribe errors + }); + subscription = await register.pushManager.subscribe({ + userVisibleOnly: true, + applicationServerKey: urlBase64ToUint8Array(VAPID_PUBLIC_KEY) + }); + } } else { // Subscribe to push - subscription = await register.pushManager.subscribe({ - userVisibleOnly: true, - applicationServerKey: urlBase64ToUint8Array(VAPID_PUBLIC_KEY) - }); + try { + subscription = await register.pushManager.subscribe({ + userVisibleOnly: true, + applicationServerKey: urlBase64ToUint8Array(VAPID_PUBLIC_KEY) + }); + } catch (subscribeError: any) { + // If subscription fails, try to clear any invalid subscriptions and retry once + console.warn('[Push] Initial subscription failed, attempting to clear and retry...'); + try { + const allSubscriptions = await register.pushManager.getSubscription(); + if (allSubscriptions) { + await allSubscriptions.unsubscribe().catch(() => { + // Ignore unsubscribe errors + }); + } + // Retry subscription + subscription = await register.pushManager.subscribe({ + userVisibleOnly: true, + applicationServerKey: urlBase64ToUint8Array(VAPID_PUBLIC_KEY) + }); + } catch (retryError: any) { + // Provide more specific error messages + const errorMsg = subscribeError?.message || retryError?.message || 'Unknown error'; + if (errorMsg.includes('push service error') || errorMsg.includes('Registration failed')) { + throw new Error('Push service error: The browser\'s push service rejected the subscription. This may be due to an invalid VAPID key, network issues, or browser push service problems. Please verify your VAPID key configuration and try again.'); + } + throw new Error(`Failed to subscribe to push notifications: ${errorMsg}`); + } + } } } catch (error: any) { - throw new Error(`Failed to subscribe to push notifications: ${error?.message || 'Unknown error'}`); + // Provide more helpful error messages + const errorMsg = error?.message || 'Unknown error'; + if (errorMsg.includes('push service error') || errorMsg.includes('Registration failed')) { + throw new Error('Push service error: The browser\'s push service rejected the subscription. Please verify your VAPID key is correct and matches the backend configuration. If the problem persists, try clearing your browser cache and service workers.'); + } + if (errorMsg.includes('Invalid key')) { + throw new Error('Invalid VAPID key format. Please verify that VITE_PUBLIC_VAPID_KEY is correctly set and matches the backend VAPID_PUBLIC_KEY.'); + } + throw new Error(`Failed to subscribe to push notifications: ${errorMsg}`); } // Convert subscription to JSON format for backend diff --git a/tailwind.config.cjs b/tailwind.config.cjs index ca7b58b..01394bf 100644 --- a/tailwind.config.cjs +++ b/tailwind.config.cjs @@ -62,6 +62,7 @@ module.exports = { 're-gold': 'var(--re-gold)', 're-dark': 'var(--re-dark)', 're-light-green': 'var(--re-light-green)', + 're-red': 'var(--re-red)', }, borderRadius: { lg: 'var(--radius)', diff --git a/tailwind.config.ts b/tailwind.config.ts index 06e4978..95d041d 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -63,6 +63,7 @@ const config: Config = { 're-gold': 'var(--re-gold)', 're-dark': 'var(--re-dark)', 're-light-green': 'var(--re-light-green)', + 're-red': 'var(--re-red)', }, borderRadius: { lg: 'var(--radius)',