notification preferances added approver performance api altererd resume added for initiator also

This commit is contained in:
laxmanhalaki 2025-12-03 20:01:37 +05:30
parent 1bebf3a46a
commit 7358c3ff30
29 changed files with 1047 additions and 179 deletions

View File

@ -59,7 +59,7 @@ export function AIConfig() {
openaiApiKey: configMap['OPENAI_API_KEY'] || '', openaiApiKey: configMap['OPENAI_API_KEY'] || '',
geminiApiKey: configMap['GEMINI_API_KEY'] || '', geminiApiKey: configMap['GEMINI_API_KEY'] || '',
aiRemarkGeneration: configMap['AI_REMARK_GENERATION_ENABLED'] === 'true', aiRemarkGeneration: configMap['AI_REMARK_GENERATION_ENABLED'] === 'true',
maxRemarkChars: parseInt(configMap['AI_REMARK_MAX_CHARACTERS'] || '500') maxRemarkChars: parseInt(configMap['AI_MAX_REMARK_LENGTH'] || '2000')
}); });
} catch (error: any) { } catch (error: any) {
console.error('Failed to load AI configurations:', error); console.error('Failed to load AI configurations:', error);
@ -81,7 +81,7 @@ export function AIConfig() {
updateConfiguration('OPENAI_API_KEY', config.openaiApiKey), updateConfiguration('OPENAI_API_KEY', config.openaiApiKey),
updateConfiguration('GEMINI_API_KEY', config.geminiApiKey), updateConfiguration('GEMINI_API_KEY', config.geminiApiKey),
updateConfiguration('AI_REMARK_GENERATION_ENABLED', config.aiRemarkGeneration.toString()), updateConfiguration('AI_REMARK_GENERATION_ENABLED', config.aiRemarkGeneration.toString()),
updateConfiguration('AI_REMARK_MAX_CHARACTERS', config.maxRemarkChars.toString()) updateConfiguration('AI_MAX_REMARK_LENGTH', config.maxRemarkChars.toString())
]); ]);
toast.success('AI configuration saved successfully'); toast.success('AI configuration saved successfully');

View File

@ -26,19 +26,19 @@ export function AIParameters({
<CardContent> <CardContent>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="max-remark-chars" className="text-sm font-medium"> <Label htmlFor="max-remark-chars" className="text-sm font-medium">
Maximum Remark Characters Maximum Remark Length
</Label> </Label>
<Input <Input
id="max-remark-chars" id="max-remark-chars"
type="number" type="number"
min="100" min="500"
max="2000" max="5000"
value={maxRemarkChars} value={maxRemarkChars}
onChange={(e) => onMaxRemarkCharsChange(parseInt(e.target.value) || 500)} onChange={(e) => onMaxRemarkCharsChange(parseInt(e.target.value) || 2000)}
className="border-gray-200 focus:border-re-green focus:ring-2 focus:ring-re-green/20" className="border-gray-200 focus:border-re-green focus:ring-2 focus:ring-re-green/20"
/> />
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
Maximum character limit for AI-generated conclusion remarks (100-2000 characters) Maximum character length for AI-generated conclusion remarks (500-5000 characters)
</p> </p>
</div> </div>
</CardContent> </CardContent>

View File

@ -258,9 +258,9 @@ export function ConfigurationManager({ onConfigUpdate }: ConfigurationManagerPro
} }
}; };
// Filter out notification rules, dashboard layout categories, and allow external sharing // Filter out dashboard layout category and specific config keys
const excludedCategories = ['NOTIFICATION_RULES', 'DASHBOARD_LAYOUT']; const excludedCategories = ['DASHBOARD_LAYOUT'];
const excludedConfigKeys = ['ALLOW_EXTERNAL_SHARING']; const excludedConfigKeys = ['ALLOW_EXTERNAL_SHARING', 'NOTIFICATION_BATCH_DELAY_MS', 'AI_REMARK_MAX_CHARACTERS'];
const filteredConfigurations = configurations.filter( const filteredConfigurations = configurations.filter(
config => !excludedCategories.includes(config.configCategory) && config => !excludedCategories.includes(config.configCategory) &&
!excludedConfigKeys.includes(config.configKey) !excludedConfigKeys.includes(config.configKey)

View File

@ -0,0 +1,192 @@
import { useState, useEffect } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Switch } from '@/components/ui/switch';
import { Label } from '@/components/ui/label';
import { Bell, Mail, MessageSquare, Loader2, CheckCircle, AlertCircle } from 'lucide-react';
import { getNotificationPreferences, updateNotificationPreferences, NotificationPreferences } from '@/services/userPreferenceApi';
export function NotificationPreferencesCard() {
const [preferences, setPreferences] = useState<NotificationPreferences>({
emailNotificationsEnabled: true,
pushNotificationsEnabled: true,
inAppNotificationsEnabled: true
});
const [loading, setLoading] = useState(true);
const [updating, setUpdating] = useState<string | null>(null);
const [successMessage, setSuccessMessage] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
loadPreferences();
}, []);
const loadPreferences = async () => {
try {
setLoading(true);
setError(null);
const data = await getNotificationPreferences();
setPreferences(data);
} catch (err: any) {
console.error('[NotificationPreferences] Failed to load preferences:', err);
setError(err.response?.data?.message || 'Failed to load notification preferences');
} finally {
setLoading(false);
}
};
const handleToggle = async (key: keyof NotificationPreferences, value: boolean) => {
try {
setUpdating(key);
setError(null);
setSuccessMessage(null);
const updateData = { [key]: value };
const updated = await updateNotificationPreferences(updateData);
setPreferences(updated);
// Show success message
const prefName = key === 'emailNotificationsEnabled' ? 'Email'
: key === 'pushNotificationsEnabled' ? 'Push'
: 'In-App';
setSuccessMessage(`${prefName} notifications ${value ? 'enabled' : 'disabled'}`);
setTimeout(() => setSuccessMessage(null), 3000);
} catch (err: any) {
console.error('[NotificationPreferences] Failed to update preference:', err);
setError(err.response?.data?.message || 'Failed to update notification preference');
// Revert the UI change
loadPreferences();
} finally {
setUpdating(null);
}
};
if (loading) {
return (
<Card className="shadow-lg hover:shadow-xl transition-all duration-300 border-0 rounded-md">
<CardContent className="p-12 flex items-center justify-center">
<Loader2 className="w-8 h-8 animate-spin text-gray-400" />
</CardContent>
</Card>
);
}
return (
<Card className="shadow-lg hover:shadow-xl transition-all duration-300 border-0 rounded-md group">
<CardHeader className="pb-4">
<div className="flex items-center gap-3">
<div className="p-3 bg-gradient-to-br from-slate-600 to-slate-700 rounded-md shadow-md group-hover:shadow-lg transition-shadow">
<Bell className="h-5 w-5 text-white" />
</div>
<div>
<CardTitle className="text-lg font-semibold text-gray-900">Notification Preferences</CardTitle>
<CardDescription className="text-sm text-gray-600">Control how you receive notifications</CardDescription>
</div>
</div>
</CardHeader>
<CardContent className="space-y-6">
{/* Success Message */}
{successMessage && (
<div className="flex items-center gap-2 p-3 bg-green-50 border border-green-200 rounded-md">
<CheckCircle className="w-4 h-4 text-green-600 shrink-0" />
<p className="text-sm text-green-800 font-medium">{successMessage}</p>
</div>
)}
{/* Error Message */}
{error && (
<div className="flex items-center gap-2 p-3 bg-red-50 border border-red-200 rounded-md">
<AlertCircle className="w-4 h-4 text-red-600 shrink-0" />
<p className="text-sm text-red-800 font-medium">{error}</p>
</div>
)}
{/* Email Notifications */}
<div className="flex items-center justify-between p-4 bg-gradient-to-br from-gray-50 to-gray-100 rounded-md border border-gray-200 hover:border-gray-300 transition-colors">
<div className="flex items-center gap-3">
<div className="p-2 bg-white rounded-md shadow-sm">
<Mail className="w-5 h-5 text-slate-600" />
</div>
<div>
<Label htmlFor="email-notifications" className="text-sm font-semibold text-gray-900 cursor-pointer">
Email Notifications
</Label>
<p className="text-xs text-gray-600 mt-0.5">Receive notifications via email</p>
</div>
</div>
<div className="flex items-center gap-2">
{updating === 'emailNotificationsEnabled' && (
<Loader2 className="w-4 h-4 animate-spin text-gray-400" />
)}
<Switch
id="email-notifications"
checked={preferences.emailNotificationsEnabled}
onCheckedChange={(checked) => handleToggle('emailNotificationsEnabled', checked)}
disabled={updating === 'emailNotificationsEnabled'}
/>
</div>
</div>
{/* Push Notifications */}
<div className="flex items-center justify-between p-4 bg-gradient-to-br from-gray-50 to-gray-100 rounded-md border border-gray-200 hover:border-gray-300 transition-colors">
<div className="flex items-center gap-3">
<div className="p-2 bg-white rounded-md shadow-sm">
<Bell className="w-5 h-5 text-slate-600" />
</div>
<div>
<Label htmlFor="push-notifications" className="text-sm font-semibold text-gray-900 cursor-pointer">
Push Notifications
</Label>
<p className="text-xs text-gray-600 mt-0.5">Receive browser push notifications</p>
</div>
</div>
<div className="flex items-center gap-2">
{updating === 'pushNotificationsEnabled' && (
<Loader2 className="w-4 h-4 animate-spin text-gray-400" />
)}
<Switch
id="push-notifications"
checked={preferences.pushNotificationsEnabled}
onCheckedChange={(checked) => handleToggle('pushNotificationsEnabled', checked)}
disabled={updating === 'pushNotificationsEnabled'}
/>
</div>
</div>
{/* In-App Notifications */}
<div className="flex items-center justify-between p-4 bg-gradient-to-br from-gray-50 to-gray-100 rounded-md border border-gray-200 hover:border-gray-300 transition-colors">
<div className="flex items-center gap-3">
<div className="p-2 bg-white rounded-md shadow-sm">
<MessageSquare className="w-5 h-5 text-slate-600" />
</div>
<div>
<Label htmlFor="inapp-notifications" className="text-sm font-semibold text-gray-900 cursor-pointer">
In-App Notifications
</Label>
<p className="text-xs text-gray-600 mt-0.5">Show notifications within the application</p>
</div>
</div>
<div className="flex items-center gap-2">
{updating === 'inAppNotificationsEnabled' && (
<Loader2 className="w-4 h-4 animate-spin text-gray-400" />
)}
<Switch
id="inapp-notifications"
checked={preferences.inAppNotificationsEnabled}
onCheckedChange={(checked) => handleToggle('inAppNotificationsEnabled', checked)}
disabled={updating === 'inAppNotificationsEnabled'}
/>
</div>
</div>
<div className="pt-4 border-t border-gray-200">
<p className="text-xs text-gray-500 text-center">
These preferences control which notification channels you receive alerts through.
You'll still receive critical notifications regardless of these settings.
</p>
</div>
</CardContent>
</Card>
);
}

View File

@ -0,0 +1,224 @@
import { useState, useEffect } from 'react';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Switch } from '@/components/ui/switch';
import { Label } from '@/components/ui/label';
import { Bell, Mail, MessageSquare, Loader2, CheckCircle, AlertCircle, Settings } from 'lucide-react';
import { getNotificationPreferences, updateNotificationPreferences, NotificationPreferences } from '@/services/userPreferenceApi';
import { Separator } from '@/components/ui/separator';
interface NotificationPreferencesModalProps {
open: boolean;
onClose: () => void;
}
export function NotificationPreferencesModal({ open, onClose }: NotificationPreferencesModalProps) {
const [preferences, setPreferences] = useState<NotificationPreferences>({
emailNotificationsEnabled: true,
pushNotificationsEnabled: true,
inAppNotificationsEnabled: true
});
const [loading, setLoading] = useState(true);
const [updating, setUpdating] = useState<string | null>(null);
const [successMessage, setSuccessMessage] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (open) {
loadPreferences();
}
}, [open]);
const loadPreferences = async () => {
try {
setLoading(true);
setError(null);
const data = await getNotificationPreferences();
setPreferences(data);
} catch (err: any) {
console.error('[NotificationPreferences] Failed to load preferences:', err);
setError(err.response?.data?.message || 'Failed to load notification preferences');
} finally {
setLoading(false);
}
};
const handleToggle = async (key: keyof NotificationPreferences, value: boolean) => {
try {
setUpdating(key);
setError(null);
setSuccessMessage(null);
const updateData = { [key]: value };
const updated = await updateNotificationPreferences(updateData);
setPreferences(updated);
// Show success message
const prefName = key === 'emailNotificationsEnabled' ? 'Email'
: key === 'pushNotificationsEnabled' ? 'Push'
: 'In-App';
setSuccessMessage(`${prefName} notifications ${value ? 'enabled' : 'disabled'}`);
setTimeout(() => setSuccessMessage(null), 3000);
} catch (err: any) {
console.error('[NotificationPreferences] Failed to update preference:', err);
setError(err.response?.data?.message || 'Failed to update notification preference');
// Revert the UI change
loadPreferences();
} finally {
setUpdating(null);
}
};
return (
<Dialog open={open} onOpenChange={onClose}>
<DialogContent className="sm:max-w-[600px] max-h-[85vh] overflow-y-auto">
<DialogHeader>
<div className="flex items-center gap-3">
<div className="p-2.5 bg-gradient-to-br from-slate-600 to-slate-700 rounded-lg">
<Settings className="h-6 w-6 text-white" />
</div>
<div>
<DialogTitle className="text-xl font-semibold">Notification Preferences</DialogTitle>
<DialogDescription className="text-sm">
Customize how you receive notifications for workflow updates
</DialogDescription>
</div>
</div>
</DialogHeader>
<Separator className="my-4" />
{loading ? (
<div className="p-12 flex items-center justify-center">
<Loader2 className="w-8 h-8 animate-spin text-gray-400" />
</div>
) : (
<div className="space-y-6">
{/* Success Message */}
{successMessage && (
<div className="flex items-center gap-2 p-3 bg-green-50 border border-green-200 rounded-md animate-in fade-in slide-in-from-top-2">
<CheckCircle className="w-4 h-4 text-green-600 shrink-0" />
<p className="text-sm text-green-800 font-medium">{successMessage}</p>
</div>
)}
{/* Error Message */}
{error && (
<div className="flex items-center gap-2 p-3 bg-red-50 border border-red-200 rounded-md animate-in fade-in slide-in-from-top-2">
<AlertCircle className="w-4 h-4 text-red-600 shrink-0" />
<p className="text-sm text-red-800 font-medium">{error}</p>
</div>
)}
{/* Email Notifications */}
<div className="space-y-2">
<div className="flex items-center justify-between p-4 bg-gradient-to-br from-gray-50 to-gray-100 rounded-lg border border-gray-200 hover:border-gray-300 transition-all">
<div className="flex items-center gap-4">
<div className="p-3 bg-white rounded-lg shadow-sm">
<Mail className="w-6 h-6 text-slate-600" />
</div>
<div>
<Label htmlFor="email-notifications-modal" className="text-base font-semibold text-gray-900 cursor-pointer">
Email Notifications
</Label>
<p className="text-sm text-gray-600 mt-1">
Receive important updates and alerts via email
</p>
</div>
</div>
<div className="flex items-center gap-2">
{updating === 'emailNotificationsEnabled' && (
<Loader2 className="w-4 h-4 animate-spin text-gray-400" />
)}
<Switch
id="email-notifications-modal"
checked={preferences.emailNotificationsEnabled}
onCheckedChange={(checked) => handleToggle('emailNotificationsEnabled', checked)}
disabled={updating === 'emailNotificationsEnabled'}
/>
</div>
</div>
</div>
{/* Push Notifications */}
<div className="space-y-2">
<div className="flex items-center justify-between p-4 bg-gradient-to-br from-gray-50 to-gray-100 rounded-lg border border-gray-200 hover:border-gray-300 transition-all">
<div className="flex items-center gap-4">
<div className="p-3 bg-white rounded-lg shadow-sm">
<Bell className="w-6 h-6 text-slate-600" />
</div>
<div>
<Label htmlFor="push-notifications-modal" className="text-base font-semibold text-gray-900 cursor-pointer">
Push Notifications
</Label>
<p className="text-sm text-gray-600 mt-1">
Get instant browser notifications for real-time updates
</p>
</div>
</div>
<div className="flex items-center gap-2">
{updating === 'pushNotificationsEnabled' && (
<Loader2 className="w-4 h-4 animate-spin text-gray-400" />
)}
<Switch
id="push-notifications-modal"
checked={preferences.pushNotificationsEnabled}
onCheckedChange={(checked) => handleToggle('pushNotificationsEnabled', checked)}
disabled={updating === 'pushNotificationsEnabled'}
/>
</div>
</div>
</div>
{/* In-App Notifications */}
<div className="space-y-2">
<div className="flex items-center justify-between p-4 bg-gradient-to-br from-gray-50 to-gray-100 rounded-lg border border-gray-200 hover:border-gray-300 transition-all">
<div className="flex items-center gap-4">
<div className="p-3 bg-white rounded-lg shadow-sm">
<MessageSquare className="w-6 h-6 text-slate-600" />
</div>
<div>
<Label htmlFor="inapp-notifications-modal" className="text-base font-semibold text-gray-900 cursor-pointer">
In-App Notifications
</Label>
<p className="text-sm text-gray-600 mt-1">
View notifications in the notification center
</p>
</div>
</div>
<div className="flex items-center gap-2">
{updating === 'inAppNotificationsEnabled' && (
<Loader2 className="w-4 h-4 animate-spin text-gray-400" />
)}
<Switch
id="inapp-notifications-modal"
checked={preferences.inAppNotificationsEnabled}
onCheckedChange={(checked) => handleToggle('inAppNotificationsEnabled', checked)}
disabled={updating === 'inAppNotificationsEnabled'}
/>
</div>
</div>
</div>
<Separator />
{/* Info Section */}
<div className="p-4 bg-gray-50 border border-gray-200 rounded-lg">
<p className="text-sm text-gray-700 leading-relaxed">
<span className="font-semibold">Note:</span> These settings control your notification preferences across all channels.
Critical system alerts and urgent notifications may still be delivered regardless of these settings to ensure important information reaches you.
</p>
</div>
</div>
)}
</DialogContent>
</Dialog>
);
}

View File

@ -0,0 +1,176 @@
import { useState, useEffect } from 'react';
import { Switch } from '@/components/ui/switch';
import { Label } from '@/components/ui/label';
import { Bell, Mail, MessageSquare, Loader2, CheckCircle, AlertCircle } from 'lucide-react';
import { getNotificationPreferences, updateNotificationPreferences, NotificationPreferences } from '@/services/userPreferenceApi';
export function NotificationPreferencesSimple() {
const [preferences, setPreferences] = useState<NotificationPreferences>({
emailNotificationsEnabled: true,
pushNotificationsEnabled: true,
inAppNotificationsEnabled: true
});
const [loading, setLoading] = useState(true);
const [updating, setUpdating] = useState<string | null>(null);
const [successMessage, setSuccessMessage] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
loadPreferences();
}, []);
const loadPreferences = async () => {
try {
setLoading(true);
setError(null);
const data = await getNotificationPreferences();
setPreferences(data);
} catch (err: any) {
console.error('[NotificationPreferences] Failed to load preferences:', err);
setError(err.response?.data?.message || 'Failed to load notification preferences');
} finally {
setLoading(false);
}
};
const handleToggle = async (key: keyof NotificationPreferences, value: boolean) => {
try {
setUpdating(key);
setError(null);
setSuccessMessage(null);
const updateData = { [key]: value };
const updated = await updateNotificationPreferences(updateData);
setPreferences(updated);
// Show success message
const prefName = key === 'emailNotificationsEnabled' ? 'Email'
: key === 'pushNotificationsEnabled' ? 'Push'
: 'In-App';
setSuccessMessage(`${prefName} notifications ${value ? 'enabled' : 'disabled'}`);
setTimeout(() => setSuccessMessage(null), 3000);
} catch (err: any) {
console.error('[NotificationPreferences] Failed to update preference:', err);
setError(err.response?.data?.message || 'Failed to update notification preference');
// Revert the UI change
loadPreferences();
} finally {
setUpdating(null);
}
};
if (loading) {
return (
<div className="p-8 flex items-center justify-center">
<Loader2 className="w-6 h-6 animate-spin text-gray-400" />
</div>
);
}
return (
<div className="space-y-4">
{/* Success Message */}
{successMessage && (
<div className="flex items-center gap-2 p-3 bg-green-50 border border-green-200 rounded-md">
<CheckCircle className="w-4 h-4 text-green-600 shrink-0" />
<p className="text-sm text-green-800 font-medium">{successMessage}</p>
</div>
)}
{/* Error Message */}
{error && (
<div className="flex items-center gap-2 p-3 bg-red-50 border border-red-200 rounded-md">
<AlertCircle className="w-4 h-4 text-red-600 shrink-0" />
<p className="text-sm text-red-800 font-medium">{error}</p>
</div>
)}
{/* Email Notifications */}
<div className="flex items-center justify-between p-4 bg-gradient-to-br from-gray-50 to-gray-100 rounded-md border border-gray-200 hover:border-gray-300 transition-colors">
<div className="flex items-center gap-3">
<div className="p-2 bg-white rounded-md shadow-sm">
<Mail className="w-5 h-5 text-slate-600" />
</div>
<div>
<Label htmlFor="email-notifications" className="text-sm font-semibold text-gray-900 cursor-pointer">
Email Notifications
</Label>
<p className="text-xs text-gray-600 mt-0.5">Receive notifications via email</p>
</div>
</div>
<div className="flex items-center gap-2">
{updating === 'emailNotificationsEnabled' && (
<Loader2 className="w-4 h-4 animate-spin text-gray-400" />
)}
<Switch
id="email-notifications"
checked={preferences.emailNotificationsEnabled}
onCheckedChange={(checked) => handleToggle('emailNotificationsEnabled', checked)}
disabled={updating === 'emailNotificationsEnabled'}
/>
</div>
</div>
{/* Push Notifications */}
<div className="flex items-center justify-between p-4 bg-gradient-to-br from-gray-50 to-gray-100 rounded-md border border-gray-200 hover:border-gray-300 transition-colors">
<div className="flex items-center gap-3">
<div className="p-2 bg-white rounded-md shadow-sm">
<Bell className="w-5 h-5 text-slate-600" />
</div>
<div>
<Label htmlFor="push-notifications" className="text-sm font-semibold text-gray-900 cursor-pointer">
Push Notifications
</Label>
<p className="text-xs text-gray-600 mt-0.5">Receive browser push notifications</p>
</div>
</div>
<div className="flex items-center gap-2">
{updating === 'pushNotificationsEnabled' && (
<Loader2 className="w-4 h-4 animate-spin text-gray-400" />
)}
<Switch
id="push-notifications"
checked={preferences.pushNotificationsEnabled}
onCheckedChange={(checked) => handleToggle('pushNotificationsEnabled', checked)}
disabled={updating === 'pushNotificationsEnabled'}
/>
</div>
</div>
{/* In-App Notifications */}
<div className="flex items-center justify-between p-4 bg-gradient-to-br from-gray-50 to-gray-100 rounded-md border border-gray-200 hover:border-gray-300 transition-colors">
<div className="flex items-center gap-3">
<div className="p-2 bg-white rounded-md shadow-sm">
<MessageSquare className="w-5 h-5 text-slate-600" />
</div>
<div>
<Label htmlFor="inapp-notifications" className="text-sm font-semibold text-gray-900 cursor-pointer">
In-App Notifications
</Label>
<p className="text-xs text-gray-600 mt-0.5">Show notifications within the application</p>
</div>
</div>
<div className="flex items-center gap-2">
{updating === 'inAppNotificationsEnabled' && (
<Loader2 className="w-4 h-4 animate-spin text-gray-400" />
)}
<Switch
id="inapp-notifications"
checked={preferences.inAppNotificationsEnabled}
onCheckedChange={(checked) => handleToggle('inAppNotificationsEnabled', checked)}
disabled={updating === 'inAppNotificationsEnabled'}
/>
</div>
</div>
<div className="pt-2 border-t border-gray-200">
<p className="text-xs text-gray-500 text-center">
These preferences control which notification channels you receive alerts through.
You'll still receive critical notifications regardless of these settings.
</p>
</div>
</div>
);
}

View File

@ -639,9 +639,9 @@ export function ApprovalStepCard({
</p> </p>
)} )}
{/* Skip Approver Button - Only show for initiator on pending/in-review/paused levels */} {/* Skip Approver Button - Only show for initiator on pending/in-review levels (not when paused) */}
{/* When paused, initiator can skip the approver which will negate the pause */} {/* User must resume first before skipping */}
{isInitiator && (isActive || isPaused || step.status === 'pending') && !isCompleted && !isRejected && step.levelId && onSkipApprover && ( {isInitiator && !isPaused && (isActive || step.status === 'pending') && !isCompleted && !isRejected && step.levelId && onSkipApprover && (
<div className="mt-2 sm:mt-3 pt-2 sm:pt-3 border-t border-gray-200"> <div className="mt-2 sm:mt-3 pt-2 sm:pt-3 border-t border-gray-200">
<Button <Button
variant="outline" variant="outline"

View File

@ -640,26 +640,36 @@ export function useRequestDetails(
if (!requestIdentifier || !apiRequest) return; if (!requestIdentifier || !apiRequest) return;
const socket = getSocket(); const socket = getSocket();
if (!socket) return; if (!socket) {
console.warn('[useRequestDetails] Socket not available');
return;
}
console.log('[useRequestDetails] Setting up socket listener for request:', apiRequest.requestId);
/** /**
* Handler: Request updated by another user * Handler: Request updated by another user
* Silently refresh to show latest changes * Silently refresh to show latest changes
*/ */
const handleRequestUpdated = (data: any) => { const handleRequestUpdated = (data: any) => {
console.log('[useRequestDetails] 📡 Received request:updated event:', data);
// Verify this update is for the current request // Verify this update is for the current request
if (data?.requestId === apiRequest.requestId || data?.requestNumber === requestIdentifier) { if (data?.requestId === apiRequest.requestId || data?.requestNumber === requestIdentifier) {
console.log('[useRequestDetails] 🔄 Request updated remotely, refreshing silently...'); console.log('[useRequestDetails] 🔄 Request updated remotely, refreshing silently...');
// Silent refresh - no loading state, no user interruption // Silent refresh - no loading state, no user interruption
refreshDetails(); refreshDetails();
} else {
console.log('[useRequestDetails] ⚠️ Event for different request, ignoring');
} }
}; };
// Register listener // Register listener
socket.on('request:updated', handleRequestUpdated); socket.on('request:updated', handleRequestUpdated);
console.log('[useRequestDetails] ✅ Registered listener for request:updated');
// Cleanup on unmount // Cleanup on unmount
return () => { return () => {
console.log('[useRequestDetails] 🧹 Cleaning up socket listener');
socket.off('request:updated', handleRequestUpdated); socket.off('request:updated', handleRequestUpdated);
}; };
}, [requestIdentifier, apiRequest, refreshDetails]); }, [requestIdentifier, apiRequest, refreshDetails]);

View File

@ -4,7 +4,6 @@
* This is a refactored version that uses modular components, hooks, and utilities. * This is a refactored version that uses modular components, hooks, and utilities.
*/ */
import { useMemo } from 'react';
import { useSearchParams } from 'react-router-dom'; import { useSearchParams } from 'react-router-dom';
// Components // Components
@ -19,9 +18,6 @@ import { ApproverPerformanceRequestList } from './components/ApproverPerformance
import { useApproverPerformanceFilters } from './hooks/useApproverPerformanceFilters'; import { useApproverPerformanceFilters } from './hooks/useApproverPerformanceFilters';
import { useApproverPerformanceData } from './hooks/useApproverPerformanceData'; import { useApproverPerformanceData } from './hooks/useApproverPerformanceData';
// Utils
import { calculateApproverStats } from './utils/statsCalculations';
const itemsPerPage = 10; const itemsPerPage = 10;
export function ApproverPerformance() { export function ApproverPerformance() {
@ -47,10 +43,7 @@ export function ApproverPerformance() {
itemsPerPage itemsPerPage
}); });
// Calculate stats from filtered data // All stats come from backend (data.approverStats) - no frontend calculation needed
const calculatedStats = useMemo(() => {
return calculateApproverStats(data.allFilteredRequests);
}, [data.allFilteredRequests]);
if (!approverId) { if (!approverId) {
return <ApproverPerformanceEmpty />; return <ApproverPerformanceEmpty />;
@ -69,16 +62,19 @@ export function ApproverPerformance() {
{data.approverStats && ( {data.approverStats && (
<ApproverPerformanceStatsCards <ApproverPerformanceStatsCards
approverStats={data.approverStats} approverStats={data.approverStats}
calculatedStats={calculatedStats}
/> />
)} )}
{/* Filtered Request Stats - Approver's Actions */} {/* Filtered Request Stats - Approver's Actions */}
<ApproverPerformanceActionsStats {data.approverStats && (
approverName={approverName} <ApproverPerformanceActionsStats
approverStats={data.approverStats} approverName={approverName}
calculatedStats={calculatedStats} approverStats={data.approverStats}
/> dateRange={filters.dateRange}
customStartDate={filters.customStartDate}
customEndDate={filters.customEndDate}
/>
)}
{/* Filters */} {/* Filters */}
<ApproverPerformanceFilters <ApproverPerformanceFilters

View File

@ -4,26 +4,30 @@
import { CheckCircle, XCircle, Clock, FileText, Users, Target, Award, AlertCircle, BarChart3, Archive } from 'lucide-react'; import { CheckCircle, XCircle, Clock, FileText, Users, Target, Award, AlertCircle, BarChart3, Archive } from 'lucide-react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import type { ApproverPerformance } from '@/services/dashboard.service'; import type { ApproverPerformance, DateRange } from '@/services/dashboard.service';
import type { ApproverPerformanceStats } from '../types/approverPerformance.types'; import { formatDateRangeText } from '@/pages/Dashboard/utils/dateRangeFormatter';
interface ApproverPerformanceActionsStatsProps { interface ApproverPerformanceActionsStatsProps {
approverName: string; approverName: string;
approverStats: ApproverPerformance | null; approverStats: ApproverPerformance;
calculatedStats: ApproverPerformanceStats; dateRange?: DateRange;
customStartDate?: Date;
customEndDate?: Date;
} }
export function ApproverPerformanceActionsStats({ export function ApproverPerformanceActionsStats({
approverName, approverName,
approverStats, approverStats,
calculatedStats dateRange = 'all',
customStartDate,
customEndDate
}: ApproverPerformanceActionsStatsProps) { }: ApproverPerformanceActionsStatsProps) {
return ( return (
<Card data-testid="approver-actions-stats"> <Card data-testid="approver-actions-stats">
<CardHeader> <CardHeader>
<CardTitle>Approver's Actions (Filtered)</CardTitle> <CardTitle>Approver's Actions</CardTitle>
<CardDescription> <CardDescription>
Statistics based on {approverName}'s actions with current filters applied Statistics for all requests by {approverName} {dateRange && dateRange !== 'all' ? formatDateRangeText(dateRange, customStartDate, customEndDate, '') : '(all time)'}
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
@ -38,41 +42,45 @@ export function ApproverPerformanceActionsStats({
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
<CheckCircle className="w-5 h-5 text-green-600" /> <CheckCircle className="w-5 h-5 text-green-600" />
<span className="text-xs text-green-600 font-medium"> <span className="text-xs text-green-600 font-medium">
{calculatedStats.completedActions > 0 ? `${calculatedStats.approvalRate}%` : '0%'} {approverStats && (approverStats.approvedCount + approverStats.rejectedCount) > 0
? `${Math.round((approverStats.approvedCount / (approverStats.approvedCount + approverStats.rejectedCount)) * 100)}%`
: '0%'}
</span> </span>
</div> </div>
<div className="text-2xl font-bold text-green-700">{calculatedStats.approvedByApprover}</div> <div className="text-2xl font-bold text-green-700">{approverStats?.approvedCount || 0}</div>
<div className="text-xs text-gray-600 mt-1">Approved by Approver</div> <div className="text-xs text-gray-600 mt-1">Approved by Approver</div>
</div> </div>
<div className="p-4 bg-red-50 rounded-lg border border-red-200"> <div className="p-4 bg-red-50 rounded-lg border border-red-200">
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
<XCircle className="w-5 h-5 text-red-600" /> <XCircle className="w-5 h-5 text-red-600" />
<span className="text-xs text-red-600 font-medium"> <span className="text-xs text-red-600 font-medium">
{calculatedStats.completedActions > 0 ? `${calculatedStats.rejectionRate}%` : '0%'} {approverStats && (approverStats.approvedCount + approverStats.rejectedCount) > 0
? `${Math.round((approverStats.rejectedCount / (approverStats.approvedCount + approverStats.rejectedCount)) * 100)}%`
: '0%'}
</span> </span>
</div> </div>
<div className="text-2xl font-bold text-red-700">{calculatedStats.rejectedByApprover}</div> <div className="text-2xl font-bold text-red-700">{approverStats?.rejectedCount || 0}</div>
<div className="text-xs text-gray-600 mt-1">Rejected by Approver</div> <div className="text-xs text-gray-600 mt-1">Rejected by Approver</div>
</div> </div>
<div className="p-4 bg-yellow-50 rounded-lg border border-yellow-200"> <div className="p-4 bg-yellow-50 rounded-lg border border-yellow-200">
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
<Clock className="w-5 h-5 text-yellow-600" /> <Clock className="w-5 h-5 text-yellow-600" />
</div> </div>
<div className="text-2xl font-bold text-yellow-700">{calculatedStats.pendingByApprover}</div> <div className="text-2xl font-bold text-yellow-700">{approverStats?.pendingCount || 0}</div>
<div className="text-xs text-gray-600 mt-1">Pending Actions</div> <div className="text-xs text-gray-600 mt-1">Pending Actions</div>
</div> </div>
<div className="p-4 bg-gray-50 rounded-lg border border-gray-200"> <div className="p-4 bg-gray-50 rounded-lg border border-gray-200">
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
<Archive className="w-5 h-5 text-gray-600" /> <Archive className="w-5 h-5 text-gray-600" />
</div> </div>
<div className="text-2xl font-bold text-gray-700">{calculatedStats.closedByApprover}</div> <div className="text-2xl font-bold text-gray-700">{approverStats?.closedCount || 0}</div>
<div className="text-xs text-gray-600 mt-1">Closed Requests</div> <div className="text-xs text-gray-600 mt-1">Closed Requests</div>
</div> </div>
<div className="p-4 bg-blue-50 rounded-lg border border-blue-200"> <div className="p-4 bg-blue-50 rounded-lg border border-blue-200">
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
<FileText className="w-5 h-5 text-blue-600" /> <FileText className="w-5 h-5 text-blue-600" />
</div> </div>
<div className="text-2xl font-bold text-blue-700">{calculatedStats.total}</div> <div className="text-2xl font-bold text-blue-700">{approverStats?.totalApproved || 0}</div>
<div className="text-xs text-gray-600 mt-1">Total Requests</div> <div className="text-xs text-gray-600 mt-1">Total Requests</div>
</div> </div>
</div> </div>
@ -89,24 +97,24 @@ export function ApproverPerformanceActionsStats({
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
<Award className="w-5 h-5 text-green-600" /> <Award className="w-5 h-5 text-green-600" />
<span className="text-xs text-green-600 font-medium"> <span className="text-xs text-green-600 font-medium">
{approverStats?.tatCompliancePercent !== undefined ? `${approverStats.tatCompliancePercent}%` : (calculatedStats.completedActions > 0 ? `${calculatedStats.tatComplianceRate}%` : 'N/A')} {approverStats?.tatCompliancePercent !== undefined ? `${approverStats.tatCompliancePercent}%` : 'N/A'}
</span> </span>
</div> </div>
<div className="text-2xl font-bold text-green-700">{calculatedStats.compliant}</div> <div className="text-2xl font-bold text-green-700">{approverStats?.withinTatCount || 0}</div>
<div className="text-xs text-gray-600 mt-1">TAT Compliant</div> <div className="text-xs text-gray-600 mt-1">TAT Compliant</div>
</div> </div>
<div className="p-4 bg-red-50 rounded-lg border border-red-200"> <div className="p-4 bg-red-50 rounded-lg border border-red-200">
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
<AlertCircle className="w-5 h-5 text-red-600" /> <AlertCircle className="w-5 h-5 text-red-600" />
</div> </div>
<div className="text-2xl font-bold text-red-700">{calculatedStats.breached}</div> <div className="text-2xl font-bold text-red-700">{approverStats?.breachedCount || 0}</div>
<div className="text-xs text-gray-600 mt-1">TAT Breached</div> <div className="text-xs text-gray-600 mt-1">TAT Breached</div>
</div> </div>
<div className="p-4 bg-purple-50 rounded-lg border border-purple-200"> <div className="p-4 bg-purple-50 rounded-lg border border-purple-200">
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
<BarChart3 className="w-5 h-5 text-purple-600" /> <BarChart3 className="w-5 h-5 text-purple-600" />
</div> </div>
<div className="text-2xl font-bold text-purple-700">{calculatedStats.completedActions}</div> <div className="text-2xl font-bold text-purple-700">{approverStats ? (approverStats.approvedCount + approverStats.rejectedCount) : 0}</div>
<div className="text-xs text-gray-600 mt-1">Completed Actions</div> <div className="text-xs text-gray-600 mt-1">Completed Actions</div>
</div> </div>
</div> </div>

View File

@ -136,9 +136,12 @@ export function ApproverPerformanceFilters({
<SelectValue placeholder="Date Range" /> <SelectValue placeholder="Date Range" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="all">All Time</SelectItem>
<SelectItem value="today">Today</SelectItem> <SelectItem value="today">Today</SelectItem>
<SelectItem value="week">This Week</SelectItem> <SelectItem value="week">This Week</SelectItem>
<SelectItem value="month">This Month</SelectItem> <SelectItem value="month">This Month</SelectItem>
<SelectItem value="last7days">Last 7 Days</SelectItem>
<SelectItem value="last30days">Last 30 Days</SelectItem>
<SelectItem value="custom">Custom Range</SelectItem> <SelectItem value="custom">Custom Range</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
@ -219,7 +222,7 @@ export function ApproverPerformanceFilters({
onTempStartDateChange(customStartDate); onTempStartDateChange(customStartDate);
onTempEndDateChange(customEndDate); onTempEndDateChange(customEndDate);
if (!customStartDate || !customEndDate) { if (!customStartDate || !customEndDate) {
onDateRangeChange('month'); onDateRangeChange('all');
} }
}} }}
data-testid="cancel-date-button" data-testid="cancel-date-button"

View File

@ -6,20 +6,16 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Progress } from '@/components/ui/progress'; import { Progress } from '@/components/ui/progress';
import { CheckCircle, Clock, Target, Timer } from 'lucide-react'; import { CheckCircle, Clock, Target, Timer } from 'lucide-react';
import type { ApproverPerformance } from '@/services/dashboard.service'; import type { ApproverPerformance } from '@/services/dashboard.service';
import type { ApproverPerformanceStats } from '../types/approverPerformance.types';
interface ApproverPerformanceStatsCardsProps { interface ApproverPerformanceStatsCardsProps {
approverStats: ApproverPerformance | null; approverStats: ApproverPerformance;
calculatedStats: ApproverPerformanceStats;
} }
export function ApproverPerformanceStatsCards({ export function ApproverPerformanceStatsCards({
approverStats, approverStats
calculatedStats
}: ApproverPerformanceStatsCardsProps) { }: ApproverPerformanceStatsCardsProps) {
if (!approverStats) return null;
const tatCompliance = approverStats?.tatCompliancePercent ?? calculatedStats.tatComplianceRate; const tatCompliance = approverStats?.tatCompliancePercent ?? 0;
return ( return (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4" data-testid="approver-stats-cards"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4" data-testid="approver-stats-cards">

View File

@ -42,29 +42,40 @@ export function useApproverPerformanceData({
const isInitialMount = useRef(true); const isInitialMount = useRef(true);
// Fetch approver performance stats // Fetch stats for this approver (ONLY on date/priority/SLA changes)
const fetchApproverStats = useCallback(async () => { const fetchApproverStats = useCallback(async () => {
if (!approverId) return; if (!approverId) {
return;
}
try { try {
const result = await dashboardService.getApproverPerformance( const dateRangeToSend = dateRange === 'all' ? undefined : dateRange;
dateRange, console.log('[Stats] Fetching with filters:', {
1, dateRange: dateRangeToSend,
100,
customStartDate, customStartDate,
customEndDate customEndDate,
priority: priorityFilter,
sla: slaComplianceFilter
});
const stats = await dashboardService.getSingleApproverStats(
approverId,
dateRangeToSend,
customStartDate,
customEndDate,
priorityFilter !== 'all' ? priorityFilter : undefined,
slaComplianceFilter !== 'all' ? slaComplianceFilter : undefined
); );
const approver = result.performance.find((p: ApproverPerformance) => p.approverId === approverId); console.log('[Stats] Received stats:', stats);
if (approver) { setApproverStats(stats);
setApproverStats(approver);
}
} catch (error) { } catch (error) {
console.error('Failed to fetch approver stats:', error); console.error('[ApproverPerformance] Failed to fetch approver stats:', error);
setApproverStats(null);
} }
}, [approverId, dateRange, customStartDate, customEndDate]); }, [approverId, dateRange, customStartDate, customEndDate, priorityFilter, slaComplianceFilter]);
// Fetch requests for this approver // Fetch requests for this approver (on ANY filter change)
const fetchRequests = useCallback(async (page: number = 1) => { const fetchRequests = useCallback(async (page: number = 1) => {
if (!approverId) { if (!approverId) {
setLoading(false); setLoading(false);
@ -79,7 +90,7 @@ export function useApproverPerformanceData({
approverId, approverId,
page, page,
itemsPerPage, itemsPerPage,
dateRange, dateRange === 'all' ? undefined : dateRange,
customStartDate, customStartDate,
customEndDate, customEndDate,
statusFilter !== 'all' ? statusFilter : undefined, statusFilter !== 'all' ? statusFilter : undefined,
@ -91,23 +102,8 @@ export function useApproverPerformanceData({
setRequests(result.requests); setRequests(result.requests);
setTotalRecords(result.pagination.totalRecords); setTotalRecords(result.pagination.totalRecords);
setTotalPages(result.pagination.totalPages); setTotalPages(result.pagination.totalPages);
setCurrentPage(page); setCurrentPage(result.pagination.currentPage);
setAllFilteredRequests(result.requests);
// For stats calculation, fetch ALL data (without pagination)
const statsResult = await dashboardService.getRequestsByApprover(
approverId,
1,
10000,
dateRange,
customStartDate,
customEndDate,
statusFilter !== 'all' ? statusFilter : undefined,
priorityFilter !== 'all' ? priorityFilter : undefined,
slaComplianceFilter !== 'all' ? slaComplianceFilter : undefined,
searchTerm || undefined
);
setAllFilteredRequests(statsResult.requests);
} catch (error) { } catch (error) {
console.error('Failed to fetch requests:', error); console.error('Failed to fetch requests:', error);
} finally { } finally {
@ -130,15 +126,29 @@ export function useApproverPerformanceData({
useEffect(() => { useEffect(() => {
if (isInitialMount.current) { if (isInitialMount.current) {
isInitialMount.current = false; isInitialMount.current = false;
fetchApproverStats(); fetchApproverStats(); // Fetch stats once on mount
fetchRequests(1); fetchRequests(1); // Fetch requests once on mount
} }
}, []); // Only run on mount }, []); // Only run on mount
// Refetch when filters change // Refetch stats ONLY when date/priority/SLA changes (NOT status or search)
useEffect(() => { useEffect(() => {
if (!isInitialMount.current) { if (!isInitialMount.current) {
fetchApproverStats(); fetchApproverStats();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
dateRange,
customStartDate,
customEndDate,
priorityFilter,
slaComplianceFilter
// NO statusFilter, NO searchTerm - stats don't depend on these
]);
// Refetch requests when ANY filter changes (including status and search)
useEffect(() => {
if (!isInitialMount.current) {
fetchRequests(1); fetchRequests(1);
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
@ -150,35 +160,18 @@ export function useApproverPerformanceData({
priorityFilter, priorityFilter,
slaComplianceFilter, slaComplianceFilter,
searchTerm searchTerm
// fetchApproverStats and fetchRequests excluded to prevent infinite loops
]); ]);
const handleRefresh = useCallback(() => { const handleRefresh = useCallback(() => {
setRefreshing(true); setRefreshing(true);
fetchApproverStats(); fetchApproverStats(); // Refresh stats
fetchRequests(1); fetchRequests(1); // Refresh requests
}, [fetchApproverStats, fetchRequests]); }, [fetchApproverStats, fetchRequests]);
const handlePageChange = useCallback((page: number) => { const handlePageChange = useCallback((page: number) => {
setCurrentPage(page); // Use server-side pagination - fetch from backend
// Use client-side pagination since we have allFilteredRequests fetchRequests(page);
const startIdx = (page - 1) * itemsPerPage; }, [fetchRequests]);
const endIdx = startIdx + itemsPerPage;
const paginatedRequests = allFilteredRequests.slice(startIdx, endIdx);
setRequests(paginatedRequests);
}, [allFilteredRequests, itemsPerPage]);
// Update paginated data when allFilteredRequests changes
useEffect(() => {
const startIdx = (currentPage - 1) * itemsPerPage;
const endIdx = startIdx + itemsPerPage;
const paginatedRequests = allFilteredRequests.slice(startIdx, endIdx);
setRequests(paginatedRequests);
setTotalPages(Math.ceil(allFilteredRequests.length / itemsPerPage));
if (currentPage > Math.ceil(allFilteredRequests.length / itemsPerPage) && allFilteredRequests.length > 0) {
setCurrentPage(1);
}
}, [allFilteredRequests, currentPage, itemsPerPage]);
return { return {
requests, requests,

View File

@ -13,7 +13,7 @@ export function useApproverPerformanceFilters() {
const [statusFilter, setStatusFilter] = useState<string>(searchParams.get('status') || 'all'); const [statusFilter, setStatusFilter] = useState<string>(searchParams.get('status') || 'all');
const [priorityFilter, setPriorityFilter] = useState<string>(searchParams.get('priority') || 'all'); const [priorityFilter, setPriorityFilter] = useState<string>(searchParams.get('priority') || 'all');
const [slaComplianceFilter, setSlaComplianceFilter] = useState<string>(searchParams.get('slaCompliance') || 'all'); const [slaComplianceFilter, setSlaComplianceFilter] = useState<string>(searchParams.get('slaCompliance') || 'all');
const [dateRange, setDateRange] = useState<DateRange>((searchParams.get('dateRange') as DateRange) || 'month'); const [dateRange, setDateRange] = useState<DateRange>((searchParams.get('dateRange') as DateRange) || 'all');
const [customStartDate, setCustomStartDate] = useState<Date | undefined>( const [customStartDate, setCustomStartDate] = useState<Date | undefined>(
searchParams.get('startDate') ? new Date(searchParams.get('startDate')!) : undefined searchParams.get('startDate') ? new Date(searchParams.get('startDate')!) : undefined
); );
@ -29,7 +29,7 @@ export function useApproverPerformanceFilters() {
setStatusFilter('all'); setStatusFilter('all');
setPriorityFilter('all'); setPriorityFilter('all');
setSlaComplianceFilter('all'); setSlaComplianceFilter('all');
setDateRange('month'); setDateRange('all');
setCustomStartDate(undefined); setCustomStartDate(undefined);
setCustomEndDate(undefined); setCustomEndDate(undefined);
setTempCustomStartDate(undefined); setTempCustomStartDate(undefined);

View File

@ -17,7 +17,7 @@ import { buildApprovalLevels } from './approvalLevelBuilders';
export function buildCreatePayload( export function buildCreatePayload(
formData: FormData, formData: FormData,
selectedTemplate: RequestTemplate | null, selectedTemplate: RequestTemplate | null,
user: any _user: any
): CreateWorkflowPayload { ): CreateWorkflowPayload {
// Filter out spectators who are also approvers (backend will handle validation) // Filter out spectators who are also approvers (backend will handle validation)
const approverEmails = new Set( const approverEmails = new Set(
@ -41,11 +41,13 @@ export function buildCreatePayload(
tat: a?.tat || '', tat: a?.tat || '',
tatType: a?.tatType || 'hours', tatType: a?.tatType || 'hours',
})), })),
spectators: filteredSpectators.map((s) => ({ spectators: filteredSpectators.map((s: any) => ({
userId: s?.userId || '',
name: s?.name || '',
email: s?.email || '', email: s?.email || '',
})), })),
// Note: participants array is auto-generated by backend ccList: [], // Auto-generated by backend
// No need to send it from frontend participants: [], // Auto-generated by backend from approvers and spectators
}; };
} }
@ -55,7 +57,7 @@ export function buildCreatePayload(
*/ */
export function buildUpdatePayload( export function buildUpdatePayload(
formData: FormData, formData: FormData,
user: any, _user: any,
documentsToDelete: string[] documentsToDelete: string[]
): UpdateWorkflowPayload { ): UpdateWorkflowPayload {
const approvalLevels = buildApprovalLevels( const approvalLevels = buildApprovalLevels(
@ -68,7 +70,7 @@ export function buildUpdatePayload(
description: formData.description, description: formData.description,
priority: formData.priority === 'express' ? 'EXPRESS' : 'STANDARD', priority: formData.priority === 'express' ? 'EXPRESS' : 'STANDARD',
approvalLevels, approvalLevels,
// Note: participants array is auto-generated by backend participants: [], // Auto-generated by backend from approval levels
deleteDocumentIds: documentsToDelete.length > 0 ? documentsToDelete : undefined, deleteDocumentIds: documentsToDelete.length > 0 ? documentsToDelete : undefined,
}; };
} }

View File

@ -70,9 +70,12 @@ export function DashboardFiltersBar({
<SelectValue placeholder="Select period" /> <SelectValue placeholder="Select period" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="all">All Time</SelectItem>
<SelectItem value="today">Today</SelectItem> <SelectItem value="today">Today</SelectItem>
<SelectItem value="week">This Week</SelectItem> <SelectItem value="week">This Week</SelectItem>
<SelectItem value="month">This Month</SelectItem> <SelectItem value="month">This Month</SelectItem>
<SelectItem value="last7days">Last 7 Days</SelectItem>
<SelectItem value="last30days">Last 30 Days</SelectItem>
<SelectItem value="custom">Custom Range</SelectItem> <SelectItem value="custom">Custom Range</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
@ -169,9 +172,13 @@ export function DashboardFiltersBar({
<SelectValue placeholder="Select period" /> <SelectValue placeholder="Select period" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="all">All Time</SelectItem>
<SelectItem value="today">Today</SelectItem> <SelectItem value="today">Today</SelectItem>
<SelectItem value="week">This Week</SelectItem> <SelectItem value="week">This Week</SelectItem>
<SelectItem value="month">This Month</SelectItem> <SelectItem value="month">This Month</SelectItem>
<SelectItem value="last7days">Last 7 Days</SelectItem>
<SelectItem value="last30days">Last 30 Days</SelectItem>
<SelectItem value="custom">Custom Range</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>

View File

@ -11,6 +11,7 @@ import type { CriticalAlertData } from '@/components/dashboard/CriticalAlertCard
import { Pagination } from '@/components/common/Pagination'; import { Pagination } from '@/components/common/Pagination';
import { formatBreachTime } from '../../utils/dashboardCalculations'; import { formatBreachTime } from '../../utils/dashboardCalculations';
import { KPIClickFilters } from '../../components/types/dashboard.types'; import { KPIClickFilters } from '../../components/types/dashboard.types';
import { formatDateRangeDescription } from '../../utils/dateRangeFormatter';
interface TATBreachReportProps { interface TATBreachReportProps {
breachedRequests: (CriticalRequest | CriticalAlertData)[]; breachedRequests: (CriticalRequest | CriticalAlertData)[];
@ -56,7 +57,7 @@ export function TATBreachReport({
<div> <div>
<CardTitle className="text-base sm:text-lg lg:text-xl">TAT Breach Report</CardTitle> <CardTitle className="text-base sm:text-lg lg:text-xl">TAT Breach Report</CardTitle>
<CardDescription className="text-xs sm:text-sm"> <CardDescription className="text-xs sm:text-sm">
Requests that breached defined turnaround time Requests that breached TAT - {formatDateRangeDescription(dateRange, customStartDate, customEndDate)}
</CardDescription> </CardDescription>
</div> </div>
</div> </div>

View File

@ -6,7 +6,7 @@ import { useState, useCallback } from 'react';
import { DateRange } from '@/services/dashboard.service'; import { DateRange } from '@/services/dashboard.service';
export function useDashboardFilters() { export function useDashboardFilters() {
const [dateRange, setDateRange] = useState<DateRange>('month'); const [dateRange, setDateRange] = useState<DateRange>('all');
const [customStartDate, setCustomStartDate] = useState<Date | undefined>(undefined); const [customStartDate, setCustomStartDate] = useState<Date | undefined>(undefined);
const [customEndDate, setCustomEndDate] = useState<Date | undefined>(undefined); const [customEndDate, setCustomEndDate] = useState<Date | undefined>(undefined);
const [showCustomDatePicker, setShowCustomDatePicker] = useState(false); const [showCustomDatePicker, setShowCustomDatePicker] = useState(false);
@ -42,7 +42,7 @@ export function useDashboardFilters() {
setCustomStartDate(undefined); setCustomStartDate(undefined);
setCustomEndDate(undefined); setCustomEndDate(undefined);
setShowCustomDatePicker(false); setShowCustomDatePicker(false);
setDateRange('month'); setDateRange('all');
}, []); }, []);
return { return {

View File

@ -0,0 +1,61 @@
import { format } from 'date-fns';
import type { DateRange } from '@/services/dashboard.service';
/**
* Format date range for display in dashboard components
* Returns a human-readable string describing the active date filter
*/
export function formatDateRangeText(
dateRange: DateRange,
customStartDate?: Date,
customEndDate?: Date,
prefix: string = 'for'
): string {
if (dateRange === 'custom' && customStartDate && customEndDate) {
return `${prefix} ${format(customStartDate, 'MMM d, yyyy')} - ${format(customEndDate, 'MMM d, yyyy')}`;
}
const rangeMap: Record<DateRange, string> = {
'all': '(All Time)',
'today': `${prefix} Today`,
'week': `${prefix} This Week`,
'month': `${prefix} This Month`,
'quarter': `${prefix} This Quarter`,
'year': `${prefix} This Year`,
'last30days': `${prefix} Last 30 Days`,
'custom': `${prefix} Custom Range`
};
return rangeMap[dateRange] || `${prefix} Selected Period`;
}
/**
* Format date range for card descriptions
*/
export function formatDateRangeDescription(
dateRange: DateRange,
customStartDate?: Date,
customEndDate?: Date
): string {
if (dateRange === 'all') {
return 'All historical data';
}
if (dateRange === 'custom' && customStartDate && customEndDate) {
return `Data from ${format(customStartDate, 'MMM d, yyyy')} to ${format(customEndDate, 'MMM d, yyyy')}`;
}
const rangeMap: Record<DateRange, string> = {
'all': 'All historical data',
'today': "Today's data",
'week': 'This week data',
'month': 'This month data',
'quarter': 'This quarter data',
'year': 'This year data',
'last30days': 'Last 30 days data',
'custom': 'Custom date range'
};
return rangeMap[dateRange] || 'Filtered data';
}

View File

@ -8,6 +8,7 @@ import { Button } from '@/components/ui/button';
import { Avatar, AvatarFallback } from '@/components/ui/avatar'; import { Avatar, AvatarFallback } from '@/components/ui/avatar';
import { UserPlus, Eye, CheckCircle, XCircle, Share2, Pause, Play, AlertCircle } from 'lucide-react'; import { UserPlus, Eye, CheckCircle, XCircle, Share2, Pause, Play, AlertCircle } from 'lucide-react';
import { getSharedRecipients, type SharedRecipient } from '@/services/summaryApi'; import { getSharedRecipients, type SharedRecipient } from '@/services/summaryApi';
import { useAuth } from '@/contexts/AuthContext';
interface QuickActionsSidebarProps { interface QuickActionsSidebarProps {
request: any; request: any;
@ -42,16 +43,22 @@ export function QuickActionsSidebar({
summaryId, summaryId,
refreshTrigger, refreshTrigger,
}: QuickActionsSidebarProps) { }: QuickActionsSidebarProps) {
const { user } = useAuth();
const [sharedRecipients, setSharedRecipients] = useState<SharedRecipient[]>([]); const [sharedRecipients, setSharedRecipients] = useState<SharedRecipient[]>([]);
const [loadingRecipients, setLoadingRecipients] = useState(false); const [loadingRecipients, setLoadingRecipients] = useState(false);
const isClosed = request?.status === 'closed'; const isClosed = request?.status === 'closed';
const isPaused = request?.pauseInfo?.isPaused || false; const isPaused = request?.pauseInfo?.isPaused || false;
const pausedByUserId = request?.pauseInfo?.pausedBy?.userId;
const currentUserId = (user as any)?.userId || '';
// Both approver AND initiator can pause (when not already paused and not closed) // Both approver AND initiator can pause (when not already paused and not closed)
const canPause = !isPaused && !isClosed && (currentApprovalLevel || isInitiator); const canPause = !isPaused && !isClosed && (currentApprovalLevel || isInitiator);
// Both approver AND initiator can resume directly
// Resume: Can be done by the person who paused OR by both initiator and approver
const canResume = isPaused && onResume && (currentApprovalLevel || isInitiator); const canResume = isPaused && onResume && (currentApprovalLevel || isInitiator);
// Retrigger is no longer needed since initiator can resume directly
const canRetrigger = false; // Disabled - kept for backwards compatibility // Retrigger: Only for initiator when approver paused (initiator asks approver to resume)
const canRetrigger = isPaused && isInitiator && pausedByUserId && pausedByUserId !== currentUserId && onRetrigger;
// Fetch shared recipients when request is closed and summaryId is available // Fetch shared recipients when request is closed and summaryId is available
useEffect(() => { useEffect(() => {

View File

@ -8,6 +8,7 @@ import { RichTextEditor } from '@/components/ui/rich-text-editor';
import { Avatar, AvatarFallback } from '@/components/ui/avatar'; import { Avatar, AvatarFallback } from '@/components/ui/avatar';
import { FormattedDescription } from '@/components/common/FormattedDescription'; import { FormattedDescription } from '@/components/common/FormattedDescription';
import { User, FileText, Mail, Phone, CheckCircle, RefreshCw, Loader2, Pause, Play, AlertCircle } from 'lucide-react'; import { User, FileText, Mail, Phone, CheckCircle, RefreshCw, Loader2, Pause, Play, AlertCircle } from 'lucide-react';
import { useAuth } from '@/contexts/AuthContext';
import { formatDateTime, formatDateShort } from '@/utils/dateFormatter'; import { formatDateTime, formatDateShort } from '@/utils/dateFormatter';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime'; import relativeTime from 'dayjs/plugin/relativeTime';
@ -48,18 +49,21 @@ export function OverviewTab({
onResume, onResume,
onRetrigger, onRetrigger,
currentUserIsApprover = false, currentUserIsApprover = false,
pausedByUserId, pausedByUserId: _pausedByUserId,
currentUserId, currentUserId: _currentUserId,
}: OverviewTabProps) { }: OverviewTabProps) {
void _onPause; // Marked as intentionally unused - available for future use void _onPause; // Marked as intentionally unused - available for future use
void pausedByUserId; // Kept for backwards compatibility const { user } = useAuth();
void currentUserId; // Kept for backwards compatibility
const pauseInfo = request?.pauseInfo; const pauseInfo = request?.pauseInfo;
const isPaused = pauseInfo?.isPaused || false; const isPaused = pauseInfo?.isPaused || false;
// Both approver AND initiator can resume directly const pausedByUserId = pauseInfo?.pausedBy?.userId;
const canResume = isPaused && (currentUserIsApprover || isInitiator); const currentUserId = (user as any)?.userId || '';
// Retrigger is no longer needed since initiator can resume directly
const canRetrigger = false; // Disabled - kept for backwards compatibility // Resume: Can be done by both initiator and approver
const canResume = isPaused && onResume && (currentUserIsApprover || isInitiator);
// Retrigger: Only for initiator when approver paused (initiator asks approver to resume)
const canRetrigger = isPaused && isInitiator && pausedByUserId && pausedByUserId !== currentUserId && onRetrigger;
return ( return (
<div className="space-y-4 sm:space-y-6" data-testid="overview-tab-content"> <div className="space-y-4 sm:space-y-6" data-testid="overview-tab-content">
{/* Request Initiator Card */} {/* Request Initiator Card */}

View File

@ -319,8 +319,8 @@ export function Requests({ onViewRequest }: RequestsProps) {
slaCompliance: filters.slaComplianceFilter !== 'all' ? filters.slaComplianceFilter : undefined slaCompliance: filters.slaComplianceFilter !== 'all' ? filters.slaComplianceFilter : undefined
}; };
// All Requests (admin/normal user) should always have a date range // All Requests (admin/normal user) should always have a date range
// Default to 'month' if no date range is selected // Default to 'all' if no date range is selected
const statsDateRange = filters.dateRange || 'month'; const statsDateRange = filters.dateRange || 'all';
fetchBackendStatsRef.current( fetchBackendStatsRef.current(
statsDateRange, statsDateRange,
@ -660,9 +660,12 @@ export function Requests({ onViewRequest }: RequestsProps) {
<SelectValue placeholder="Date Range" /> <SelectValue placeholder="Date Range" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="all">All Time</SelectItem>
<SelectItem value="today">Today</SelectItem> <SelectItem value="today">Today</SelectItem>
<SelectItem value="week">This Week</SelectItem> <SelectItem value="week">This Week</SelectItem>
<SelectItem value="month">This Month</SelectItem> <SelectItem value="month">This Month</SelectItem>
<SelectItem value="last7days">Last 7 Days</SelectItem>
<SelectItem value="last30days">Last 30 Days</SelectItem>
<SelectItem value="custom">Custom Range</SelectItem> <SelectItem value="custom">Custom Range</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>

View File

@ -580,9 +580,12 @@ export function UserAllRequests({ onViewRequest }: RequestsProps) {
<SelectValue placeholder="Date Range" /> <SelectValue placeholder="Date Range" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="all">All Time</SelectItem>
<SelectItem value="today">Today</SelectItem> <SelectItem value="today">Today</SelectItem>
<SelectItem value="week">This Week</SelectItem> <SelectItem value="week">This Week</SelectItem>
<SelectItem value="month">This Month</SelectItem> <SelectItem value="month">This Month</SelectItem>
<SelectItem value="last7days">Last 7 Days</SelectItem>
<SelectItem value="last30days">Last 30 Days</SelectItem>
<SelectItem value="custom">Custom Range</SelectItem> <SelectItem value="custom">Custom Range</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>

View File

@ -20,7 +20,7 @@ export function useRequestsFilters() {
const [approverFilterType, setApproverFilterType] = useState<'current' | 'any'>( const [approverFilterType, setApproverFilterType] = useState<'current' | 'any'>(
searchParams.get('approverType') === 'any' ? 'any' : 'current' searchParams.get('approverType') === 'any' ? 'any' : 'current'
); );
const [dateRange, setDateRange] = useState<DateRange>((searchParams.get('dateRange') as DateRange) || 'month'); const [dateRange, setDateRange] = useState<DateRange>((searchParams.get('dateRange') as DateRange) || 'all');
const [customStartDate, setCustomStartDate] = useState<Date | undefined>( const [customStartDate, setCustomStartDate] = useState<Date | undefined>(
searchParams.get('startDate') ? new Date(searchParams.get('startDate')!) : undefined searchParams.get('startDate') ? new Date(searchParams.get('startDate')!) : undefined
); );
@ -101,7 +101,7 @@ export function useRequestsFilters() {
setInitiatorFilter('all'); setInitiatorFilter('all');
setApproverFilter('all'); setApproverFilter('all');
setApproverFilterType('current'); setApproverFilterType('current');
setDateRange('month'); setDateRange('all');
setCustomStartDate(undefined); setCustomStartDate(undefined);
setCustomEndDate(undefined); setCustomEndDate(undefined);
setShowCustomDatePicker(false); setShowCustomDatePicker(false);
@ -138,7 +138,7 @@ export function useRequestsFilters() {
departmentFilter !== 'all' || departmentFilter !== 'all' ||
initiatorFilter !== 'all' || initiatorFilter !== 'all' ||
approverFilter !== 'all' || approverFilter !== 'all' ||
dateRange !== 'month' || dateRange !== 'all' ||
customStartDate || customStartDate ||
customEndDate customEndDate
); );

View File

@ -16,7 +16,9 @@ import { ConfigurationManager } from '@/components/admin/ConfigurationManager';
import { HolidayManager } from '@/components/admin/HolidayManager'; import { HolidayManager } from '@/components/admin/HolidayManager';
import { UserRoleManager } from '@/components/admin/UserRoleManager'; import { UserRoleManager } from '@/components/admin/UserRoleManager';
import { NotificationStatusModal } from '@/components/settings/NotificationStatusModal'; import { NotificationStatusModal } from '@/components/settings/NotificationStatusModal';
import { useState } from 'react'; import { NotificationPreferencesModal } from '@/components/settings/NotificationPreferencesModal';
import { useState, useEffect } from 'react';
import { getUserSubscriptions } from '@/services/notificationApi';
export function Settings() { export function Settings() {
const { user } = useAuth(); const { user } = useAuth();
@ -25,6 +27,26 @@ export function Settings() {
const [notificationSuccess, setNotificationSuccess] = useState(false); const [notificationSuccess, setNotificationSuccess] = useState(false);
const [notificationMessage, setNotificationMessage] = useState<string>(); const [notificationMessage, setNotificationMessage] = useState<string>();
const [isEnablingNotifications, setIsEnablingNotifications] = useState(false); const [isEnablingNotifications, setIsEnablingNotifications] = useState(false);
const [showPreferencesModal, setShowPreferencesModal] = useState(false);
const [hasSubscription, setHasSubscription] = useState(false);
const [checkingSubscription, setCheckingSubscription] = useState(true);
useEffect(() => {
checkSubscriptionStatus();
}, []);
const checkSubscriptionStatus = async () => {
try {
setCheckingSubscription(true);
const subscriptions = await getUserSubscriptions();
setHasSubscription(subscriptions.length > 0);
} catch (error) {
console.error('Failed to check subscription status:', error);
setHasSubscription(false);
} finally {
setCheckingSubscription(false);
}
};
const handleEnableNotifications = async () => { const handleEnableNotifications = async () => {
setIsEnablingNotifications(true); setIsEnablingNotifications(true);
@ -93,6 +115,8 @@ export function Settings() {
setNotificationSuccess(true); setNotificationSuccess(true);
setNotificationMessage('Push notifications have been successfully enabled! You will now receive notifications for workflow updates, approvals, and TAT alerts.'); setNotificationMessage('Push notifications have been successfully enabled! You will now receive notifications for workflow updates, approvals, and TAT alerts.');
setShowNotificationModal(true); setShowNotificationModal(true);
// Recheck subscription status
await checkSubscriptionStatus();
} catch (error: any) { } catch (error: any) {
console.error('[Settings] Error enabling notifications:', error); console.error('[Settings] Error enabling notifications:', error);
setNotificationSuccess(false); setNotificationSuccess(false);
@ -177,7 +201,7 @@ export function Settings() {
{/* User Settings Tab */} {/* User Settings Tab */}
<TabsContent value="user" className="mt-0 space-y-0"> <TabsContent value="user" className="mt-0 space-y-0">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 sm:gap-6"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-4 sm:gap-6">
{/* Notification Settings */} {/* Enable Push Notifications Setup */}
<Card className="shadow-lg hover:shadow-xl transition-all duration-300 border-0 rounded-md group"> <Card className="shadow-lg hover:shadow-xl transition-all duration-300 border-0 rounded-md group">
<CardHeader className="pb-4"> <CardHeader className="pb-4">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
@ -185,20 +209,38 @@ export function Settings() {
<Bell className="h-5 w-5 text-white" /> <Bell className="h-5 w-5 text-white" />
</div> </div>
<div> <div>
<CardTitle className="text-lg font-semibold text-gray-900">Notifications</CardTitle> <CardTitle className="text-lg font-semibold text-gray-900">Browser Push Setup</CardTitle>
<CardDescription className="text-sm text-gray-600">Manage notification preferences</CardDescription> <CardDescription className="text-sm text-gray-600">Register this browser for push notifications</CardDescription>
</div> </div>
</div> </div>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="space-y-4"> <div className="space-y-4">
{checkingSubscription ? (
<div className="p-4 bg-gray-50 border border-gray-200 rounded-md flex items-center justify-center">
<p className="text-sm text-gray-600">Checking registration status...</p>
</div>
) : hasSubscription ? (
<div className="p-3 bg-green-50 border border-green-200 rounded-md">
<p className="text-xs text-green-800 font-medium">
This browser is already registered for push notifications.
</p>
</div>
) : (
<div className="p-3 bg-blue-50 border border-blue-200 rounded-md">
<p className="text-xs text-blue-800">
Click below to register this browser for receiving push notifications.
This needs to be done once per browser/device.
</p>
</div>
)}
<Button <Button
onClick={handleEnableNotifications} onClick={handleEnableNotifications}
disabled={isEnablingNotifications} disabled={isEnablingNotifications || hasSubscription}
className="w-full bg-re-green hover:bg-re-green/90 text-white shadow-md hover:shadow-lg transition-all disabled:opacity-50 disabled:cursor-not-allowed" className="w-full bg-re-green hover:bg-re-green/90 text-white shadow-md hover:shadow-lg transition-all disabled:opacity-50 disabled:cursor-not-allowed"
> >
<Bell className={`w-4 h-4 mr-2 ${isEnablingNotifications ? 'animate-pulse' : ''}`} /> <Bell className={`w-4 h-4 mr-2 ${isEnablingNotifications ? 'animate-pulse' : ''}`} />
{isEnablingNotifications ? 'Enabling...' : 'Enable Push Notifications'} {isEnablingNotifications ? 'Registering...' : hasSubscription ? 'Already Registered' : 'Register Browser for Push'}
</Button> </Button>
</div> </div>
</CardContent> </CardContent>
@ -257,16 +299,18 @@ export function Settings() {
</div> </div>
<div> <div>
<CardTitle className="text-lg font-semibold text-gray-900">Preferences</CardTitle> <CardTitle className="text-lg font-semibold text-gray-900">Preferences</CardTitle>
<CardDescription className="text-sm text-gray-600">Application preferences</CardDescription> <CardDescription className="text-sm text-gray-600">Notification and application preferences</CardDescription>
</div> </div>
</div> </div>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="space-y-4"> <Button
<div className="p-4 bg-gradient-to-br from-gray-50 to-gray-100 rounded-xl border border-gray-200"> onClick={() => setShowPreferencesModal(true)}
<p className="text-sm text-gray-600 text-center">User preferences will be available soon</p> className="w-full bg-re-green hover:bg-re-green/90 text-white shadow-md hover:shadow-lg transition-all"
</div> >
</div> <Shield className="w-4 h-4 mr-2" />
Manage Preferences
</Button>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
@ -292,7 +336,7 @@ export function Settings() {
<> <>
{/* Non-Admin User Settings Only */} {/* Non-Admin User Settings Only */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 sm:gap-6"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-4 sm:gap-6">
{/* Notification Settings */} {/* Enable Push Notifications Setup */}
<Card className="shadow-lg hover:shadow-xl transition-all duration-300 border-0 rounded-md group"> <Card className="shadow-lg hover:shadow-xl transition-all duration-300 border-0 rounded-md group">
<CardHeader className="pb-4"> <CardHeader className="pb-4">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
@ -300,20 +344,38 @@ export function Settings() {
<Bell className="h-5 w-5 text-white" /> <Bell className="h-5 w-5 text-white" />
</div> </div>
<div> <div>
<CardTitle className="text-lg font-semibold text-gray-900">Notifications</CardTitle> <CardTitle className="text-lg font-semibold text-gray-900">Browser Push Setup</CardTitle>
<CardDescription className="text-sm text-gray-600">Manage notification preferences</CardDescription> <CardDescription className="text-sm text-gray-600">Register this browser for push notifications</CardDescription>
</div> </div>
</div> </div>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="space-y-4"> <div className="space-y-4">
{checkingSubscription ? (
<div className="p-4 bg-gray-50 border border-gray-200 rounded-md flex items-center justify-center">
<p className="text-sm text-gray-600">Checking registration status...</p>
</div>
) : hasSubscription ? (
<div className="p-3 bg-green-50 border border-green-200 rounded-md">
<p className="text-xs text-green-800 font-medium">
This browser is already registered for push notifications.
</p>
</div>
) : (
<div className="p-3 bg-blue-50 border border-blue-200 rounded-md">
<p className="text-xs text-blue-800">
Click below to register this browser for receiving push notifications.
This needs to be done once per browser/device.
</p>
</div>
)}
<Button <Button
onClick={handleEnableNotifications} onClick={handleEnableNotifications}
disabled={isEnablingNotifications} disabled={isEnablingNotifications || hasSubscription}
className="w-full bg-re-green hover:bg-re-green/90 text-white shadow-md hover:shadow-lg transition-all disabled:opacity-50 disabled:cursor-not-allowed" className="w-full bg-re-green hover:bg-re-green/90 text-white shadow-md hover:shadow-lg transition-all disabled:opacity-50 disabled:cursor-not-allowed"
> >
<Bell className={`w-4 h-4 mr-2 ${isEnablingNotifications ? 'animate-pulse' : ''}`} /> <Bell className={`w-4 h-4 mr-2 ${isEnablingNotifications ? 'animate-pulse' : ''}`} />
{isEnablingNotifications ? 'Enabling...' : 'Enable Push Notifications'} {isEnablingNotifications ? 'Registering...' : hasSubscription ? 'Already Registered' : 'Register Browser for Push'}
</Button> </Button>
</div> </div>
</CardContent> </CardContent>
@ -372,16 +434,18 @@ export function Settings() {
</div> </div>
<div> <div>
<CardTitle className="text-lg font-semibold text-gray-900">Preferences</CardTitle> <CardTitle className="text-lg font-semibold text-gray-900">Preferences</CardTitle>
<CardDescription className="text-sm text-gray-600">Application preferences</CardDescription> <CardDescription className="text-sm text-gray-600">Notification and application preferences</CardDescription>
</div> </div>
</div> </div>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="space-y-4"> <Button
<div className="p-4 bg-gradient-to-br from-gray-50 to-gray-100 rounded-md border border-gray-200"> onClick={() => setShowPreferencesModal(true)}
<p className="text-sm text-gray-600 text-center">User preferences will be available soon</p> className="w-full bg-re-green hover:bg-re-green/90 text-white shadow-md hover:shadow-lg transition-all"
</div> >
</div> <Shield className="w-4 h-4 mr-2" />
Manage Preferences
</Button>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
@ -395,6 +459,11 @@ export function Settings() {
success={notificationSuccess} success={notificationSuccess}
message={notificationMessage} message={notificationMessage}
/> />
<NotificationPreferencesModal
open={showPreferencesModal}
onClose={() => setShowPreferencesModal(false)}
/>
</div> </div>
); );
} }

View File

@ -119,9 +119,14 @@ export interface ApproverPerformance {
approverId: string; approverId: string;
approverName: string; approverName: string;
totalApproved: number; totalApproved: number;
approvedCount: number;
rejectedCount: number;
closedCount: number;
tatCompliancePercent: number; tatCompliancePercent: number;
avgResponseHours: number; avgResponseHours: number;
pendingCount: number; pendingCount: number;
withinTatCount: number;
breachedCount: number;
} }
export interface UpcomingDeadline { export interface UpcomingDeadline {
@ -158,7 +163,7 @@ export interface PriorityDistribution {
complianceRate: number; complianceRate: number;
} }
export type DateRange = 'today' | 'week' | 'month' | 'quarter' | 'year' | 'last30days' | 'custom'; export type DateRange = 'all' | 'today' | 'week' | 'month' | 'quarter' | 'year' | 'last30days' | 'custom';
class DashboardService { class DashboardService {
/** /**
@ -442,8 +447,17 @@ class DashboardService {
/** /**
* Get Approver Performance metrics with pagination * Get Approver Performance metrics with pagination
* Supports priority and SLA filters for consistent stats behavior
*/ */
async getApproverPerformance(dateRange?: DateRange, page: number = 1, limit: number = 10, startDate?: Date, endDate?: Date): Promise<{ async getApproverPerformance(
dateRange?: DateRange,
page: number = 1,
limit: number = 10,
startDate?: Date,
endDate?: Date,
priority?: string,
slaCompliance?: string
): Promise<{
performance: ApproverPerformance[], performance: ApproverPerformance[],
pagination: { pagination: {
currentPage: number, currentPage: number,
@ -453,11 +467,24 @@ class DashboardService {
} }
}> { }> {
try { try {
const params: any = { dateRange, page, limit }; const params: any = {
dateRange,
page,
limit: limit || 10 // Explicitly set limit (default 10 if not provided)
};
if (dateRange === 'custom' && startDate && endDate) { if (dateRange === 'custom' && startDate && endDate) {
params.startDate = startDate.toISOString(); params.startDate = startDate.toISOString();
params.endDate = endDate.toISOString(); params.endDate = endDate.toISOString();
} }
if (priority && priority !== 'all') {
params.priority = priority;
}
if (slaCompliance && slaCompliance !== 'all') {
params.slaCompliance = slaCompliance;
}
console.log('[Dashboard Service] Fetching approver performance with params:', params);
const response = await apiClient.get('/dashboard/stats/approver-performance', { params }); const response = await apiClient.get('/dashboard/stats/approver-performance', { params });
return { return {
performance: response.data.data, performance: response.data.data,
@ -595,6 +622,36 @@ class DashboardService {
} }
} }
/**
* Get single approver stats only (dedicated endpoint for performance)
* Only respects date, priority, and SLA filters
*/
async getSingleApproverStats(
approverId: string,
dateRange?: DateRange,
startDate?: Date,
endDate?: Date,
priority?: string,
slaCompliance?: string
): Promise<ApproverPerformance> {
try {
const params: any = { approverId };
if (dateRange) params.dateRange = dateRange;
if (dateRange === 'custom' && startDate && endDate) {
params.startDate = startDate.toISOString();
params.endDate = endDate.toISOString();
}
if (priority && priority !== 'all') params.priority = priority;
if (slaCompliance && slaCompliance !== 'all') params.slaCompliance = slaCompliance;
const response = await apiClient.get('/dashboard/stats/single-approver', { params });
return response.data.data;
} catch (error) {
console.error('Failed to fetch single approver stats:', error);
throw error;
}
}
/** /**
* Get requests filtered by approver ID for detailed performance analysis * Get requests filtered by approver ID for detailed performance analysis
*/ */
@ -610,7 +667,7 @@ class DashboardService {
slaCompliance?: string, slaCompliance?: string,
search?: string search?: string
): Promise<{ ): Promise<{
requests: any[], requests: any[],
pagination: { pagination: {
currentPage: number, currentPage: number,
totalPages: number, totalPages: number,

View File

@ -12,27 +12,33 @@ export interface Notification {
actionUrl?: string; actionUrl?: string;
actionRequired: boolean; actionRequired: boolean;
metadata?: any; metadata?: any;
sentVia: string[];
readAt?: string; readAt?: string;
createdAt: string; createdAt: string;
} }
class NotificationApi { export interface PushSubscription {
subscriptionId: string;
endpoint: string;
userAgent?: string;
createdAt: string;
}
const notificationApi = {
/** /**
* Get user's notifications * Get user's notifications with pagination
*/ */
async list(params?: { page?: number; limit?: number; unreadOnly?: boolean }) { async list(params?: { page?: number; limit?: number; unreadOnly?: boolean }) {
const response = await apiClient.get('/notifications', { params }); const response = await apiClient.get('/notifications', { params });
return response.data; return response.data;
} },
/** /**
* Get unread count * Get unread notification count
*/ */
async getUnreadCount() { async getUnreadCount() {
const response = await apiClient.get('/notifications/unread-count'); const response = await apiClient.get('/notifications/unread-count');
return response.data.data.unreadCount; return response.data;
} },
/** /**
* Mark notification as read * Mark notification as read
@ -40,15 +46,15 @@ class NotificationApi {
async markAsRead(notificationId: string) { async markAsRead(notificationId: string) {
const response = await apiClient.patch(`/notifications/${notificationId}/read`); const response = await apiClient.patch(`/notifications/${notificationId}/read`);
return response.data; return response.data;
} },
/** /**
* Mark all as read * Mark all notifications as read
*/ */
async markAllAsRead() { async markAllAsRead() {
const response = await apiClient.post('/notifications/mark-all-read'); const response = await apiClient.post('/notifications/mark-all-read');
return response.data; return response.data;
} },
/** /**
* Delete notification * Delete notification
@ -57,8 +63,16 @@ class NotificationApi {
const response = await apiClient.delete(`/notifications/${notificationId}`); const response = await apiClient.delete(`/notifications/${notificationId}`);
return response.data; return response.data;
} }
} };
/**
* Get current user's push notification subscriptions
*/
export const getUserSubscriptions = async (): Promise<PushSubscription[]> => {
const response = await apiClient.get<{ success: boolean; data: { subscriptions: PushSubscription[]; count: number } }>(
'/notifications/subscriptions'
);
return response.data.data.subscriptions;
};
export const notificationApi = new NotificationApi();
export default notificationApi; export default notificationApi;

View File

@ -0,0 +1,37 @@
import apiClient from './authApi';
export interface NotificationPreferences {
emailNotificationsEnabled: boolean;
pushNotificationsEnabled: boolean;
inAppNotificationsEnabled: boolean;
}
export interface UpdateNotificationPreferences {
emailNotificationsEnabled?: boolean;
pushNotificationsEnabled?: boolean;
inAppNotificationsEnabled?: boolean;
}
/**
* Get current user's notification preferences
*/
export const getNotificationPreferences = async (): Promise<NotificationPreferences> => {
const response = await apiClient.get<{ success: boolean; data: NotificationPreferences }>(
'/user/preferences/notifications'
);
return response.data.data;
};
/**
* Update current user's notification preferences
*/
export const updateNotificationPreferences = async (
preferences: UpdateNotificationPreferences
): Promise<NotificationPreferences> => {
const response = await apiClient.put<{ success: boolean; data: NotificationPreferences }>(
'/user/preferences/notifications',
preferences
);
return response.data.data;
};

View File

@ -72,6 +72,11 @@ export const getStatusConfig = (status: string) => {
color: 'bg-yellow-100 text-yellow-800 border-yellow-200', color: 'bg-yellow-100 text-yellow-800 border-yellow-200',
label: 'pending' label: 'pending'
}; };
case 'paused':
return {
color: 'bg-gray-400 text-gray-100 border-gray-500',
label: 'paused'
};
case 'in-review': case 'in-review':
return { return {
color: 'bg-blue-100 text-blue-800 border-blue-200', color: 'bg-blue-100 text-blue-800 border-blue-200',