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() {
-
-
-
-
+
+
+
Approval Portal
-
- Royal Enfield
-
-
- Approval & Request Management Portal
-
@@ -58,12 +56,14 @@ export function Auth() {
{isLoading ? (
<>
-
+
Logging in...
>
) : (
diff --git a/src/pages/Auth/AuthCallback.tsx b/src/pages/Auth/AuthCallback.tsx
index f0a7ac2..c58b3e0 100644
--- a/src/pages/Auth/AuthCallback.tsx
+++ b/src/pages/Auth/AuthCallback.tsx
@@ -1,6 +1,7 @@
import { useEffect, useState } from 'react';
import { useAuth } from '@/contexts/AuthContext';
import { CheckCircle2, AlertCircle, Loader2 } from 'lucide-react';
+import { ReLogo } from '@/assets';
export function AuthCallback() {
const { isAuthenticated, isLoading, error, user } = useAuth();
@@ -59,24 +60,14 @@ export function AuthCallback() {
{/* Logo/Brand Section */}
-
-
-
-
+
+
+
Approval Portal
-
Royal Enfield
-
Approval Portal
{/* Main Loader Card */}
@@ -99,10 +90,10 @@ export function AuthCallback() {
) : (
-
+
{/* Outer rotating ring */}
-
-
+
+
)}
@@ -119,11 +110,11 @@ export function AuthCallback() {
{authStep !== 'error' && (
-
+
Validating credentials
{authStep === 'complete' && (
@@ -156,7 +147,7 @@ export function AuthCallback() {
);
diff --git a/src/pages/RequestDetail/RequestDetail.tsx b/src/pages/RequestDetail/RequestDetail.tsx
index b000e68..9c654ec 100644
--- a/src/pages/RequestDetail/RequestDetail.tsx
+++ b/src/pages/RequestDetail/RequestDetail.tsx
@@ -22,6 +22,7 @@ import {
Activity,
MessageSquare,
AlertTriangle,
+ FileCheck,
} from 'lucide-react';
import { Badge } from '@/components/ui/badge';
@@ -37,13 +38,14 @@ import { downloadDocument } from '@/services/workflowApi';
// Components
import { RequestDetailHeader } from './components/RequestDetailHeader';
import { ShareSummaryModal } from '@/components/modals/ShareSummaryModal';
-import { createSummary } from '@/services/summaryApi';
+import { createSummary, getSummaryDetails, type SummaryDetails } from '@/services/summaryApi';
import { toast } from 'sonner';
import { OverviewTab } from './components/tabs/OverviewTab';
import { WorkflowTab } from './components/tabs/WorkflowTab';
import { DocumentsTab } from './components/tabs/DocumentsTab';
import { ActivityTab } from './components/tabs/ActivityTab';
import { WorkNotesTab } from './components/tabs/WorkNotesTab';
+import { SummaryTab } from './components/tabs/SummaryTab';
import { QuickActionsSidebar } from './components/QuickActionsSidebar';
import { RequestDetailModals } from './components/RequestDetailModals';
import { RequestDetailProps } from './types/requestDetail.types';
@@ -100,6 +102,8 @@ function RequestDetailInner({ requestId: propRequestId, onBack, dynamicRequests
const [activeTab, setActiveTab] = useState(initialTab);
const [showShareSummaryModal, setShowShareSummaryModal] = useState(false);
const [summaryId, setSummaryId] = useState
(null);
+ const [summaryDetails, setSummaryDetails] = useState(null);
+ const [loadingSummary, setLoadingSummary] = useState(false);
const { user } = useAuth();
// Custom hooks
@@ -185,26 +189,80 @@ function RequestDetailInner({ requestId: propRequestId, onBack, dynamicRequests
}
try {
- // Check if summary already exists, if not create it
+ // Get or create summary (backend returns existing summary if it exists - idempotent)
+ // Summary should already exist from closure, but create if missing (handles edge cases)
let currentSummaryId = summaryId;
if (!currentSummaryId) {
const summary = await createSummary(apiRequest.requestId);
currentSummaryId = summary.summaryId;
setSummaryId(currentSummaryId);
+ // Refresh summary details after creating
+ try {
+ const details = await getSummaryDetails(currentSummaryId);
+ setSummaryDetails(details);
+ } catch (error) {
+ console.error('Failed to fetch summary details after creation:', error);
+ }
+ }
+ // Open share modal with the summary ID (only after we have the ID)
+ if (currentSummaryId) {
+ setShowShareSummaryModal(true);
}
- setShowShareSummaryModal(true);
} catch (error: any) {
console.error('Failed to create/get summary:', error);
- if (error?.response?.status === 400 && error?.response?.data?.message?.includes('already exists')) {
- // Summary already exists, try to get it
- toast.error('Summary already exists. Please refresh the page.');
- } else {
- toast.error(error?.response?.data?.message || 'Failed to prepare summary for sharing');
- }
+ const errorMessage = error?.response?.data?.error || error?.response?.data?.message || error?.message;
+ toast.error(errorMessage || 'Failed to prepare summary for sharing');
}
};
- const needsClosure = request?.status === 'approved' && isInitiator;
+ const needsClosure = (request?.status === 'approved' || request?.status === 'rejected') && isInitiator;
+
+ // Check if request is closed (or needs closure for approved/rejected)
+ const isClosed = request?.status === 'closed' || (request?.status === 'approved' && !isInitiator) || (request?.status === 'rejected' && !isInitiator);
+
+ // Fetch summary details if request is closed
+ // Summary should be automatically created when request is closed, but we'll create it if missing (idempotent)
+ useEffect(() => {
+ const fetchSummaryDetails = async () => {
+ if (!isClosed || !apiRequest?.requestId) {
+ setSummaryDetails(null);
+ setSummaryId(null);
+ return;
+ }
+
+ try {
+ setLoadingSummary(true);
+ // Use createSummary which is idempotent - returns existing summary if it exists, creates if missing
+ // This handles cases where summary creation failed during closure or was not created yet
+ const summary = await createSummary(apiRequest.requestId);
+ if (summary?.summaryId) {
+ setSummaryId(summary.summaryId);
+ // Fetch full summary details
+ try {
+ const details = await getSummaryDetails(summary.summaryId);
+ setSummaryDetails(details);
+ } catch (error: any) {
+ // If we can't get details, clear summary
+ console.error('Failed to fetch summary details:', error);
+ setSummaryDetails(null);
+ setSummaryId(null);
+ }
+ } else {
+ setSummaryDetails(null);
+ setSummaryId(null);
+ }
+ } catch (error: any) {
+ // If summary creation fails, don't show tab but log error
+ console.error('Summary not available:', error?.message);
+ setSummaryDetails(null);
+ setSummaryId(null);
+ } finally {
+ setLoadingSummary(false);
+ }
+ };
+
+ fetchSummaryDetails();
+ }, [isClosed, apiRequest?.requestId]);
// Get current levels for WorkNotesTab
const currentLevels = (request?.approvalFlow || [])
@@ -260,7 +318,7 @@ function RequestDetailInner({ requestId: propRequestId, onBack, dynamicRequests
{/* Tabs */}
-
+
Overview
+ {isClosed && summaryDetails && (
+
+
+ Summary
+
+ )}
+ {isClosed && (
+
+
+
+ )}
+
- {/* Quick Actions Card - Hide entire card for spectators */}
- {!isSpectator && (
+ {/* Quick Actions Card - Hide entire card for spectators and closed requests */}
+ {!isSpectator && request.status !== 'closed' && (
Quick Actions
@@ -92,13 +92,13 @@ export function QuickActionsSidebar({
)}
{/* Spectators Card */}
- {request.spectators && request.spectators.length > 0 && (
-
-
- Spectators
-
-
- {request.spectators.map((spectator: any, index: number) => (
+
+
+ Spectators
+
+
+ {request.spectators && request.spectators.length > 0 ? (
+ request.spectators.map((spectator: any, index: number) => (
@@ -110,10 +110,14 @@ export function QuickActionsSidebar({
{spectator.role}
- ))}
-
-
- )}
+ ))
+ ) : (
+
+ )}
+
+
);
}
diff --git a/src/pages/RequestDetail/components/tabs/OverviewTab.tsx b/src/pages/RequestDetail/components/tabs/OverviewTab.tsx
index 6bf4545..cc18859 100644
--- a/src/pages/RequestDetail/components/tabs/OverviewTab.tsx
+++ b/src/pages/RequestDetail/components/tabs/OverviewTab.tsx
@@ -210,15 +210,25 @@ export function OverviewTab({
{/* Conclusion Remark Section */}
{needsClosure && (
-
+
-
-
+
+
Conclusion Remark - Final Step
- All approvals are complete. Please review and finalize the conclusion to close this request.
+ {request.status === 'rejected'
+ ? 'This request was rejected. Please review the AI-generated closure remark and finalize it to close this request.'
+ : 'All approvals are complete. Please review and finalize the conclusion to close this request.'}
void;
+ isInitiator?: boolean;
+}
+
+export function SummaryTab({ summary, loading, onShare, isInitiator }: SummaryTabProps) {
+ const getStatusIcon = (status: string) => {
+ const statusLower = status.toLowerCase();
+ if (statusLower === 'approved') return ;
+ if (statusLower === 'rejected') return ;
+ if (statusLower === 'pending' || statusLower === 'in progress') return ;
+ return ;
+ };
+
+ const getStatusColor = (status: string) => {
+ const statusLower = status.toLowerCase();
+ if (statusLower === 'approved') return 'bg-green-100 text-green-700 border-green-300';
+ if (statusLower === 'rejected') return 'bg-red-100 text-red-700 border-red-300';
+ if (statusLower === 'pending' || statusLower === 'in progress') return 'bg-orange-100 text-orange-700 border-orange-300';
+ return 'bg-gray-100 text-gray-700 border-gray-300';
+ };
+
+ // Helper function to get designation or department (fallback to department if designation is N/A or empty)
+ const getDesignationOrDepartment = (designation?: string | null, department?: string | null) => {
+ if (designation && designation.trim() && designation.trim().toUpperCase() !== 'N/A') {
+ return designation;
+ }
+ if (department && department.trim() && department.trim().toUpperCase() !== 'N/A') {
+ return department;
+ }
+ return 'N/A';
+ };
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ if (!summary) {
+ return (
+
+
+
+
Summary Not Available
+
Summary has not been generated for this request yet.
+
+
+ );
+ }
+
+ return (
+
+ {/* Summary Card */}
+
+
+
+
+
{summary.title}
+
Request #{summary.requestNumber}
+
+ {isInitiator && onShare ? (
+
+
+ Share
+
+ ) : (
+
+ {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)}
+
+
+
+ {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)',