notification preferances added approver performance api altererd resume added for initiator also
This commit is contained in:
parent
1bebf3a46a
commit
7358c3ff30
@ -59,7 +59,7 @@ export function AIConfig() {
|
||||
openaiApiKey: configMap['OPENAI_API_KEY'] || '',
|
||||
geminiApiKey: configMap['GEMINI_API_KEY'] || '',
|
||||
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) {
|
||||
console.error('Failed to load AI configurations:', error);
|
||||
@ -81,7 +81,7 @@ export function AIConfig() {
|
||||
updateConfiguration('OPENAI_API_KEY', config.openaiApiKey),
|
||||
updateConfiguration('GEMINI_API_KEY', config.geminiApiKey),
|
||||
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');
|
||||
|
||||
@ -26,19 +26,19 @@ export function AIParameters({
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="max-remark-chars" className="text-sm font-medium">
|
||||
Maximum Remark Characters
|
||||
Maximum Remark Length
|
||||
</Label>
|
||||
<Input
|
||||
id="max-remark-chars"
|
||||
type="number"
|
||||
min="100"
|
||||
max="2000"
|
||||
min="500"
|
||||
max="5000"
|
||||
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"
|
||||
/>
|
||||
<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>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
@ -258,9 +258,9 @@ export function ConfigurationManager({ onConfigUpdate }: ConfigurationManagerPro
|
||||
}
|
||||
};
|
||||
|
||||
// Filter out notification rules, dashboard layout categories, and allow external sharing
|
||||
const excludedCategories = ['NOTIFICATION_RULES', 'DASHBOARD_LAYOUT'];
|
||||
const excludedConfigKeys = ['ALLOW_EXTERNAL_SHARING'];
|
||||
// Filter out dashboard layout category and specific config keys
|
||||
const excludedCategories = ['DASHBOARD_LAYOUT'];
|
||||
const excludedConfigKeys = ['ALLOW_EXTERNAL_SHARING', 'NOTIFICATION_BATCH_DELAY_MS', 'AI_REMARK_MAX_CHARACTERS'];
|
||||
const filteredConfigurations = configurations.filter(
|
||||
config => !excludedCategories.includes(config.configCategory) &&
|
||||
!excludedConfigKeys.includes(config.configKey)
|
||||
|
||||
192
src/components/settings/NotificationPreferences.tsx
Normal file
192
src/components/settings/NotificationPreferences.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
||||
224
src/components/settings/NotificationPreferencesModal.tsx
Normal file
224
src/components/settings/NotificationPreferencesModal.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
||||
176
src/components/settings/NotificationPreferencesSimple.tsx
Normal file
176
src/components/settings/NotificationPreferencesSimple.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -639,9 +639,9 @@ export function ApprovalStepCard({
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Skip Approver Button - Only show for initiator on pending/in-review/paused levels */}
|
||||
{/* When paused, initiator can skip the approver which will negate the pause */}
|
||||
{isInitiator && (isActive || isPaused || step.status === 'pending') && !isCompleted && !isRejected && step.levelId && onSkipApprover && (
|
||||
{/* Skip Approver Button - Only show for initiator on pending/in-review levels (not when paused) */}
|
||||
{/* User must resume first before skipping */}
|
||||
{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">
|
||||
<Button
|
||||
variant="outline"
|
||||
|
||||
@ -640,26 +640,36 @@ export function useRequestDetails(
|
||||
if (!requestIdentifier || !apiRequest) return;
|
||||
|
||||
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
|
||||
* Silently refresh to show latest changes
|
||||
*/
|
||||
const handleRequestUpdated = (data: any) => {
|
||||
console.log('[useRequestDetails] 📡 Received request:updated event:', data);
|
||||
// Verify this update is for the current request
|
||||
if (data?.requestId === apiRequest.requestId || data?.requestNumber === requestIdentifier) {
|
||||
console.log('[useRequestDetails] 🔄 Request updated remotely, refreshing silently...');
|
||||
// Silent refresh - no loading state, no user interruption
|
||||
refreshDetails();
|
||||
} else {
|
||||
console.log('[useRequestDetails] ⚠️ Event for different request, ignoring');
|
||||
}
|
||||
};
|
||||
|
||||
// Register listener
|
||||
socket.on('request:updated', handleRequestUpdated);
|
||||
console.log('[useRequestDetails] ✅ Registered listener for request:updated');
|
||||
|
||||
// Cleanup on unmount
|
||||
return () => {
|
||||
console.log('[useRequestDetails] 🧹 Cleaning up socket listener');
|
||||
socket.off('request:updated', handleRequestUpdated);
|
||||
};
|
||||
}, [requestIdentifier, apiRequest, refreshDetails]);
|
||||
|
||||
@ -4,7 +4,6 @@
|
||||
* This is a refactored version that uses modular components, hooks, and utilities.
|
||||
*/
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
|
||||
// Components
|
||||
@ -19,9 +18,6 @@ import { ApproverPerformanceRequestList } from './components/ApproverPerformance
|
||||
import { useApproverPerformanceFilters } from './hooks/useApproverPerformanceFilters';
|
||||
import { useApproverPerformanceData } from './hooks/useApproverPerformanceData';
|
||||
|
||||
// Utils
|
||||
import { calculateApproverStats } from './utils/statsCalculations';
|
||||
|
||||
const itemsPerPage = 10;
|
||||
|
||||
export function ApproverPerformance() {
|
||||
@ -47,10 +43,7 @@ export function ApproverPerformance() {
|
||||
itemsPerPage
|
||||
});
|
||||
|
||||
// Calculate stats from filtered data
|
||||
const calculatedStats = useMemo(() => {
|
||||
return calculateApproverStats(data.allFilteredRequests);
|
||||
}, [data.allFilteredRequests]);
|
||||
// All stats come from backend (data.approverStats) - no frontend calculation needed
|
||||
|
||||
if (!approverId) {
|
||||
return <ApproverPerformanceEmpty />;
|
||||
@ -69,16 +62,19 @@ export function ApproverPerformance() {
|
||||
{data.approverStats && (
|
||||
<ApproverPerformanceStatsCards
|
||||
approverStats={data.approverStats}
|
||||
calculatedStats={calculatedStats}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Filtered Request Stats - Approver's Actions */}
|
||||
<ApproverPerformanceActionsStats
|
||||
approverName={approverName}
|
||||
approverStats={data.approverStats}
|
||||
calculatedStats={calculatedStats}
|
||||
/>
|
||||
{data.approverStats && (
|
||||
<ApproverPerformanceActionsStats
|
||||
approverName={approverName}
|
||||
approverStats={data.approverStats}
|
||||
dateRange={filters.dateRange}
|
||||
customStartDate={filters.customStartDate}
|
||||
customEndDate={filters.customEndDate}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Filters */}
|
||||
<ApproverPerformanceFilters
|
||||
|
||||
@ -4,26 +4,30 @@
|
||||
|
||||
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 type { ApproverPerformance } from '@/services/dashboard.service';
|
||||
import type { ApproverPerformanceStats } from '../types/approverPerformance.types';
|
||||
import type { ApproverPerformance, DateRange } from '@/services/dashboard.service';
|
||||
import { formatDateRangeText } from '@/pages/Dashboard/utils/dateRangeFormatter';
|
||||
|
||||
interface ApproverPerformanceActionsStatsProps {
|
||||
approverName: string;
|
||||
approverStats: ApproverPerformance | null;
|
||||
calculatedStats: ApproverPerformanceStats;
|
||||
approverStats: ApproverPerformance;
|
||||
dateRange?: DateRange;
|
||||
customStartDate?: Date;
|
||||
customEndDate?: Date;
|
||||
}
|
||||
|
||||
export function ApproverPerformanceActionsStats({
|
||||
approverName,
|
||||
approverStats,
|
||||
calculatedStats
|
||||
dateRange = 'all',
|
||||
customStartDate,
|
||||
customEndDate
|
||||
}: ApproverPerformanceActionsStatsProps) {
|
||||
return (
|
||||
<Card data-testid="approver-actions-stats">
|
||||
<CardHeader>
|
||||
<CardTitle>Approver's Actions (Filtered)</CardTitle>
|
||||
<CardTitle>Approver's Actions</CardTitle>
|
||||
<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>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
@ -38,41 +42,45 @@ export function ApproverPerformanceActionsStats({
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<CheckCircle className="w-5 h-5 text-green-600" />
|
||||
<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>
|
||||
</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>
|
||||
<div className="p-4 bg-red-50 rounded-lg border border-red-200">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<XCircle className="w-5 h-5 text-red-600" />
|
||||
<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>
|
||||
</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>
|
||||
<div className="p-4 bg-yellow-50 rounded-lg border border-yellow-200">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<Clock className="w-5 h-5 text-yellow-600" />
|
||||
</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>
|
||||
<div className="p-4 bg-gray-50 rounded-lg border border-gray-200">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<Archive className="w-5 h-5 text-gray-600" />
|
||||
</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>
|
||||
<div className="p-4 bg-blue-50 rounded-lg border border-blue-200">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<FileText className="w-5 h-5 text-blue-600" />
|
||||
</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>
|
||||
</div>
|
||||
@ -89,24 +97,24 @@ export function ApproverPerformanceActionsStats({
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<Award className="w-5 h-5 text-green-600" />
|
||||
<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>
|
||||
</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>
|
||||
<div className="p-4 bg-red-50 rounded-lg border border-red-200">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<AlertCircle className="w-5 h-5 text-red-600" />
|
||||
</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>
|
||||
<div className="p-4 bg-purple-50 rounded-lg border border-purple-200">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<BarChart3 className="w-5 h-5 text-purple-600" />
|
||||
</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>
|
||||
</div>
|
||||
|
||||
@ -136,9 +136,12 @@ export function ApproverPerformanceFilters({
|
||||
<SelectValue placeholder="Date Range" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All Time</SelectItem>
|
||||
<SelectItem value="today">Today</SelectItem>
|
||||
<SelectItem value="week">This Week</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>
|
||||
</Select>
|
||||
@ -219,7 +222,7 @@ export function ApproverPerformanceFilters({
|
||||
onTempStartDateChange(customStartDate);
|
||||
onTempEndDateChange(customEndDate);
|
||||
if (!customStartDate || !customEndDate) {
|
||||
onDateRangeChange('month');
|
||||
onDateRangeChange('all');
|
||||
}
|
||||
}}
|
||||
data-testid="cancel-date-button"
|
||||
|
||||
@ -6,20 +6,16 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Progress } from '@/components/ui/progress';
|
||||
import { CheckCircle, Clock, Target, Timer } from 'lucide-react';
|
||||
import type { ApproverPerformance } from '@/services/dashboard.service';
|
||||
import type { ApproverPerformanceStats } from '../types/approverPerformance.types';
|
||||
|
||||
interface ApproverPerformanceStatsCardsProps {
|
||||
approverStats: ApproverPerformance | null;
|
||||
calculatedStats: ApproverPerformanceStats;
|
||||
approverStats: ApproverPerformance;
|
||||
}
|
||||
|
||||
export function ApproverPerformanceStatsCards({
|
||||
approverStats,
|
||||
calculatedStats
|
||||
approverStats
|
||||
}: ApproverPerformanceStatsCardsProps) {
|
||||
if (!approverStats) return null;
|
||||
|
||||
const tatCompliance = approverStats?.tatCompliancePercent ?? calculatedStats.tatComplianceRate;
|
||||
const tatCompliance = approverStats?.tatCompliancePercent ?? 0;
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4" data-testid="approver-stats-cards">
|
||||
|
||||
@ -42,29 +42,40 @@ export function useApproverPerformanceData({
|
||||
|
||||
const isInitialMount = useRef(true);
|
||||
|
||||
// Fetch approver performance stats
|
||||
// Fetch stats for this approver (ONLY on date/priority/SLA changes)
|
||||
const fetchApproverStats = useCallback(async () => {
|
||||
if (!approverId) return;
|
||||
if (!approverId) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await dashboardService.getApproverPerformance(
|
||||
dateRange,
|
||||
1,
|
||||
100,
|
||||
const dateRangeToSend = dateRange === 'all' ? undefined : dateRange;
|
||||
console.log('[Stats] Fetching with filters:', {
|
||||
dateRange: dateRangeToSend,
|
||||
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);
|
||||
if (approver) {
|
||||
setApproverStats(approver);
|
||||
}
|
||||
console.log('[Stats] Received stats:', stats);
|
||||
setApproverStats(stats);
|
||||
} 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) => {
|
||||
if (!approverId) {
|
||||
setLoading(false);
|
||||
@ -79,7 +90,7 @@ export function useApproverPerformanceData({
|
||||
approverId,
|
||||
page,
|
||||
itemsPerPage,
|
||||
dateRange,
|
||||
dateRange === 'all' ? undefined : dateRange,
|
||||
customStartDate,
|
||||
customEndDate,
|
||||
statusFilter !== 'all' ? statusFilter : undefined,
|
||||
@ -91,23 +102,8 @@ export function useApproverPerformanceData({
|
||||
setRequests(result.requests);
|
||||
setTotalRecords(result.pagination.totalRecords);
|
||||
setTotalPages(result.pagination.totalPages);
|
||||
setCurrentPage(page);
|
||||
|
||||
// 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);
|
||||
setCurrentPage(result.pagination.currentPage);
|
||||
setAllFilteredRequests(result.requests);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch requests:', error);
|
||||
} finally {
|
||||
@ -130,15 +126,29 @@ export function useApproverPerformanceData({
|
||||
useEffect(() => {
|
||||
if (isInitialMount.current) {
|
||||
isInitialMount.current = false;
|
||||
fetchApproverStats();
|
||||
fetchRequests(1);
|
||||
fetchApproverStats(); // Fetch stats once on mount
|
||||
fetchRequests(1); // Fetch requests once on mount
|
||||
}
|
||||
}, []); // Only run on mount
|
||||
|
||||
// Refetch when filters change
|
||||
// Refetch stats ONLY when date/priority/SLA changes (NOT status or search)
|
||||
useEffect(() => {
|
||||
if (!isInitialMount.current) {
|
||||
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);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@ -150,35 +160,18 @@ export function useApproverPerformanceData({
|
||||
priorityFilter,
|
||||
slaComplianceFilter,
|
||||
searchTerm
|
||||
// fetchApproverStats and fetchRequests excluded to prevent infinite loops
|
||||
]);
|
||||
|
||||
const handleRefresh = useCallback(() => {
|
||||
setRefreshing(true);
|
||||
fetchApproverStats();
|
||||
fetchRequests(1);
|
||||
fetchApproverStats(); // Refresh stats
|
||||
fetchRequests(1); // Refresh requests
|
||||
}, [fetchApproverStats, fetchRequests]);
|
||||
|
||||
const handlePageChange = useCallback((page: number) => {
|
||||
setCurrentPage(page);
|
||||
// Use client-side pagination since we have allFilteredRequests
|
||||
const startIdx = (page - 1) * itemsPerPage;
|
||||
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]);
|
||||
// Use server-side pagination - fetch from backend
|
||||
fetchRequests(page);
|
||||
}, [fetchRequests]);
|
||||
|
||||
return {
|
||||
requests,
|
||||
|
||||
@ -13,7 +13,7 @@ export function useApproverPerformanceFilters() {
|
||||
const [statusFilter, setStatusFilter] = useState<string>(searchParams.get('status') || 'all');
|
||||
const [priorityFilter, setPriorityFilter] = useState<string>(searchParams.get('priority') || '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>(
|
||||
searchParams.get('startDate') ? new Date(searchParams.get('startDate')!) : undefined
|
||||
);
|
||||
@ -29,7 +29,7 @@ export function useApproverPerformanceFilters() {
|
||||
setStatusFilter('all');
|
||||
setPriorityFilter('all');
|
||||
setSlaComplianceFilter('all');
|
||||
setDateRange('month');
|
||||
setDateRange('all');
|
||||
setCustomStartDate(undefined);
|
||||
setCustomEndDate(undefined);
|
||||
setTempCustomStartDate(undefined);
|
||||
|
||||
@ -17,7 +17,7 @@ import { buildApprovalLevels } from './approvalLevelBuilders';
|
||||
export function buildCreatePayload(
|
||||
formData: FormData,
|
||||
selectedTemplate: RequestTemplate | null,
|
||||
user: any
|
||||
_user: any
|
||||
): CreateWorkflowPayload {
|
||||
// Filter out spectators who are also approvers (backend will handle validation)
|
||||
const approverEmails = new Set(
|
||||
@ -41,11 +41,13 @@ export function buildCreatePayload(
|
||||
tat: a?.tat || '',
|
||||
tatType: a?.tatType || 'hours',
|
||||
})),
|
||||
spectators: filteredSpectators.map((s) => ({
|
||||
spectators: filteredSpectators.map((s: any) => ({
|
||||
userId: s?.userId || '',
|
||||
name: s?.name || '',
|
||||
email: s?.email || '',
|
||||
})),
|
||||
// Note: participants array is auto-generated by backend
|
||||
// No need to send it from frontend
|
||||
ccList: [], // Auto-generated by backend
|
||||
participants: [], // Auto-generated by backend from approvers and spectators
|
||||
};
|
||||
}
|
||||
|
||||
@ -55,7 +57,7 @@ export function buildCreatePayload(
|
||||
*/
|
||||
export function buildUpdatePayload(
|
||||
formData: FormData,
|
||||
user: any,
|
||||
_user: any,
|
||||
documentsToDelete: string[]
|
||||
): UpdateWorkflowPayload {
|
||||
const approvalLevels = buildApprovalLevels(
|
||||
@ -68,7 +70,7 @@ export function buildUpdatePayload(
|
||||
description: formData.description,
|
||||
priority: formData.priority === 'express' ? 'EXPRESS' : 'STANDARD',
|
||||
approvalLevels,
|
||||
// Note: participants array is auto-generated by backend
|
||||
participants: [], // Auto-generated by backend from approval levels
|
||||
deleteDocumentIds: documentsToDelete.length > 0 ? documentsToDelete : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@ -70,9 +70,12 @@ export function DashboardFiltersBar({
|
||||
<SelectValue placeholder="Select period" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All Time</SelectItem>
|
||||
<SelectItem value="today">Today</SelectItem>
|
||||
<SelectItem value="week">This Week</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>
|
||||
</Select>
|
||||
@ -169,9 +172,13 @@ export function DashboardFiltersBar({
|
||||
<SelectValue placeholder="Select period" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All Time</SelectItem>
|
||||
<SelectItem value="today">Today</SelectItem>
|
||||
<SelectItem value="week">This Week</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>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
@ -11,6 +11,7 @@ import type { CriticalAlertData } from '@/components/dashboard/CriticalAlertCard
|
||||
import { Pagination } from '@/components/common/Pagination';
|
||||
import { formatBreachTime } from '../../utils/dashboardCalculations';
|
||||
import { KPIClickFilters } from '../../components/types/dashboard.types';
|
||||
import { formatDateRangeDescription } from '../../utils/dateRangeFormatter';
|
||||
|
||||
interface TATBreachReportProps {
|
||||
breachedRequests: (CriticalRequest | CriticalAlertData)[];
|
||||
@ -56,7 +57,7 @@ export function TATBreachReport({
|
||||
<div>
|
||||
<CardTitle className="text-base sm:text-lg lg:text-xl">TAT Breach Report</CardTitle>
|
||||
<CardDescription className="text-xs sm:text-sm">
|
||||
Requests that breached defined turnaround time
|
||||
Requests that breached TAT - {formatDateRangeDescription(dateRange, customStartDate, customEndDate)}
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -6,7 +6,7 @@ import { useState, useCallback } from 'react';
|
||||
import { DateRange } from '@/services/dashboard.service';
|
||||
|
||||
export function useDashboardFilters() {
|
||||
const [dateRange, setDateRange] = useState<DateRange>('month');
|
||||
const [dateRange, setDateRange] = useState<DateRange>('all');
|
||||
const [customStartDate, setCustomStartDate] = useState<Date | undefined>(undefined);
|
||||
const [customEndDate, setCustomEndDate] = useState<Date | undefined>(undefined);
|
||||
const [showCustomDatePicker, setShowCustomDatePicker] = useState(false);
|
||||
@ -42,7 +42,7 @@ export function useDashboardFilters() {
|
||||
setCustomStartDate(undefined);
|
||||
setCustomEndDate(undefined);
|
||||
setShowCustomDatePicker(false);
|
||||
setDateRange('month');
|
||||
setDateRange('all');
|
||||
}, []);
|
||||
|
||||
return {
|
||||
|
||||
61
src/pages/Dashboard/utils/dateRangeFormatter.ts
Normal file
61
src/pages/Dashboard/utils/dateRangeFormatter.ts
Normal 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';
|
||||
}
|
||||
|
||||
@ -8,6 +8,7 @@ import { Button } from '@/components/ui/button';
|
||||
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
|
||||
import { UserPlus, Eye, CheckCircle, XCircle, Share2, Pause, Play, AlertCircle } from 'lucide-react';
|
||||
import { getSharedRecipients, type SharedRecipient } from '@/services/summaryApi';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
|
||||
interface QuickActionsSidebarProps {
|
||||
request: any;
|
||||
@ -42,16 +43,22 @@ export function QuickActionsSidebar({
|
||||
summaryId,
|
||||
refreshTrigger,
|
||||
}: QuickActionsSidebarProps) {
|
||||
const { user } = useAuth();
|
||||
const [sharedRecipients, setSharedRecipients] = useState<SharedRecipient[]>([]);
|
||||
const [loadingRecipients, setLoadingRecipients] = useState(false);
|
||||
const isClosed = request?.status === 'closed';
|
||||
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)
|
||||
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);
|
||||
// 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
|
||||
useEffect(() => {
|
||||
|
||||
@ -8,6 +8,7 @@ import { RichTextEditor } from '@/components/ui/rich-text-editor';
|
||||
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
|
||||
import { FormattedDescription } from '@/components/common/FormattedDescription';
|
||||
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 dayjs from 'dayjs';
|
||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
@ -48,18 +49,21 @@ export function OverviewTab({
|
||||
onResume,
|
||||
onRetrigger,
|
||||
currentUserIsApprover = false,
|
||||
pausedByUserId,
|
||||
currentUserId,
|
||||
pausedByUserId: _pausedByUserId,
|
||||
currentUserId: _currentUserId,
|
||||
}: OverviewTabProps) {
|
||||
void _onPause; // Marked as intentionally unused - available for future use
|
||||
void pausedByUserId; // Kept for backwards compatibility
|
||||
void currentUserId; // Kept for backwards compatibility
|
||||
const { user } = useAuth();
|
||||
const pauseInfo = request?.pauseInfo;
|
||||
const isPaused = pauseInfo?.isPaused || false;
|
||||
// Both approver AND initiator can resume directly
|
||||
const canResume = isPaused && (currentUserIsApprover || isInitiator);
|
||||
// Retrigger is no longer needed since initiator can resume directly
|
||||
const canRetrigger = false; // Disabled - kept for backwards compatibility
|
||||
const pausedByUserId = pauseInfo?.pausedBy?.userId;
|
||||
const currentUserId = (user as any)?.userId || '';
|
||||
|
||||
// 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 (
|
||||
<div className="space-y-4 sm:space-y-6" data-testid="overview-tab-content">
|
||||
{/* Request Initiator Card */}
|
||||
|
||||
@ -319,8 +319,8 @@ export function Requests({ onViewRequest }: RequestsProps) {
|
||||
slaCompliance: filters.slaComplianceFilter !== 'all' ? filters.slaComplianceFilter : undefined
|
||||
};
|
||||
// All Requests (admin/normal user) should always have a date range
|
||||
// Default to 'month' if no date range is selected
|
||||
const statsDateRange = filters.dateRange || 'month';
|
||||
// Default to 'all' if no date range is selected
|
||||
const statsDateRange = filters.dateRange || 'all';
|
||||
|
||||
fetchBackendStatsRef.current(
|
||||
statsDateRange,
|
||||
@ -660,9 +660,12 @@ export function Requests({ onViewRequest }: RequestsProps) {
|
||||
<SelectValue placeholder="Date Range" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All Time</SelectItem>
|
||||
<SelectItem value="today">Today</SelectItem>
|
||||
<SelectItem value="week">This Week</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>
|
||||
</Select>
|
||||
|
||||
@ -580,9 +580,12 @@ export function UserAllRequests({ onViewRequest }: RequestsProps) {
|
||||
<SelectValue placeholder="Date Range" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All Time</SelectItem>
|
||||
<SelectItem value="today">Today</SelectItem>
|
||||
<SelectItem value="week">This Week</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>
|
||||
</Select>
|
||||
|
||||
@ -20,7 +20,7 @@ export function useRequestsFilters() {
|
||||
const [approverFilterType, setApproverFilterType] = useState<'current' | 'any'>(
|
||||
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>(
|
||||
searchParams.get('startDate') ? new Date(searchParams.get('startDate')!) : undefined
|
||||
);
|
||||
@ -101,7 +101,7 @@ export function useRequestsFilters() {
|
||||
setInitiatorFilter('all');
|
||||
setApproverFilter('all');
|
||||
setApproverFilterType('current');
|
||||
setDateRange('month');
|
||||
setDateRange('all');
|
||||
setCustomStartDate(undefined);
|
||||
setCustomEndDate(undefined);
|
||||
setShowCustomDatePicker(false);
|
||||
@ -138,7 +138,7 @@ export function useRequestsFilters() {
|
||||
departmentFilter !== 'all' ||
|
||||
initiatorFilter !== 'all' ||
|
||||
approverFilter !== 'all' ||
|
||||
dateRange !== 'month' ||
|
||||
dateRange !== 'all' ||
|
||||
customStartDate ||
|
||||
customEndDate
|
||||
);
|
||||
|
||||
@ -16,7 +16,9 @@ import { ConfigurationManager } from '@/components/admin/ConfigurationManager';
|
||||
import { HolidayManager } from '@/components/admin/HolidayManager';
|
||||
import { UserRoleManager } from '@/components/admin/UserRoleManager';
|
||||
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() {
|
||||
const { user } = useAuth();
|
||||
@ -25,6 +27,26 @@ export function Settings() {
|
||||
const [notificationSuccess, setNotificationSuccess] = useState(false);
|
||||
const [notificationMessage, setNotificationMessage] = useState<string>();
|
||||
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 () => {
|
||||
setIsEnablingNotifications(true);
|
||||
@ -93,6 +115,8 @@ export function Settings() {
|
||||
setNotificationSuccess(true);
|
||||
setNotificationMessage('Push notifications have been successfully enabled! You will now receive notifications for workflow updates, approvals, and TAT alerts.');
|
||||
setShowNotificationModal(true);
|
||||
// Recheck subscription status
|
||||
await checkSubscriptionStatus();
|
||||
} catch (error: any) {
|
||||
console.error('[Settings] Error enabling notifications:', error);
|
||||
setNotificationSuccess(false);
|
||||
@ -177,7 +201,7 @@ export function Settings() {
|
||||
{/* User Settings Tab */}
|
||||
<TabsContent value="user" className="mt-0 space-y-0">
|
||||
<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">
|
||||
<CardHeader className="pb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
@ -185,20 +209,38 @@ export function Settings() {
|
||||
<Bell className="h-5 w-5 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle className="text-lg font-semibold text-gray-900">Notifications</CardTitle>
|
||||
<CardDescription className="text-sm text-gray-600">Manage notification preferences</CardDescription>
|
||||
<CardTitle className="text-lg font-semibold text-gray-900">Browser Push Setup</CardTitle>
|
||||
<CardDescription className="text-sm text-gray-600">Register this browser for push notifications</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<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
|
||||
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"
|
||||
>
|
||||
<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>
|
||||
</div>
|
||||
</CardContent>
|
||||
@ -257,16 +299,18 @@ export function Settings() {
|
||||
</div>
|
||||
<div>
|
||||
<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>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div className="p-4 bg-gradient-to-br from-gray-50 to-gray-100 rounded-xl border border-gray-200">
|
||||
<p className="text-sm text-gray-600 text-center">User preferences will be available soon</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => setShowPreferencesModal(true)}
|
||||
className="w-full bg-re-green hover:bg-re-green/90 text-white shadow-md hover:shadow-lg transition-all"
|
||||
>
|
||||
<Shield className="w-4 h-4 mr-2" />
|
||||
Manage Preferences
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
@ -292,7 +336,7 @@ export function Settings() {
|
||||
<>
|
||||
{/* Non-Admin User Settings Only */}
|
||||
<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">
|
||||
<CardHeader className="pb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
@ -300,20 +344,38 @@ export function Settings() {
|
||||
<Bell className="h-5 w-5 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle className="text-lg font-semibold text-gray-900">Notifications</CardTitle>
|
||||
<CardDescription className="text-sm text-gray-600">Manage notification preferences</CardDescription>
|
||||
<CardTitle className="text-lg font-semibold text-gray-900">Browser Push Setup</CardTitle>
|
||||
<CardDescription className="text-sm text-gray-600">Register this browser for push notifications</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<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
|
||||
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"
|
||||
>
|
||||
<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>
|
||||
</div>
|
||||
</CardContent>
|
||||
@ -372,16 +434,18 @@ export function Settings() {
|
||||
</div>
|
||||
<div>
|
||||
<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>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div className="p-4 bg-gradient-to-br from-gray-50 to-gray-100 rounded-md border border-gray-200">
|
||||
<p className="text-sm text-gray-600 text-center">User preferences will be available soon</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => setShowPreferencesModal(true)}
|
||||
className="w-full bg-re-green hover:bg-re-green/90 text-white shadow-md hover:shadow-lg transition-all"
|
||||
>
|
||||
<Shield className="w-4 h-4 mr-2" />
|
||||
Manage Preferences
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
@ -395,6 +459,11 @@ export function Settings() {
|
||||
success={notificationSuccess}
|
||||
message={notificationMessage}
|
||||
/>
|
||||
|
||||
<NotificationPreferencesModal
|
||||
open={showPreferencesModal}
|
||||
onClose={() => setShowPreferencesModal(false)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -119,9 +119,14 @@ export interface ApproverPerformance {
|
||||
approverId: string;
|
||||
approverName: string;
|
||||
totalApproved: number;
|
||||
approvedCount: number;
|
||||
rejectedCount: number;
|
||||
closedCount: number;
|
||||
tatCompliancePercent: number;
|
||||
avgResponseHours: number;
|
||||
pendingCount: number;
|
||||
withinTatCount: number;
|
||||
breachedCount: number;
|
||||
}
|
||||
|
||||
export interface UpcomingDeadline {
|
||||
@ -158,7 +163,7 @@ export interface PriorityDistribution {
|
||||
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 {
|
||||
/**
|
||||
@ -442,8 +447,17 @@ class DashboardService {
|
||||
|
||||
/**
|
||||
* 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[],
|
||||
pagination: {
|
||||
currentPage: number,
|
||||
@ -453,11 +467,24 @@ class DashboardService {
|
||||
}
|
||||
}> {
|
||||
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) {
|
||||
params.startDate = startDate.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 });
|
||||
return {
|
||||
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
|
||||
*/
|
||||
|
||||
@ -12,27 +12,33 @@ export interface Notification {
|
||||
actionUrl?: string;
|
||||
actionRequired: boolean;
|
||||
metadata?: any;
|
||||
sentVia: string[];
|
||||
readAt?: 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 }) {
|
||||
const response = await apiClient.get('/notifications', { params });
|
||||
return response.data;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get unread count
|
||||
* Get unread notification count
|
||||
*/
|
||||
async getUnreadCount() {
|
||||
const response = await apiClient.get('/notifications/unread-count');
|
||||
return response.data.data.unreadCount;
|
||||
}
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Mark notification as read
|
||||
@ -40,15 +46,15 @@ class NotificationApi {
|
||||
async markAsRead(notificationId: string) {
|
||||
const response = await apiClient.patch(`/notifications/${notificationId}/read`);
|
||||
return response.data;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Mark all as read
|
||||
* Mark all notifications as read
|
||||
*/
|
||||
async markAllAsRead() {
|
||||
const response = await apiClient.post('/notifications/mark-all-read');
|
||||
return response.data;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete notification
|
||||
@ -57,8 +63,16 @@ class NotificationApi {
|
||||
const response = await apiClient.delete(`/notifications/${notificationId}`);
|
||||
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;
|
||||
|
||||
|
||||
37
src/services/userPreferenceApi.ts
Normal file
37
src/services/userPreferenceApi.ts
Normal 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;
|
||||
};
|
||||
|
||||
@ -72,6 +72,11 @@ export const getStatusConfig = (status: string) => {
|
||||
color: 'bg-yellow-100 text-yellow-800 border-yellow-200',
|
||||
label: 'pending'
|
||||
};
|
||||
case 'paused':
|
||||
return {
|
||||
color: 'bg-gray-400 text-gray-100 border-gray-500',
|
||||
label: 'paused'
|
||||
};
|
||||
case 'in-review':
|
||||
return {
|
||||
color: 'bg-blue-100 text-blue-800 border-blue-200',
|
||||
|
||||
Loading…
Reference in New Issue
Block a user