From 8f3f484dbce52917569cfa19d982f009d19e7d93 Mon Sep 17 00:00:00 2001 From: laxmanhalaki Date: Mon, 24 Nov 2025 21:19:33 +0530 Subject: [PATCH] reported bugs like preview issue and new requirement all request for normal user,share request is implemented --- index.html | 2 + src/App.tsx | 44 +- src/assets/images/Re_Logo.png | Bin 0 -> 30239 bytes src/assets/index.ts | 21 + .../common/FilePreview/FilePreview.tsx | 39 +- .../dashboard/StatsCard/StatsCard.tsx | 7 +- .../layout/PageLayout/PageLayout.tsx | 38 +- src/components/modals/ShareSummaryModal.tsx | 210 ++++++ .../NotificationStatusModal.tsx | 2 +- .../CreateRequest/ApprovalWorkflowStep.tsx | 87 ++- .../CreateRequest/ParticipantsStep.tsx | 56 ++ .../workflow/CreateRequest/WizardFooter.tsx | 4 +- src/hooks/useRequestDetails.ts | 19 +- .../components/sections/TATBreachReport.tsx | 16 +- src/pages/MyRequests/MyRequests.tsx | 108 ++- .../MyRequests/components/MyRequestsStats.tsx | 29 +- src/pages/MyRequests/hooks/useMyRequests.ts | 2 +- src/pages/RequestDetail/RequestDetail.tsx | 46 ++ .../components/RequestDetailHeader.tsx | 46 +- src/pages/Requests/Requests.tsx | 218 ++++-- src/pages/Requests/UserAllRequests.tsx | 684 ++++++++++++++++++ .../Requests/components/RequestsHeader.tsx | 11 +- .../Requests/components/RequestsStats.tsx | 30 +- .../Requests/services/requestsService.ts | 228 +++--- .../Requests/services/userRequestsService.ts | 129 ++++ .../Requests/utils/requestCalculations.ts | 11 +- src/pages/Requests/utils/requestFilters.ts | 8 + src/pages/Settings/Settings.tsx | 58 ++ src/pages/SharedSummaries/SharedSummaries.tsx | 236 ++++++ .../SharedSummaries/SharedSummaryDetail.tsx | 241 ++++++ src/services/authApi.ts | 18 +- src/services/dashboard.service.ts | 35 +- src/services/summaryApi.ts | 127 ++++ src/services/workflowApi.ts | 101 ++- src/utils/pushNotifications.ts | 17 +- src/vite-env.d.ts | 52 ++ 36 files changed, 2686 insertions(+), 294 deletions(-) create mode 100644 src/assets/images/Re_Logo.png create mode 100644 src/assets/index.ts create mode 100644 src/components/modals/ShareSummaryModal.tsx create mode 100644 src/pages/Requests/UserAllRequests.tsx create mode 100644 src/pages/Requests/services/userRequestsService.ts create mode 100644 src/pages/SharedSummaries/SharedSummaries.tsx create mode 100644 src/pages/SharedSummaries/SharedSummaryDetail.tsx create mode 100644 src/services/summaryApi.ts diff --git a/index.html b/index.html index 0488ca4..5846836 100644 --- a/index.html +++ b/index.html @@ -2,6 +2,8 @@ + + diff --git a/src/App.tsx b/src/App.tsx index 8d816b5..4bce3ae 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -5,11 +5,15 @@ import { Dashboard } from '@/pages/Dashboard'; import { OpenRequests } from '@/pages/OpenRequests'; import { ClosedRequests } from '@/pages/ClosedRequests'; import { RequestDetail } from '@/pages/RequestDetail'; +import { SharedSummaries } from '@/pages/SharedSummaries/SharedSummaries'; +import { SharedSummaryDetail } from '@/pages/SharedSummaries/SharedSummaryDetail'; import { WorkNotes } from '@/pages/WorkNotes'; import { CreateRequest } from '@/pages/CreateRequest'; import { ClaimManagementWizard } from '@/components/workflow/ClaimManagementWizard'; import { MyRequests } from '@/pages/MyRequests'; import { Requests } from '@/pages/Requests/Requests'; +import { UserAllRequests } from '@/pages/Requests/UserAllRequests'; +import { useAuth, hasManagementAccess } from '@/contexts/AuthContext'; import { ApproverPerformance } from '@/pages/ApproverPerformance/ApproverPerformance'; import { Profile } from '@/pages/Profile'; import { Settings } from '@/pages/Settings'; @@ -34,6 +38,22 @@ interface AppProps { onLogout?: () => void; } +// Component to conditionally render Admin or User All Requests screen +// This ensures that when navigating from the sidebar, the correct screen is shown based on user role +function RequestsRoute({ onViewRequest }: { onViewRequest: (requestId: string) => void }) { + const { user } = useAuth(); + const isAdmin = hasManagementAccess(user); + + // Render separate screens based on user role + // Admin/Management users see all organization requests + // Regular users see only their participant requests (approver/spectator, NOT initiator) + if (isAdmin) { + return ; + } else { + return ; + } +} + // Main Application Routes Component function AppRoutes({ onLogout }: AppProps) { const navigate = useNavigate(); @@ -487,6 +507,26 @@ function AppRoutes({ onLogout }: AppProps) { } /> + {/* Shared Summaries */} + + + + } + /> + + {/* Shared Summary Detail */} + + + + } + /> + {/* My Requests */} - {/* Requests - Advanced Filtering Screen (Admin/Management) */} + {/* Requests - Separate screens for Admin and Regular Users */} - + } /> diff --git a/src/assets/images/Re_Logo.png b/src/assets/images/Re_Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..6ceec941ba3712fd880c12775f1bb87bf662ae2a GIT binary patch literal 30239 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 ''); + throw new Error(`Failed to load file: ${response.status} ${response.statusText}. ${errorText}`); } const blob = await response.blob(); + + // Check if blob is valid + if (blob.size === 0) { + throw new Error('File is empty or could not be loaded'); + } + + // Verify blob type matches expected type + if (isPDF && !blob.type.includes('pdf') && blob.type !== 'application/octet-stream') { + console.warn(`Expected PDF but got ${blob.type}`); + } + const url = window.URL.createObjectURL(blob); setBlobUrl(url); } catch (err) { console.error('Failed to load file for preview:', err); - setError('Failed to load file for preview'); + setError(err instanceof Error ? err.message : 'Failed to load file for preview'); } finally { setLoading(false); } @@ -82,9 +105,10 @@ export function FilePreview({ return () => { if (blobUrl) { window.URL.revokeObjectURL(blobUrl); + setBlobUrl(null); } }; - }, [open, fileUrl, canPreview]); + }, [open, fileUrl, canPreview, isPDF]); const handleDownload = async () => { if (onDownload && attachmentId) { @@ -218,6 +242,9 @@ export function FilePreview({ minHeight: '70vh', height: '100%' }} + onError={() => { + setError('Failed to load PDF preview'); + }} /> )} diff --git a/src/components/dashboard/StatsCard/StatsCard.tsx b/src/components/dashboard/StatsCard/StatsCard.tsx index 22da5e4..1d2ddf2 100644 --- a/src/components/dashboard/StatsCard/StatsCard.tsx +++ b/src/components/dashboard/StatsCard/StatsCard.tsx @@ -10,6 +10,7 @@ interface StatsCardProps { textColor: string; valueColor: string; testId?: string; + onClick?: () => void; } export function StatsCard({ @@ -20,12 +21,14 @@ export function StatsCard({ gradient, textColor, valueColor, - testId = 'stats-card' + testId = 'stats-card', + onClick }: StatsCardProps) { return (
diff --git a/src/components/layout/PageLayout/PageLayout.tsx b/src/components/layout/PageLayout/PageLayout.tsx index 4309408..4f7c135 100644 --- a/src/components/layout/PageLayout/PageLayout.tsx +++ b/src/components/layout/PageLayout/PageLayout.tsx @@ -1,7 +1,6 @@ import { useState, useEffect, useMemo } from 'react'; -import { Bell, Settings, User, Plus, Search, Home, FileText, CheckCircle, LogOut, PanelLeft, PanelLeftClose, List } from 'lucide-react'; +import { Bell, Settings, User, Plus, Home, FileText, CheckCircle, LogOut, PanelLeft, PanelLeftClose, List, Share2 } from 'lucide-react'; import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; import { Badge } from '@/components/ui/badge'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'; @@ -15,8 +14,8 @@ import { AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/alert-dialog'; -import { useAuth, hasManagementAccess } from '@/contexts/AuthContext'; -import royalEnfieldLogo from '@/assets/images/royal_enfield_logo.png'; +import { useAuth } from '@/contexts/AuthContext'; +import { ReLogo } from '@/assets'; import notificationApi, { Notification } from '@/services/notificationApi'; import { getSocket, joinUserRoom } from '@/utils/socket'; import { formatDistanceToNow } from 'date-fns'; @@ -57,28 +56,23 @@ export function PageLayout({ children, currentPage = 'dashboard', onNavigate, on } }; - // Check if user has management access (ADMIN or MANAGEMENT role) - const isManagement = useMemo(() => hasManagementAccess(user), [user]); - const menuItems = useMemo(() => { const items = [ { id: 'dashboard', label: 'Dashboard', icon: Home }, + // Add "All Requests" for all users (admin sees org-level, regular users see their participant requests) + { id: 'requests', label: 'All Requests', icon: List }, ]; - // Add "All Requests" only for ADMIN and MANAGEMENT roles, right after Dashboard - if (isManagement) { - items.push({ id: 'requests', label: 'All Requests', icon: List }); - } - // Add remaining menu items items.push( { id: 'my-requests', label: 'My Requests', icon: User }, { id: 'open-requests', label: 'Open Requests', icon: FileText }, - { id: 'closed-requests', label: 'Closed Requests', icon: CheckCircle } + { id: 'closed-requests', label: 'Closed Requests', icon: CheckCircle }, + { id: 'shared-summaries', label: 'Shared Summary', icon: Share2 } ); return items; - }, [isManagement]); + }, []); const toggleSidebar = () => { setSidebarOpen(!sidebarOpen); @@ -228,16 +222,13 @@ export function PageLayout({ children, currentPage = 'dashboard', onNavigate, on `}>
-
+
Royal Enfield Logo -
-

Royal Enfield

-

Approval Portal

-
+

Approval Portal

@@ -292,13 +283,14 @@ export function PageLayout({ children, currentPage = 'dashboard', onNavigate, on > {sidebarOpen ? : } -
+ {/* Search bar commented out */} + {/*
-
+
*/}
diff --git a/src/components/modals/ShareSummaryModal.tsx b/src/components/modals/ShareSummaryModal.tsx new file mode 100644 index 0000000..0c2de64 --- /dev/null +++ b/src/components/modals/ShareSummaryModal.tsx @@ -0,0 +1,210 @@ +import { useState, useEffect } from 'react'; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Checkbox } from '@/components/ui/checkbox'; +import { Loader2, Search, User, X } from 'lucide-react'; +import { toast } from 'sonner'; +import { shareSummary } from '@/services/summaryApi'; +import { searchUsers } from '@/services/userApi'; + +interface ShareSummaryModalProps { + isOpen: boolean; + onClose: () => void; + summaryId: string; + requestTitle: string; + onSuccess?: () => void; +} + +export function ShareSummaryModal({ isOpen, onClose, summaryId, requestTitle, onSuccess }: ShareSummaryModalProps) { + const [searchTerm, setSearchTerm] = useState(''); + const [users, setUsers] = useState>([]); + const [selectedUserIds, setSelectedUserIds] = useState>(new Set()); + const [searching, setSearching] = useState(false); + const [sharing, setSharing] = useState(false); + + // Search users + useEffect(() => { + if (!isOpen || !searchTerm.trim()) { + setUsers([]); + return; + } + + const searchTimeout = setTimeout(async () => { + try { + setSearching(true); + const response = await searchUsers(searchTerm); + const results = response?.data?.data || response?.data || []; + setUsers(Array.isArray(results) ? results : []); + } catch (error) { + console.error('Failed to search users:', error); + toast.error('Failed to search users'); + } finally { + setSearching(false); + } + }, 300); + + return () => clearTimeout(searchTimeout); + }, [searchTerm, isOpen]); + + const handleToggleUser = (userId: string) => { + setSelectedUserIds(prev => { + const newSet = new Set(prev); + if (newSet.has(userId)) { + newSet.delete(userId); + } else { + newSet.add(userId); + } + return newSet; + }); + }; + + const handleShare = async () => { + if (selectedUserIds.size === 0) { + toast.error('Please select at least one user to share with'); + return; + } + + try { + setSharing(true); + await shareSummary(summaryId, Array.from(selectedUserIds)); + toast.success(`Summary shared with ${selectedUserIds.size} user(s)`); + setSelectedUserIds(new Set()); + setSearchTerm(''); + setUsers([]); + onSuccess?.(); + onClose(); + } catch (error: any) { + console.error('Failed to share summary:', error); + toast.error(error?.response?.data?.message || 'Failed to share summary'); + } finally { + setSharing(false); + } + }; + + const handleClose = () => { + setSelectedUserIds(new Set()); + setSearchTerm(''); + setUsers([]); + onClose(); + }; + + return ( + + + + Share Summary + + +
+
+ +

{requestTitle}

+
+ +
+ +
+ + setSearchTerm(e.target.value)} + className="pl-10" + /> +
+
+ + {searching && ( +
+ +
+ )} + + {!searching && users.length > 0 && ( +
+ {users.map((user) => ( +
handleToggleUser(user.userId)} + > + handleToggleUser(user.userId)} + /> +
+
+ +

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

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

{user.designation || user.department}

+ )} +

{user.email}

+
+
+ ))} +
+ )} + + {!searching && searchTerm && users.length === 0 && ( +
+ No users found +
+ )} + + {selectedUserIds.size > 0 && ( +
+

+ Selected ({selectedUserIds.size}) +

+
+ {Array.from(selectedUserIds).map((userId) => { + const user = users.find(u => u.userId === userId); + return ( +
+ {user?.displayName || user?.email || userId} + +
+ ); + })} +
+
+ )} +
+ + + + + +
+
+ ); +} + diff --git a/src/components/settings/NotificationStatusModal/NotificationStatusModal.tsx b/src/components/settings/NotificationStatusModal/NotificationStatusModal.tsx index d55e749..6cf46f9 100644 --- a/src/components/settings/NotificationStatusModal/NotificationStatusModal.tsx +++ b/src/components/settings/NotificationStatusModal/NotificationStatusModal.tsx @@ -47,7 +47,7 @@ export function NotificationStatusModal({

Subscription Failed

-

+

{message || 'Unable to enable push notifications. Please check your browser settings and try again.'}

diff --git a/src/components/workflow/CreateRequest/ApprovalWorkflowStep.tsx b/src/components/workflow/CreateRequest/ApprovalWorkflowStep.tsx index b3f160e..8247d32 100644 --- a/src/components/workflow/CreateRequest/ApprovalWorkflowStep.tsx +++ b/src/components/workflow/CreateRequest/ApprovalWorkflowStep.tsx @@ -1,4 +1,5 @@ import { motion } from 'framer-motion'; +import { useEffect } from 'react'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; @@ -36,6 +37,32 @@ export function ApprovalWorkflowStep({ }: ApprovalWorkflowStepProps) { const { userSearchResults, userSearchLoading, searchUsersForIndex, clearSearchForIndex } = useMultiUserSearch(); + // Initialize approvers array when approverCount changes - moved from render to useEffect + useEffect(() => { + const approverCount = formData.approverCount || 1; + const currentApprovers = formData.approvers || []; + + // Ensure we have the correct number of approvers + if (currentApprovers.length < approverCount) { + const newApprovers = [...currentApprovers]; + // Fill missing approver slots + for (let i = currentApprovers.length; i < approverCount; i++) { + if (!newApprovers[i]) { + newApprovers[i] = { + email: '', + name: '', + level: i + 1, + tat: '' as any + }; + } + } + updateFormData('approvers', newApprovers); + } else if (currentApprovers.length > approverCount) { + // Trim excess approvers if count was reduced + updateFormData('approvers', currentApprovers.slice(0, approverCount)); + } + }, [formData.approverCount, updateFormData]); + const handleApproverEmailChange = (index: number, value: string) => { const newApprovers = [...formData.approvers]; const previousEmail = newApprovers[index]?.email; @@ -61,6 +88,36 @@ export function ApprovalWorkflowStep({ const handleUserSelect = async (index: number, selectedUser: any) => { try { + // Check for duplicates in other approver slots (excluding current index) + const isDuplicateApprover = formData.approvers?.some( + (approver: any, idx: number) => + idx !== index && + (approver.userId === selectedUser.userId || approver.email?.toLowerCase() === selectedUser.email?.toLowerCase()) + ); + + if (isDuplicateApprover) { + onValidationError({ + type: 'error', + email: selectedUser.email, + message: 'This user is already added as an approver in another level.' + }); + return; + } + + // Check for duplicates in spectators + const isDuplicateSpectator = formData.spectators?.some( + (spectator: any) => spectator.userId === selectedUser.userId || spectator.email?.toLowerCase() === selectedUser.email?.toLowerCase() + ); + + if (isDuplicateSpectator) { + onValidationError({ + type: 'error', + email: selectedUser.email, + message: 'This user is already added as a spectator. A user cannot be both an approver and a spectator.' + }); + return; + } + const dbUser = await ensureUserExists({ userId: selectedUser.userId, email: selectedUser.email, @@ -210,11 +267,13 @@ export function ApprovalWorkflowStep({ const level = index + 1; const isLast = level === (formData.approverCount || 1); - if (!formData.approvers[index]) { - const newApprovers = [...formData.approvers]; - newApprovers[index] = { email: '', name: '', level: level, tat: '' as any }; - updateFormData('approvers', newApprovers); - } + // Ensure approver exists (should be initialized by useEffect, but provide fallback) + const approver = formData.approvers[index] || { + email: '', + name: '', + level: level, + tat: '' as any + }; return (
@@ -223,13 +282,13 @@ export function ApprovalWorkflowStep({
@@ -250,7 +309,7 @@ export function ApprovalWorkflowStep({ - {formData.approvers[index]?.email && formData.approvers[index]?.userId && ( + {approver.email && approver.userId && ( Verified @@ -262,7 +321,7 @@ export function ApprovalWorkflowStep({ id={`approver-${level}`} type="email" placeholder="approver@royalenfield.com" - value={formData.approvers[index]?.email || ''} + value={approver.email || ''} onChange={(e) => handleApproverEmailChange(index, e.target.value)} className="h-10 border-2 border-gray-300 focus:border-blue-500 mt-1 w-full" data-testid={`approval-workflow-approver-${level}-email-input`} @@ -300,17 +359,17 @@ export function ApprovalWorkflowStep({ { const newApprovers = [...formData.approvers]; newApprovers[index] = { ...newApprovers[index], tat: parseInt(e.target.value) || '', level: level, - tatType: formData.approvers[index]?.tatType || 'hours' + tatType: approver.tatType || 'hours' }; updateFormData('approvers', newApprovers); }} @@ -318,7 +377,7 @@ export function ApprovalWorkflowStep({ data-testid={`approval-workflow-approver-${level}-tat-input`} /> filters.setSearchTerm(e.target.value)} + className="pl-10 h-10" + data-testid="search-input" + /> +
+ + + + + + + + +
+ + {/* User Filters - Initiator and Approver */} +
+ {/* Initiator Filter */} +
+ +
+ {initiatorSearch.selectedUser ? ( +
+ + {initiatorSearch.selectedUser.displayName || initiatorSearch.selectedUser.email} + + +
+ ) : ( + <> + initiatorSearch.handleSearch(e.target.value)} + onFocus={() => { + if (initiatorSearch.searchResults.length > 0) { + initiatorSearch.setShowResults(true); + } + }} + onBlur={() => setTimeout(() => initiatorSearch.setShowResults(false), 200)} + className="h-10" + data-testid="initiator-search-input" + /> + {initiatorSearch.showResults && initiatorSearch.searchResults.length > 0 && ( +
+ {initiatorSearch.searchResults.map((user) => ( + + ))} +
+ )} + + )} +
+
+ + {/* Approver Filter */} +
+
+ + {filters.approverFilter !== 'all' && ( + + )} +
+
+ {approverSearch.selectedUser ? ( +
+ + {approverSearch.selectedUser.displayName || approverSearch.selectedUser.email} + + +
+ ) : ( + <> + approverSearch.handleSearch(e.target.value)} + onFocus={() => { + if (approverSearch.searchResults.length > 0) { + approverSearch.setShowResults(true); + } + }} + onBlur={() => setTimeout(() => approverSearch.setShowResults(false), 200)} + className="h-10" + data-testid="approver-search-input" + /> + {approverSearch.showResults && approverSearch.searchResults.length > 0 && ( +
+ {approverSearch.searchResults.map((user) => ( + + ))} +
+ )} + + )} +
+
+
+ + {/* Date Range Filter */} +
+ + + + {filters.dateRange === 'custom' && ( + + + + + +
+
+
+ + { + const date = e.target.value ? new Date(e.target.value) : undefined; + if (date) { + filters.setCustomStartDate(date); + if (filters.customEndDate && date > filters.customEndDate) { + filters.setCustomEndDate(date); + } + } else { + filters.setCustomStartDate(undefined); + } + }} + max={format(new Date(), 'yyyy-MM-dd')} + className="w-full" + /> +
+
+ + { + const date = e.target.value ? new Date(e.target.value) : undefined; + if (date) { + filters.setCustomEndDate(date); + if (filters.customStartDate && date < filters.customStartDate) { + filters.setCustomStartDate(date); + } + } else { + filters.setCustomEndDate(undefined); + } + }} + min={filters.customStartDate ? format(filters.customStartDate, 'yyyy-MM-dd') : undefined} + max={format(new Date(), 'yyyy-MM-dd')} + className="w-full" + /> +
+
+
+ + +
+
+
+
+ )} +
+
+ + + + {/* Requests List */} + + + {/* Pagination */} + +
+ ); +} + diff --git a/src/pages/Requests/components/RequestsHeader.tsx b/src/pages/Requests/components/RequestsHeader.tsx index 1ff0648..b51f3be 100644 --- a/src/pages/Requests/components/RequestsHeader.tsx +++ b/src/pages/Requests/components/RequestsHeader.tsx @@ -8,18 +8,14 @@ import { PageHeader } from '@/components/common/PageHeader'; interface RequestsHeaderProps { isOrgLevel: boolean; - totalRequests: number; loading: boolean; - loadingStats: boolean; exporting: boolean; onExport: () => void; } export function RequestsHeader({ isOrgLevel, - totalRequests, loading, - loadingStats, exporting, onExport }: RequestsHeaderProps) { @@ -27,15 +23,10 @@ export function RequestsHeader({
+
+
+
+ ))} +
+ + {/* Pagination */} + {totalPages > 1 && ( +
+
+ Showing {((currentPage - 1) * itemsPerPage) + 1} to {Math.min(currentPage * itemsPerPage, totalRecords)} of {totalRecords} summaries +
+
+ + + Page {currentPage} of {totalPages} + + +
+
+ )} + + )} +
+ + ); +} + diff --git a/src/pages/SharedSummaries/SharedSummaryDetail.tsx b/src/pages/SharedSummaries/SharedSummaryDetail.tsx new file mode 100644 index 0000000..f153c5c --- /dev/null +++ b/src/pages/SharedSummaries/SharedSummaryDetail.tsx @@ -0,0 +1,241 @@ +import { useState, useEffect } from 'react'; +import { useParams, useNavigate } from 'react-router-dom'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { Loader2, ArrowLeft, FileText, CheckCircle, XCircle, Clock } from 'lucide-react'; +import { getSummaryDetails, markAsViewed, type SummaryDetails } from '@/services/summaryApi'; +import { format } from 'date-fns'; +import { toast } from 'sonner'; + +export function SharedSummaryDetail() { + const { sharedSummaryId } = useParams<{ sharedSummaryId: string }>(); + const navigate = useNavigate(); + const [summary, setSummary] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + if (!sharedSummaryId) { + navigate('/shared-summaries'); + return; + } + + const fetchSummary = async () => { + try { + setLoading(true); + // First, mark as viewed + try { + await markAsViewed(sharedSummaryId); + } catch (error) { + console.warn('Failed to mark as viewed:', error); + } + // Then get the summary details + // Note: We need to get the summaryId from the shared summary first + // For now, we'll use the sharedSummaryId to get details + // The backend should handle this, but we might need to adjust the API + const details = await getSummaryDetails(sharedSummaryId); + setSummary(details); + } catch (error: any) { + console.error('Failed to fetch summary details:', error); + toast.error(error?.response?.data?.message || 'Failed to load summary'); + navigate('/shared-summaries'); + } finally { + setLoading(false); + } + }; + + fetchSummary(); + }, [sharedSummaryId, navigate]); + + 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 ( +
+
+ +

Loading summary...

+
+
+ ); + } + + if (!summary) { + return ( +
+
+ +

Summary Not Found

+

The summary you're looking for doesn't exist.

+ +
+
+ ); + } + + return ( +
+
+ {/* Header */} +
+ +

Request Summary

+
+ + {/* Summary Card */} +
+
+
+
+

{summary.title}

+

Request #{summary.requestNumber}

+
+ + {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')} +

+
+
+ {/* Initiator remarks commented out - remarks won't come while initiating */} + {/*
+

Remarks by Concern

+

{summary.initiator.remarks}

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

Workflow

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

+ 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/services/authApi.ts b/src/services/authApi.ts index 3e13da8..e37f09e 100644 --- a/src/services/authApi.ts +++ b/src/services/authApi.ts @@ -31,12 +31,28 @@ apiClient.interceptors.request.use( } ); -// Response interceptor to handle token refresh +// Response interceptor to handle token refresh and connection errors apiClient.interceptors.response.use( (response) => response, async (error) => { const originalRequest = error.config; + // Handle connection errors gracefully in development + if (error.code === 'ERR_NETWORK' || error.code === 'ECONNREFUSED' || error.message?.includes('ERR_CONNECTION_REFUSED')) { + const isDevelopment = import.meta.env.DEV || import.meta.env.MODE === 'development'; + + if (isDevelopment) { + // In development, log a helpful message instead of spamming console + console.warn(`[API] Backend not reachable at ${API_BASE_URL}. Make sure the backend server is running on port 5000.`); + // Don't throw - let the calling code handle it gracefully + return Promise.reject({ + ...error, + isConnectionError: true, + message: 'Backend server is not reachable. Please ensure the backend is running on port 5000.' + }); + } + } + // If error is 401 and we haven't retried yet if (error.response?.status === 401 && !originalRequest._retry) { originalRequest._retry = true; diff --git a/src/services/dashboard.service.ts b/src/services/dashboard.service.ts index 3f58011..158cb98 100644 --- a/src/services/dashboard.service.ts +++ b/src/services/dashboard.service.ts @@ -181,13 +181,46 @@ class DashboardService { /** * Get request statistics */ - async getRequestStats(dateRange?: DateRange, startDate?: string, endDate?: string): Promise { + async getRequestStats( + dateRange?: DateRange, + startDate?: string, + endDate?: string, + priority?: string, + department?: string, + initiator?: string, + approver?: string, + approverType?: 'current' | 'any', + search?: string, + slaCompliance?: string + ): Promise { try { const params: any = { dateRange }; if (dateRange === 'custom' && startDate && endDate) { params.startDate = startDate; params.endDate = endDate; } + // Add filters (excluding status - stats should show all statuses) + if (priority && priority !== 'all') { + params.priority = priority; + } + if (department && department !== 'all') { + params.department = department; + } + if (initiator && initiator !== 'all') { + params.initiator = initiator; + } + if (approver && approver !== 'all') { + params.approver = approver; + } + if (approverType) { + params.approverType = approverType; + } + if (search) { + params.search = search; + } + if (slaCompliance && slaCompliance !== 'all') { + params.slaCompliance = slaCompliance; + } const response = await apiClient.get('/dashboard/stats/requests', { params }); return response.data.data; } catch (error) { diff --git a/src/services/summaryApi.ts b/src/services/summaryApi.ts new file mode 100644 index 0000000..fe57c3f --- /dev/null +++ b/src/services/summaryApi.ts @@ -0,0 +1,127 @@ +import apiClient from './authApi'; + +export interface RequestSummary { + summaryId: string; + requestId: string; + initiatorId: string; + title: string; + description: string | null; + closingRemarks: string | null; + isAiGenerated: boolean; + conclusionId: string | null; + createdAt: string; + updatedAt: string; +} + +export interface SharedSummary { + sharedSummaryId: string; + summaryId: string; + requestId: string; + requestNumber: string; + title: string; + initiatorName: string; + sharedByName: string; + sharedAt: string; + viewedAt: string | null; + isRead: boolean; + closureDate: string | null; +} + +export interface SummaryDetails { + summaryId: string; + requestId: string; + requestNumber: string; + title: string; + description: string; + closingRemarks: string; + isAiGenerated: boolean; + createdAt: string; + initiator: { + name: string; + designation: string; + department: string | null; + email: string; + status: string; + timestamp: string; + remarks: string; + }; + approvers: Array<{ + levelNumber: number; + levelName: string; + name: string; + designation: string; + department: string | null; + email: string; + status: string; + timestamp: string; + remarks: string; + }>; + workflow: { + priority: string; + status: string; + submissionDate: string | null; + closureDate: string | null; + }; +} + +/** + * Create a summary for a closed request + */ +export async function createSummary(requestId: string): Promise { + const res = await apiClient.post('/summaries', { requestId }); + return res.data.data; +} + +/** + * Get summary details + */ +export async function getSummaryDetails(summaryId: string): Promise { + const res = await apiClient.get(`/summaries/${summaryId}`); + return res.data.data; +} + +/** + * Share summary with users + */ +export async function shareSummary(summaryId: string, userIds: string[]): Promise { + const res = await apiClient.post(`/summaries/${summaryId}/share`, { userIds }); + return res.data.data; +} + +/** + * List summaries shared with current user + */ +export async function listSharedSummaries(params: { page?: number; limit?: number } = {}): Promise<{ + data: SharedSummary[]; + pagination: { page: number; limit: number; total: number; totalPages: number }; +}> { + const { page = 1, limit = 20 } = params; + const res = await apiClient.get('/summaries/shared', { params: { page, limit } }); + return { + data: res.data.data?.data || res.data.data || [], + pagination: res.data.data?.pagination || { page, limit, total: 0, totalPages: 1 } + }; +} + +/** + * Mark shared summary as viewed + */ +export async function markAsViewed(sharedSummaryId: string): Promise { + await apiClient.patch(`/summaries/shared/${sharedSummaryId}/view`); +} + +/** + * List summaries created by current user + */ +export async function listMySummaries(params: { page?: number; limit?: number } = {}): Promise<{ + data: RequestSummary[]; + pagination: { page: number; limit: number; total: number; totalPages: number }; +}> { + const { page = 1, limit = 20 } = params; + const res = await apiClient.get('/summaries/my', { params: { page, limit } }); + return { + data: res.data.data?.data || res.data.data || [], + pagination: res.data.data?.pagination || { page, limit, total: 0, totalPages: 1 } + }; +} + diff --git a/src/services/workflowApi.ts b/src/services/workflowApi.ts index 2fe1b93..a6e51cc 100644 --- a/src/services/workflowApi.ts +++ b/src/services/workflowApi.ts @@ -153,15 +153,98 @@ export async function createWorkflowMultipart(form: CreateWorkflowFromFormPayloa return { id: data?.requestId } as any; } -export async function listWorkflows(params: { page?: number; limit?: number } = {}) { - const { page = 1, limit = 20 } = params; - const res = await apiClient.get('/workflows', { params: { page, limit } }); +export async function listWorkflows(params: { page?: number; limit?: number; search?: string; status?: string; priority?: string; department?: string; initiator?: string; approver?: string; slaCompliance?: string; dateRange?: string; startDate?: string; endDate?: string } = {}) { + const { page = 1, limit = 20, search, status, priority, department, initiator, approver, slaCompliance, dateRange, startDate, endDate } = params; + const res = await apiClient.get('/workflows', { + params: { + page, + limit, + search, + status, + priority, + department, + initiator, + approver, + slaCompliance, + dateRange, + startDate, + endDate + } + }); return res.data?.data || res.data; } -export async function listMyWorkflows(params: { page?: number; limit?: number; search?: string; status?: string; priority?: string } = {}) { - const { page = 1, limit = 20, search, status, priority } = params; - const res = await apiClient.get('/workflows/my', { params: { page, limit, search, status, priority } }); +// List requests where user is a participant (not initiator) - for regular users' "All Requests" page +// SEPARATE from listWorkflows (admin) to avoid interference +export async function listParticipantRequests(params: { page?: number; limit?: number; search?: string; status?: string; priority?: string; department?: string; initiator?: string; approver?: string; approverType?: string; slaCompliance?: string; dateRange?: string; startDate?: string; endDate?: string } = {}) { + const { page = 1, limit = 20, search, status, priority, department, initiator, approver, approverType, slaCompliance, dateRange, startDate, endDate } = params; + const res = await apiClient.get('/workflows/participant-requests', { + params: { + page, + limit, + search, + status, + priority, + department, + initiator, + approver, + approverType, + slaCompliance, + dateRange, + startDate, + endDate + } + }); + // Response structure: { success, data: { data: [...], pagination: {...} } } + return { + data: res.data?.data?.data || res.data?.data || [], + pagination: res.data?.data?.pagination || { page, limit, total: 0, totalPages: 1 } + }; +} + +// DEPRECATED: Use listParticipantRequests instead +// List requests where user is a participant (not initiator) - for "All Requests" page +export async function listMyWorkflows(params: { page?: number; limit?: number; search?: string; status?: string; priority?: string; department?: string; initiator?: string; approver?: string; slaCompliance?: string; dateRange?: string; startDate?: string; endDate?: string } = {}) { + const { page = 1, limit = 20, search, status, priority, department, initiator, approver, slaCompliance, dateRange, startDate, endDate } = params; + const res = await apiClient.get('/workflows/my', { + params: { + page, + limit, + search, + status, + priority, + department, + initiator, + approver, + slaCompliance, + dateRange, + startDate, + endDate + } + }); + // Response structure: { success, data: { data: [...], pagination: {...} } } + return { + data: res.data?.data?.data || res.data?.data || [], + pagination: res.data?.data?.pagination || { page, limit, total: 0, totalPages: 1 } + }; +} + +// 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; + const res = await apiClient.get('/workflows/my-initiated', { + params: { + page, + limit, + search, + status, + priority, + department, + dateRange, + startDate, + endDate + } + }); // Response structure: { success, data: { data: [...], pagination: {...} } } return { data: res.data?.data?.data || res.data?.data || [], @@ -330,8 +413,10 @@ export async function downloadWorkNoteAttachment(attachmentId: string): Promise< export default { createWorkflowFromForm, createWorkflowMultipart, - listWorkflows, - listMyWorkflows, + listWorkflows, // Admin: All organization requests + listParticipantRequests, // Regular users: Participant requests only (not initiator) + listMyWorkflows, // DEPRECATED: Use listParticipantRequests + listMyInitiatedWorkflows, // Regular users: Initiator requests only listOpenForMe, listClosedByMe, submitWorkflow, diff --git a/src/utils/pushNotifications.ts b/src/utils/pushNotifications.ts index 4da3e54..5d3e625 100644 --- a/src/utils/pushNotifications.ts +++ b/src/utils/pushNotifications.ts @@ -112,15 +112,24 @@ export async function setupPushNotifications() { throw new Error('Notifications are not supported in this browser'); } - // Request permission + // Check permission status let permission = Notification.permission; - if (permission === 'default') { - permission = await Notification.requestPermission(); + if (permission === 'denied') { + throw new Error('Notification permission was denied. Please enable notifications in your browser settings and try again.'); } + if (permission === 'default') { + // Request permission if not already requested + permission = await Notification.requestPermission(); + if (permission !== 'granted') { + throw new Error('Notification permission was denied. Please enable notifications in your browser settings and try again.'); + } + } + + // Final check - permission should be 'granted' at this point if (permission !== 'granted') { - throw new Error('Notification permission was denied. Please enable notifications in your browser settings.'); + throw new Error('Notification permission is required. Please grant permission and try again.'); } // Register service worker (or get existing) diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index fc29386..d8abad8 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -13,3 +13,55 @@ interface ImportMeta { readonly env: ImportMetaEnv; } +// Image type declarations +declare module '*.png' { + const src: string; + export default src; +} + +declare module '*.jpg' { + const src: string; + export default src; +} + +declare module '*.jpeg' { + const src: string; + export default src; +} + +declare module '*.gif' { + const src: string; + export default src; +} + +declare module '*.svg' { + const src: string; + export default src; +} + +declare module '*.webp' { + const src: string; + export default src; +} + +// Font type declarations (for future use) +declare module '*.woff' { + const src: string; + export default src; +} + +declare module '*.woff2' { + const src: string; + export default src; +} + +declare module '*.ttf' { + const src: string; + export default src; +} + +declare module '*.otf' { + const src: string; + export default src; +} +