357 lines
14 KiB
TypeScript
357 lines
14 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import {
|
|
Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle
|
|
} from '@/components/ui/dialog';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Label } from '@/components/ui/label';
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
|
import { Switch } from '@/components/ui/switch';
|
|
import { Plus, Trash2, Bell, AlertTriangle } from 'lucide-react';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { toast } from 'sonner';
|
|
import { masterService } from '@/services/master.service';
|
|
import { ROLES, STAGES_MAP } from '@/lib/constants';
|
|
|
|
interface SLADialogProps {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
sla: any | null;
|
|
onSave: () => void;
|
|
}
|
|
|
|
export const SLADialog: React.FC<SLADialogProps> = ({ isOpen, onClose, sla, onSave }) => {
|
|
const [formData, setFormData] = useState<any>({
|
|
activityName: '',
|
|
tatHours: 24,
|
|
tatUnit: 'hours',
|
|
isActive: true,
|
|
reminders: [],
|
|
escalationConfigs: []
|
|
});
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (sla) {
|
|
setFormData({
|
|
...sla,
|
|
reminders: sla.reminders || [],
|
|
escalationConfigs: sla.escalationConfigs || []
|
|
});
|
|
}
|
|
}, [sla]);
|
|
|
|
const handleAddReminder = () => {
|
|
setFormData({
|
|
...formData,
|
|
reminders: [...formData.reminders, { timeValue: 1, timeUnit: 'days', isEnabled: true }]
|
|
});
|
|
};
|
|
|
|
const handleRemoveReminder = (index: number) => {
|
|
const newReminders = [...formData.reminders];
|
|
newReminders.splice(index, 1);
|
|
setFormData({ ...formData, reminders: newReminders });
|
|
};
|
|
|
|
const handleAddEscalation = () => {
|
|
setFormData({
|
|
...formData,
|
|
escalationConfigs: [
|
|
...formData.escalationConfigs,
|
|
{ level: formData.escalationConfigs.length + 1, timeValue: 1, timeUnit: 'days', notifyEmail: '' }
|
|
]
|
|
});
|
|
};
|
|
|
|
const handleRemoveEscalation = (index: number) => {
|
|
const newEsc = [...formData.escalationConfigs];
|
|
newEsc.splice(index, 1);
|
|
// Re-adjust levels
|
|
const adjustedEsc = newEsc.map((e, i) => ({ ...e, level: i + 1 }));
|
|
setFormData({ ...formData, escalationConfigs: adjustedEsc });
|
|
};
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
if (!formData.activityName || !formData.ownerRole) {
|
|
toast.error('Activity Name and Owner Role are required');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
setLoading(true);
|
|
await masterService.saveSlaConfig(formData);
|
|
toast.success(sla?.id ? 'SLA Configuration updated' : 'New SLA Configuration created');
|
|
onSave();
|
|
onClose();
|
|
} catch (error) {
|
|
console.error('Save SLA error:', error);
|
|
toast.error('Failed to save SLA configuration');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Dialog open={isOpen} onOpenChange={onClose}>
|
|
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
|
|
<DialogHeader>
|
|
<DialogTitle>{sla?.id ? 'Configure SLA' : 'Add New SLA'}: {formData.activityName || 'New Activity'}</DialogTitle>
|
|
<DialogDescription>
|
|
Define the Turn Around Time and notification rules for this stage.
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<form onSubmit={handleSubmit} className="space-y-6 py-4">
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="activityName">Activity Name (Workflow Stage)</Label>
|
|
<Select
|
|
value={formData.activityName}
|
|
onValueChange={(value) => {
|
|
let inferredRole = formData.ownerRole;
|
|
// Try to find the default owner in any module
|
|
for (const moduleStages of Object.values(STAGES_MAP)) {
|
|
if ((moduleStages as any)[value]) {
|
|
inferredRole = (moduleStages as any)[value];
|
|
break;
|
|
}
|
|
}
|
|
setFormData({ ...formData, activityName: value, ownerRole: inferredRole });
|
|
}}
|
|
disabled={!!sla?.id}
|
|
>
|
|
<SelectTrigger id="activityName">
|
|
<SelectValue placeholder="Select Stage" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{Object.entries(STAGES_MAP).map(([module, stages]) => (
|
|
<React.Fragment key={module}>
|
|
<div className="px-2 py-1.5 text-xs font-semibold text-slate-500 bg-slate-50">{module}</div>
|
|
{Object.keys(stages).map(stage => (
|
|
<SelectItem key={stage} value={stage}>{stage}</SelectItem>
|
|
))}
|
|
</React.Fragment>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="ownerRole">Owner Role (Auto-resolved)</Label>
|
|
<Select
|
|
value={formData.ownerRole}
|
|
onValueChange={(value) => setFormData({ ...formData, ownerRole: value })}
|
|
disabled={!!sla?.id}
|
|
>
|
|
<SelectTrigger id="ownerRole">
|
|
<SelectValue placeholder="Select Role" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{Object.values(ROLES).map(role => (
|
|
<SelectItem key={role} value={role}>{role}</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="tatHours">Target TAT</Label>
|
|
<div className="flex gap-2">
|
|
<Input
|
|
id="tatHours"
|
|
type="number"
|
|
value={formData.tatHours}
|
|
onChange={(e) => setFormData({ ...formData, tatHours: parseInt(e.target.value) })}
|
|
/>
|
|
<Select
|
|
value={formData.tatUnit}
|
|
onValueChange={(value) => setFormData({ ...formData, tatUnit: value })}
|
|
>
|
|
<SelectTrigger className="w-32">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="hours">Hours</SelectItem>
|
|
<SelectItem value="days">Days</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center space-x-2 pt-8">
|
|
<Switch
|
|
id="isActive"
|
|
checked={formData.isActive}
|
|
onCheckedChange={(checked) => setFormData({ ...formData, isActive: checked })}
|
|
/>
|
|
<Label htmlFor="isActive">Active SLA Tracking</Label>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-4">
|
|
<div className="flex items-center justify-between border-b pb-2">
|
|
<div className="flex items-center gap-2">
|
|
<Bell className="w-4 h-4 text-blue-600" />
|
|
<h4 className="font-medium text-sm">Reminders</h4>
|
|
</div>
|
|
<Button type="button" variant="outline" size="sm" onClick={handleAddReminder} className="h-7 text-xs">
|
|
<Plus className="w-3 h-3 mr-1" /> Add Reminder
|
|
</Button>
|
|
</div>
|
|
|
|
<div className="space-y-3">
|
|
{formData.reminders.map((reminder: any, idx: number) => (
|
|
<div key={idx} className="flex items-center gap-3 bg-slate-50 p-2 rounded-lg border border-slate-100">
|
|
<span className="text-xs font-medium text-slate-500 w-12 text-center">Before</span>
|
|
<Input
|
|
type="number"
|
|
className="w-20 h-8"
|
|
value={reminder.timeValue}
|
|
onChange={(e) => {
|
|
const newReminders = [...formData.reminders];
|
|
newReminders[idx].timeValue = parseInt(e.target.value);
|
|
setFormData({ ...formData, reminders: newReminders });
|
|
}}
|
|
/>
|
|
<Select
|
|
value={reminder.timeUnit}
|
|
onValueChange={(value) => {
|
|
const newReminders = [...formData.reminders];
|
|
newReminders[idx].timeUnit = value;
|
|
setFormData({ ...formData, reminders: newReminders });
|
|
}}
|
|
>
|
|
<SelectTrigger className="w-24 h-8 text-xs">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="hours">Hours</SelectItem>
|
|
<SelectItem value="days">Days</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => handleRemoveReminder(idx)}
|
|
className="h-8 w-8 p-0 text-slate-400 hover:text-red-500"
|
|
>
|
|
<Trash2 className="w-4 h-4" />
|
|
</Button>
|
|
</div>
|
|
))}
|
|
{formData.reminders.length === 0 && (
|
|
<p className="text-center text-xs text-slate-400 py-2">No reminders set</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-4">
|
|
<div className="flex items-center justify-between border-b pb-2">
|
|
<div className="flex items-center gap-2">
|
|
<AlertTriangle className="w-4 h-4 text-red-600" />
|
|
<h4 className="font-medium text-sm">Escalation Levels</h4>
|
|
</div>
|
|
<Button type="button" variant="outline" size="sm" onClick={handleAddEscalation} className="h-7 text-xs">
|
|
<Plus className="w-3 h-3 mr-1" /> Add Level
|
|
</Button>
|
|
</div>
|
|
|
|
<div className="space-y-3">
|
|
{formData.escalationConfigs.map((esc: any, idx: number) => (
|
|
<div key={idx} className="bg-slate-50 p-3 rounded-lg border border-slate-100 space-y-3">
|
|
<div className="flex items-center justify-between">
|
|
<Badge variant="outline" className="bg-red-50 text-red-700 border-red-100">
|
|
Level {esc.level}
|
|
</Badge>
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => handleRemoveEscalation(idx)}
|
|
className="h-7 w-7 p-0 text-slate-400 hover:text-red-500"
|
|
>
|
|
<Trash2 className="w-3.5 h-3.5" />
|
|
</Button>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div className="space-y-1.5">
|
|
<Label className="text-[10px] uppercase text-slate-500">Escalate After Breaching By</Label>
|
|
<div className="flex gap-2">
|
|
<Input
|
|
type="number"
|
|
className="h-8"
|
|
value={esc.timeValue}
|
|
onChange={(e) => {
|
|
const newEsc = [...formData.escalationConfigs];
|
|
newEsc[idx].timeValue = parseInt(e.target.value);
|
|
setFormData({ ...formData, escalationConfigs: newEsc });
|
|
}}
|
|
/>
|
|
<Select
|
|
value={esc.timeUnit}
|
|
onValueChange={(value) => {
|
|
const newEsc = [...formData.escalationConfigs];
|
|
newEsc[idx].timeUnit = value;
|
|
setFormData({ ...formData, escalationConfigs: newEsc });
|
|
}}
|
|
>
|
|
<SelectTrigger className="w-24 h-8 text-xs">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="hours">Hours</SelectItem>
|
|
<SelectItem value="days">Days</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-1.5">
|
|
<Label className="text-[10px] uppercase text-slate-500">Notification Recipient (Role)</Label>
|
|
<Select
|
|
value={esc.notifyRole || ''}
|
|
onValueChange={(value) => {
|
|
const newEsc = [...formData.escalationConfigs];
|
|
newEsc[idx].notifyRole = value;
|
|
newEsc[idx].notifyEmail = ''; // Clear explicit email
|
|
setFormData({ ...formData, escalationConfigs: newEsc });
|
|
}}
|
|
>
|
|
<SelectTrigger className="h-8 text-xs">
|
|
<SelectValue placeholder="Select Recipient Role" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{Object.values(ROLES).map(role => (
|
|
<SelectItem key={role} value={role}>{role}</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
{formData.escalationConfigs.length === 0 && (
|
|
<p className="text-center text-xs text-slate-400 py-2">No escalation levels defined</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<DialogFooter className="pt-4 border-t">
|
|
<Button type="button" variant="outline" onClick={onClose} disabled={loading}>
|
|
Cancel
|
|
</Button>
|
|
<Button type="submit" disabled={loading}>
|
|
{loading ? 'Saving...' : 'Save Configuration'}
|
|
</Button>
|
|
</DialogFooter>
|
|
</form>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
};
|