activity type added in admi settings
This commit is contained in:
parent
94b7c34a7a
commit
d725e523b3
451
src/components/admin/ActivityTypeManager.tsx
Normal file
451
src/components/admin/ActivityTypeManager.tsx
Normal file
@ -0,0 +1,451 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
|
import {
|
||||||
|
FileText,
|
||||||
|
Plus,
|
||||||
|
Trash2,
|
||||||
|
Edit2,
|
||||||
|
Loader2,
|
||||||
|
AlertCircle,
|
||||||
|
CheckCircle,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import {
|
||||||
|
getAllActivityTypes,
|
||||||
|
createActivityType,
|
||||||
|
updateActivityType,
|
||||||
|
deleteActivityType,
|
||||||
|
ActivityType
|
||||||
|
} from '@/services/adminApi';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
export function ActivityTypeManager() {
|
||||||
|
const [activityTypes, setActivityTypes] = useState<ActivityType[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [showAddDialog, setShowAddDialog] = useState(false);
|
||||||
|
const [editingActivityType, setEditingActivityType] = useState<ActivityType | null>(null);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [successMessage, setSuccessMessage] = useState<string | null>(null);
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
title: '',
|
||||||
|
itemCode: '',
|
||||||
|
taxationType: '',
|
||||||
|
sapRefNo: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadActivityTypes();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const loadActivityTypes = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
const data = await getAllActivityTypes(false); // Get all including inactive
|
||||||
|
setActivityTypes(data);
|
||||||
|
} catch (err: any) {
|
||||||
|
const errorMsg = err.response?.data?.error || 'Failed to load activity types';
|
||||||
|
setError(errorMsg);
|
||||||
|
toast.error(errorMsg);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAdd = () => {
|
||||||
|
setFormData({
|
||||||
|
title: '',
|
||||||
|
itemCode: '',
|
||||||
|
taxationType: '',
|
||||||
|
sapRefNo: ''
|
||||||
|
});
|
||||||
|
setEditingActivityType(null);
|
||||||
|
setShowAddDialog(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEdit = (activityType: ActivityType) => {
|
||||||
|
setFormData({
|
||||||
|
title: activityType.title,
|
||||||
|
itemCode: activityType.itemCode || '',
|
||||||
|
taxationType: activityType.taxationType || '',
|
||||||
|
sapRefNo: activityType.sapRefNo || ''
|
||||||
|
});
|
||||||
|
setEditingActivityType(activityType);
|
||||||
|
setShowAddDialog(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
try {
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
if (!formData.title.trim()) {
|
||||||
|
setError('Activity type title is required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload: Partial<ActivityType> = {
|
||||||
|
title: formData.title.trim(),
|
||||||
|
itemCode: formData.itemCode.trim() || null,
|
||||||
|
taxationType: formData.taxationType.trim() || null,
|
||||||
|
sapRefNo: formData.sapRefNo.trim() || null
|
||||||
|
};
|
||||||
|
|
||||||
|
if (editingActivityType) {
|
||||||
|
// Update existing
|
||||||
|
await updateActivityType(editingActivityType.activityTypeId, payload);
|
||||||
|
setSuccessMessage('Activity type updated successfully');
|
||||||
|
toast.success('Activity type updated successfully');
|
||||||
|
} else {
|
||||||
|
// Create new
|
||||||
|
await createActivityType(payload);
|
||||||
|
setSuccessMessage('Activity type created successfully');
|
||||||
|
toast.success('Activity type created successfully');
|
||||||
|
}
|
||||||
|
|
||||||
|
await loadActivityTypes();
|
||||||
|
setShowAddDialog(false);
|
||||||
|
setTimeout(() => setSuccessMessage(null), 3000);
|
||||||
|
} catch (err: any) {
|
||||||
|
const errorMsg = err.response?.data?.error || 'Failed to save activity type';
|
||||||
|
setError(errorMsg);
|
||||||
|
toast.error(errorMsg);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async (activityType: ActivityType) => {
|
||||||
|
if (!confirm(`Delete "${activityType.title}"? This will deactivate the activity type.`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setError(null);
|
||||||
|
await deleteActivityType(activityType.activityTypeId);
|
||||||
|
setSuccessMessage('Activity type deleted successfully');
|
||||||
|
toast.success('Activity type deleted successfully');
|
||||||
|
await loadActivityTypes();
|
||||||
|
setTimeout(() => setSuccessMessage(null), 3000);
|
||||||
|
} catch (err: any) {
|
||||||
|
const errorMsg = err.response?.data?.error || 'Failed to delete activity type';
|
||||||
|
setError(errorMsg);
|
||||||
|
toast.error(errorMsg);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Filter active and inactive activity types
|
||||||
|
const activeActivityTypes = activityTypes.filter(at => at.isActive !== false && at.isActive !== undefined);
|
||||||
|
const inactiveActivityTypes = activityTypes.filter(at => at.isActive === false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Success Message */}
|
||||||
|
{successMessage && (
|
||||||
|
<div className="p-4 bg-gradient-to-r from-green-50 to-emerald-50 border border-green-300 rounded-md shadow-sm flex items-center gap-3 animate-in fade-in slide-in-from-top-2 duration-300">
|
||||||
|
<div className="p-1.5 bg-green-500 rounded-md">
|
||||||
|
<CheckCircle className="w-4 h-4 text-white shrink-0" />
|
||||||
|
</div>
|
||||||
|
<p className="text-sm font-medium text-green-900">{successMessage}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Error Message */}
|
||||||
|
{error && (
|
||||||
|
<div className="p-4 bg-gradient-to-r from-red-50 to-rose-50 border border-red-300 rounded-md shadow-sm flex items-center gap-3 animate-in fade-in slide-in-from-top-2 duration-300">
|
||||||
|
<div className="p-1.5 bg-red-500 rounded-md">
|
||||||
|
<AlertCircle className="w-4 h-4 text-white shrink-0" />
|
||||||
|
</div>
|
||||||
|
<p className="text-sm font-medium text-red-900">{error}</p>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => setError(null)}
|
||||||
|
className="ml-auto hover:bg-red-100"
|
||||||
|
>
|
||||||
|
Dismiss
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Header */}
|
||||||
|
<Card className="shadow-lg border-0 rounded-md">
|
||||||
|
<CardHeader className="border-b border-slate-100 py-4 sm:py-5">
|
||||||
|
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3 sm:gap-4">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="p-2.5 bg-gradient-to-br from-slate-600 to-slate-700 rounded-md shadow-md">
|
||||||
|
<FileText className="w-5 h-5 text-white" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<CardTitle className="text-lg sm:text-xl font-semibold text-slate-900">Activity Types</CardTitle>
|
||||||
|
<CardDescription className="text-sm">
|
||||||
|
Manage dealer claim activity types
|
||||||
|
</CardDescription>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
onClick={handleAdd}
|
||||||
|
className="gap-2 bg-re-green hover:bg-re-green/90 text-white shadow-sm"
|
||||||
|
>
|
||||||
|
<Plus className="w-4 h-4" />
|
||||||
|
<span className="hidden xs:inline">Add Activity Type</span>
|
||||||
|
<span className="xs:hidden">Add</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Activity Types List */}
|
||||||
|
{loading ? (
|
||||||
|
<div className="flex items-center justify-center py-12">
|
||||||
|
<Loader2 className="w-8 h-8 animate-spin text-gray-400" />
|
||||||
|
</div>
|
||||||
|
) : activeActivityTypes.length === 0 ? (
|
||||||
|
<Card className="shadow-lg border-0 rounded-md">
|
||||||
|
<CardContent className="p-12 text-center">
|
||||||
|
<div className="p-4 bg-slate-100 rounded-full w-20 h-20 flex items-center justify-center mx-auto mb-4">
|
||||||
|
<FileText className="w-10 h-10 text-slate-400" />
|
||||||
|
</div>
|
||||||
|
<p className="text-slate-700 font-medium text-lg">No activity types found</p>
|
||||||
|
<p className="text-sm text-slate-500 mt-2 mb-6">Add activity types for dealer claim management</p>
|
||||||
|
<Button
|
||||||
|
onClick={handleAdd}
|
||||||
|
variant="outline"
|
||||||
|
className="gap-2 border-slate-300 hover:bg-slate-50 hover:border-slate-400 shadow-sm"
|
||||||
|
>
|
||||||
|
<Plus className="w-4 h-4" />
|
||||||
|
Add First Activity Type
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-4 sm:space-y-6">
|
||||||
|
{/* Active Activity Types */}
|
||||||
|
<Card className="shadow-lg border-0 rounded-md">
|
||||||
|
<CardHeader className="pb-3 sm:pb-4 border-b border-slate-100">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<CardTitle className="text-base sm:text-lg font-semibold text-slate-900">Active Activity Types</CardTitle>
|
||||||
|
<CardDescription className="text-xs sm:text-sm">
|
||||||
|
{activeActivityTypes.length} active type{activeActivityTypes.length !== 1 ? 's' : ''}
|
||||||
|
</CardDescription>
|
||||||
|
</div>
|
||||||
|
<div className="p-2 bg-green-50 rounded-md">
|
||||||
|
<CheckCircle className="w-4 h-4 text-green-600" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-3 pt-4">
|
||||||
|
{activeActivityTypes.map(activityType => (
|
||||||
|
<div
|
||||||
|
key={activityType.activityTypeId}
|
||||||
|
className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 sm:gap-4 p-3 sm:p-4 bg-slate-50 border border-slate-200 rounded-md hover:bg-slate-100 hover:border-slate-300 transition-all shadow-sm"
|
||||||
|
>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center gap-2 mb-2 flex-wrap">
|
||||||
|
<p className="font-semibold text-slate-900 text-sm sm:text-base">{activityType.title}</p>
|
||||||
|
<Badge variant="outline" className="bg-gradient-to-r from-green-50 to-emerald-50 text-green-800 border-green-300 text-[10px] sm:text-xs font-medium shadow-sm">
|
||||||
|
Active
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-3 text-xs sm:text-sm text-slate-600">
|
||||||
|
{activityType.itemCode && (
|
||||||
|
<span className="font-medium">Item Code: <span className="text-slate-900">{activityType.itemCode}</span></span>
|
||||||
|
)}
|
||||||
|
{activityType.taxationType && (
|
||||||
|
<span className="font-medium">Taxation: <span className="text-slate-900">{activityType.taxationType}</span></span>
|
||||||
|
)}
|
||||||
|
{activityType.sapRefNo && (
|
||||||
|
<span className="font-medium">SAP Ref: <span className="text-slate-900">{activityType.sapRefNo}</span></span>
|
||||||
|
)}
|
||||||
|
{!activityType.itemCode && !activityType.taxationType && !activityType.sapRefNo && (
|
||||||
|
<span className="text-slate-500 italic">No additional details</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 sm:gap-2 self-end sm:self-auto">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => handleEdit(activityType)}
|
||||||
|
className="gap-1.5 hover:bg-blue-50 border border-transparent hover:border-blue-200 text-xs sm:text-sm"
|
||||||
|
>
|
||||||
|
<Edit2 className="w-3.5 h-3.5 sm:w-4 sm:h-4" />
|
||||||
|
<span className="hidden xs:inline">Edit</span>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => handleDelete(activityType)}
|
||||||
|
className="gap-1.5 text-red-600 hover:text-red-700 hover:bg-red-50 border border-transparent hover:border-red-200 text-xs sm:text-sm"
|
||||||
|
>
|
||||||
|
<Trash2 className="w-3.5 h-3.5 sm:w-4 sm:h-4" />
|
||||||
|
<span className="hidden xs:inline">Delete</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Inactive Activity Types */}
|
||||||
|
{inactiveActivityTypes.length > 0 && (
|
||||||
|
<Card className="shadow-lg border-0 rounded-md border-amber-200">
|
||||||
|
<CardHeader className="pb-3 sm:pb-4 border-b border-slate-100">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<CardTitle className="text-base sm:text-lg font-semibold text-slate-900">Inactive Activity Types</CardTitle>
|
||||||
|
<CardDescription className="text-xs sm:text-sm">
|
||||||
|
{inactiveActivityTypes.length} inactive type{inactiveActivityTypes.length !== 1 ? 's' : ''}
|
||||||
|
</CardDescription>
|
||||||
|
</div>
|
||||||
|
<div className="p-2 bg-amber-50 rounded-md">
|
||||||
|
<AlertCircle className="w-4 h-4 text-amber-600" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-3 pt-4">
|
||||||
|
{inactiveActivityTypes.map(activityType => (
|
||||||
|
<div
|
||||||
|
key={activityType.activityTypeId}
|
||||||
|
className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 sm:gap-4 p-3 sm:p-4 bg-amber-50/50 border border-amber-200 rounded-md hover:bg-amber-50 hover:border-amber-300 transition-all shadow-sm"
|
||||||
|
>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center gap-2 mb-2 flex-wrap">
|
||||||
|
<p className="font-semibold text-slate-700 text-sm sm:text-base line-through">{activityType.title}</p>
|
||||||
|
<Badge variant="outline" className="bg-gradient-to-r from-amber-50 to-orange-50 text-amber-800 border-amber-300 text-[10px] sm:text-xs font-medium shadow-sm">
|
||||||
|
Inactive
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 sm:gap-2 self-end sm:self-auto">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => handleEdit(activityType)}
|
||||||
|
className="gap-1.5 hover:bg-blue-50 border border-transparent hover:border-blue-200 text-xs sm:text-sm"
|
||||||
|
>
|
||||||
|
<Edit2 className="w-3.5 h-3.5 sm:w-4 sm:h-4" />
|
||||||
|
<span className="hidden xs:inline">Edit</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Add/Edit Dialog */}
|
||||||
|
<Dialog open={showAddDialog} onOpenChange={setShowAddDialog}>
|
||||||
|
<DialogContent className="sm:max-w-[550px] max-h-[90vh] rounded-lg flex flex-col p-0">
|
||||||
|
<DialogHeader className="pb-4 border-b border-slate-100 px-6 pt-6 flex-shrink-0">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="p-2.5 bg-gradient-to-br from-slate-600 to-slate-700 rounded-lg shadow-md">
|
||||||
|
<FileText className="w-5 h-5 text-white" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<DialogTitle className="text-xl font-semibold text-slate-900">
|
||||||
|
{editingActivityType ? 'Edit Activity Type' : 'Add New Activity Type'}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription className="text-sm text-slate-600 mt-1">
|
||||||
|
{editingActivityType ? 'Update activity type information' : 'Add a new activity type for dealer claim management'}
|
||||||
|
</DialogDescription>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-5 py-6 px-6 overflow-y-auto flex-1 min-h-0">
|
||||||
|
{/* Title Field */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="title" className="text-sm font-semibold text-slate-900 flex items-center gap-1">
|
||||||
|
Title <span className="text-red-500">*</span>
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="title"
|
||||||
|
placeholder="e.g., Riders Mania Claims, Legal Claims Reimbursement"
|
||||||
|
value={formData.title}
|
||||||
|
onChange={(e) => setFormData({ ...formData, title: e.target.value })}
|
||||||
|
className="h-11 border-slate-300 focus:border-re-green focus:ring-2 focus:ring-re-green/20 rounded-lg transition-all shadow-sm"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-slate-500">Enter the activity type title</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Item Code Field */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="itemCode" className="text-sm font-semibold text-slate-900">
|
||||||
|
Item Code <span className="text-slate-400 font-normal text-xs">(Optional)</span>
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="itemCode"
|
||||||
|
placeholder="e.g., 1, 2, 3"
|
||||||
|
value={formData.itemCode}
|
||||||
|
onChange={(e) => setFormData({ ...formData, itemCode: e.target.value })}
|
||||||
|
className="h-11 border-slate-300 focus:border-re-green focus:ring-2 focus:ring-re-green/20 rounded-lg transition-all shadow-sm"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-slate-500">Optional item code for the activity type</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Taxation Type Field */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="taxationType" className="text-sm font-semibold text-slate-900">
|
||||||
|
Taxation Type <span className="text-slate-400 font-normal text-xs">(Optional)</span>
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="taxationType"
|
||||||
|
placeholder="e.g., GST, VAT, Exempt"
|
||||||
|
value={formData.taxationType}
|
||||||
|
onChange={(e) => setFormData({ ...formData, taxationType: e.target.value })}
|
||||||
|
className="h-11 border-slate-300 focus:border-re-green focus:ring-2 focus:ring-re-green/20 rounded-lg transition-all shadow-sm"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-slate-500">Optional taxation type for the activity</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* SAP Reference Number Field */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="sapRefNo" className="text-sm font-semibold text-slate-900">
|
||||||
|
SAP Reference Number <span className="text-slate-400 font-normal text-xs">(Optional)</span>
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="sapRefNo"
|
||||||
|
placeholder="e.g., SAP-12345"
|
||||||
|
value={formData.sapRefNo}
|
||||||
|
onChange={(e) => setFormData({ ...formData, sapRefNo: e.target.value })}
|
||||||
|
className="h-11 border-slate-300 focus:border-re-green focus:ring-2 focus:ring-re-green/20 rounded-lg transition-all shadow-sm"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-slate-500">Optional SAP reference number</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter className="gap-3 pt-4 border-t border-slate-100 px-6 pb-6 flex-shrink-0">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setShowAddDialog(false)}
|
||||||
|
className="h-11 border-slate-300 hover:bg-slate-50 hover:border-slate-400 shadow-sm"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleSave}
|
||||||
|
disabled={!formData.title.trim()}
|
||||||
|
className="h-11 bg-re-green hover:bg-re-green/90 text-white shadow-md hover:shadow-lg transition-all disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
<FileText className="w-4 h-4 mr-2" />
|
||||||
|
{editingActivityType ? 'Update Activity Type' : 'Add Activity Type'}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@ -1,3 +1,4 @@
|
|||||||
export { ConfigurationManager } from './ConfigurationManager';
|
export { ConfigurationManager } from './ConfigurationManager';
|
||||||
export { HolidayManager } from './HolidayManager';
|
export { HolidayManager } from './HolidayManager';
|
||||||
|
export { ActivityTypeManager } from './ActivityTypeManager';
|
||||||
|
|
||||||
|
|||||||
@ -278,7 +278,7 @@ function CustomRequestDetailInner({ requestId: propRequestId, onBack, dynamicReq
|
|||||||
};
|
};
|
||||||
|
|
||||||
const needsClosure = (request?.status === 'approved' || request?.status === 'rejected') && isInitiator;
|
const needsClosure = (request?.status === 'approved' || request?.status === 'rejected') && isInitiator;
|
||||||
const isClosed = request?.status === 'closed' || (request?.status === 'approved' && !isInitiator) || (request?.status === 'rejected' && !isInitiator);
|
const isClosed = request?.status === 'closed';
|
||||||
|
|
||||||
// Fetch summary details if request is closed
|
// Fetch summary details if request is closed
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@ -32,7 +32,7 @@ import { toast } from 'sonner';
|
|||||||
import { getAllDealers as fetchDealersFromAPI, verifyDealerLogin, type DealerInfo } from '@/services/dealerApi';
|
import { getAllDealers as fetchDealersFromAPI, verifyDealerLogin, type DealerInfo } from '@/services/dealerApi';
|
||||||
import { ClaimApproverSelectionStep } from './ClaimApproverSelectionStep';
|
import { ClaimApproverSelectionStep } from './ClaimApproverSelectionStep';
|
||||||
import { useAuth } from '@/contexts/AuthContext';
|
import { useAuth } from '@/contexts/AuthContext';
|
||||||
import { getPublicConfigurations, AdminConfiguration } from '@/services/adminApi';
|
import { getPublicConfigurations, AdminConfiguration, getActivityTypes, type ActivityType } from '@/services/adminApi';
|
||||||
import { PolicyViolationModal } from '@/components/modals/PolicyViolationModal';
|
import { PolicyViolationModal } from '@/components/modals/PolicyViolationModal';
|
||||||
|
|
||||||
// CLAIM_STEPS definition (same as in ClaimApproverSelectionStep)
|
// CLAIM_STEPS definition (same as in ClaimApproverSelectionStep)
|
||||||
@ -52,22 +52,6 @@ interface ClaimManagementWizardProps {
|
|||||||
onSubmit?: (claimData: any) => void;
|
onSubmit?: (claimData: any) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CLAIM_TYPES = [
|
|
||||||
'Riders Mania Claims',
|
|
||||||
'Marketing Cost – Bike to Vendor',
|
|
||||||
'Media Bike Service',
|
|
||||||
'ARAI Motorcycle Liquidation',
|
|
||||||
'ARAI Certification – STA Approval CNR',
|
|
||||||
'Procurement of Spares/Apparel/GMA for Events',
|
|
||||||
'Fuel for Media Bike Used for Event',
|
|
||||||
'Motorcycle Buyback and Goodwill Support',
|
|
||||||
'Liquidation of Used Motorcycle',
|
|
||||||
'Motorcycle Registration CNR (Owned or Gifted by RE)',
|
|
||||||
'Legal Claims Reimbursement',
|
|
||||||
'Service Camp Claims',
|
|
||||||
'Corporate Claims – Institutional Sales PDI'
|
|
||||||
];
|
|
||||||
|
|
||||||
const STEP_NAMES = [
|
const STEP_NAMES = [
|
||||||
'Claim Details',
|
'Claim Details',
|
||||||
'Approver Selection',
|
'Approver Selection',
|
||||||
@ -101,6 +85,27 @@ export function ClaimManagementWizard({ onBack, onSubmit }: ClaimManagementWizar
|
|||||||
violations: []
|
violations: []
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const [activityTypes, setActivityTypes] = useState<ActivityType[]>([]);
|
||||||
|
const [loadingActivityTypes, setLoadingActivityTypes] = useState(true);
|
||||||
|
|
||||||
|
// Load activity types from API on mount
|
||||||
|
useEffect(() => {
|
||||||
|
const loadActivityTypes = async () => {
|
||||||
|
try {
|
||||||
|
setLoadingActivityTypes(true);
|
||||||
|
const types = await getActivityTypes();
|
||||||
|
setActivityTypes(types);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load activity types:', error);
|
||||||
|
toast.error('Failed to load activity types. Please refresh the page.');
|
||||||
|
} finally {
|
||||||
|
setLoadingActivityTypes(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadActivityTypes();
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Load system policy on mount
|
// Load system policy on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadSystemPolicy = async () => {
|
const loadSystemPolicy = async () => {
|
||||||
@ -481,14 +486,26 @@ export function ClaimManagementWizard({ onBack, onSubmit }: ClaimManagementWizar
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="activityType" className="text-base font-semibold">Activity Type *</Label>
|
<Label htmlFor="activityType" className="text-base font-semibold">Activity Type *</Label>
|
||||||
<Select value={formData.activityType} onValueChange={(value) => updateFormData('activityType', value)}>
|
<Select
|
||||||
|
value={formData.activityType}
|
||||||
|
onValueChange={(value) => updateFormData('activityType', value)}
|
||||||
|
disabled={loadingActivityTypes}
|
||||||
|
>
|
||||||
<SelectTrigger className="mt-2 !h-12 data-[size=default]:!h-12" id="activityType">
|
<SelectTrigger className="mt-2 !h-12 data-[size=default]:!h-12" id="activityType">
|
||||||
<SelectValue placeholder="Select activity type" />
|
<SelectValue placeholder={loadingActivityTypes ? "Loading activity types..." : "Select activity type"} />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{CLAIM_TYPES.map((type) => (
|
{activityTypes.length > 0 ? (
|
||||||
<SelectItem key={type} value={type}>{type}</SelectItem>
|
activityTypes.map((type) => (
|
||||||
))}
|
<SelectItem key={type.activityTypeId} value={type.title}>
|
||||||
|
{type.title}
|
||||||
|
</SelectItem>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className="px-2 py-1.5 text-sm text-gray-500 text-center">
|
||||||
|
No activity types available
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -342,7 +342,7 @@ function DealerClaimRequestDetailInner({ requestId: propRequestId, onBack, dynam
|
|||||||
setShowShareSummaryModal(true);
|
setShowShareSummaryModal(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const isClosed = request?.status === 'closed' || (request?.status === 'approved' && !isInitiator) || (request?.status === 'rejected' && !isInitiator);
|
const isClosed = request?.status === 'closed';
|
||||||
|
|
||||||
// Fetch summary details if request is closed
|
// Fetch summary details if request is closed
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@ -8,13 +8,15 @@ import {
|
|||||||
Palette,
|
Palette,
|
||||||
Lock,
|
Lock,
|
||||||
Calendar,
|
Calendar,
|
||||||
Sliders
|
Sliders,
|
||||||
|
FileText
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { setupPushNotifications } from '@/utils/pushNotifications';
|
import { setupPushNotifications } from '@/utils/pushNotifications';
|
||||||
import { useAuth, isAdmin as checkIsAdmin } from '@/contexts/AuthContext';
|
import { useAuth, isAdmin as checkIsAdmin } from '@/contexts/AuthContext';
|
||||||
import { ConfigurationManager } from '@/components/admin/ConfigurationManager';
|
import { ConfigurationManager } from '@/components/admin/ConfigurationManager';
|
||||||
import { HolidayManager } from '@/components/admin/HolidayManager';
|
import { HolidayManager } from '@/components/admin/HolidayManager';
|
||||||
import { UserRoleManager } from '@/components/admin/UserRoleManager';
|
import { UserRoleManager } from '@/components/admin/UserRoleManager';
|
||||||
|
import { ActivityTypeManager } from '@/components/admin/ActivityTypeManager';
|
||||||
import { NotificationStatusModal } from '@/components/settings/NotificationStatusModal';
|
import { NotificationStatusModal } from '@/components/settings/NotificationStatusModal';
|
||||||
import { NotificationPreferencesModal } from '@/components/settings/NotificationPreferencesModal';
|
import { NotificationPreferencesModal } from '@/components/settings/NotificationPreferencesModal';
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
@ -30,6 +32,7 @@ export function Settings() {
|
|||||||
const [showPreferencesModal, setShowPreferencesModal] = useState(false);
|
const [showPreferencesModal, setShowPreferencesModal] = useState(false);
|
||||||
const [hasSubscription, setHasSubscription] = useState(false);
|
const [hasSubscription, setHasSubscription] = useState(false);
|
||||||
const [checkingSubscription, setCheckingSubscription] = useState(true);
|
const [checkingSubscription, setCheckingSubscription] = useState(true);
|
||||||
|
const [showActivityTypeManager, setShowActivityTypeManager] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
checkSubscriptionStatus();
|
checkSubscriptionStatus();
|
||||||
@ -161,7 +164,7 @@ export function Settings() {
|
|||||||
{/* Tabs for Admin, Cards for Non-Admin */}
|
{/* Tabs for Admin, Cards for Non-Admin */}
|
||||||
{isAdmin ? (
|
{isAdmin ? (
|
||||||
<Tabs defaultValue="user" className="w-full">
|
<Tabs defaultValue="user" className="w-full">
|
||||||
<TabsList className="grid w-full grid-cols-2 lg:grid-cols-4 mb-8 bg-slate-100 p-1 rounded-xl h-auto gap-1">
|
<TabsList className="grid w-full grid-cols-2 lg:grid-cols-5 mb-8 bg-slate-100 p-1 rounded-xl h-auto gap-1">
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
value="user"
|
value="user"
|
||||||
className="flex items-center justify-center gap-2 py-3 rounded-lg data-[state=active]:bg-white data-[state=active]:shadow-md transition-all"
|
className="flex items-center justify-center gap-2 py-3 rounded-lg data-[state=active]:bg-white data-[state=active]:shadow-md transition-all"
|
||||||
@ -194,6 +197,14 @@ export function Settings() {
|
|||||||
<span className="hidden sm:inline">Holidays</span>
|
<span className="hidden sm:inline">Holidays</span>
|
||||||
<span className="sm:hidden">Holidays</span>
|
<span className="sm:hidden">Holidays</span>
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
|
<TabsTrigger
|
||||||
|
value="templates"
|
||||||
|
className="flex items-center justify-center gap-2 py-3 rounded-lg data-[state=active]:bg-white data-[state=active]:shadow-md transition-all"
|
||||||
|
>
|
||||||
|
<FileText className="w-4 h-4" />
|
||||||
|
<span className="hidden sm:inline">Templates</span>
|
||||||
|
<span className="sm:hidden">Templates</span>
|
||||||
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
{/* Fixed width container to prevent layout shifts */}
|
{/* Fixed width container to prevent layout shifts */}
|
||||||
@ -330,6 +341,91 @@ export function Settings() {
|
|||||||
<TabsContent value="holidays" className="mt-0">
|
<TabsContent value="holidays" className="mt-0">
|
||||||
<HolidayManager />
|
<HolidayManager />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
|
{/* Template Settings Tab (Admin Only) */}
|
||||||
|
<TabsContent value="templates" className="mt-0">
|
||||||
|
{!showActivityTypeManager ? (
|
||||||
|
<Card className="shadow-lg border-0 rounded-md">
|
||||||
|
<CardHeader className="border-b border-slate-100 py-4 sm:py-5">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="p-2.5 bg-gradient-to-br from-slate-600 to-slate-700 rounded-md shadow-md">
|
||||||
|
<FileText className="w-5 h-5 text-white" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<CardTitle className="text-lg sm:text-xl font-semibold text-slate-900">Template Settings</CardTitle>
|
||||||
|
<CardDescription className="text-sm">
|
||||||
|
Manage templates and activity types for workflows
|
||||||
|
</CardDescription>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="pt-6">
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Dealer Claim Activity Settings Card */}
|
||||||
|
<Card
|
||||||
|
className="shadow-md hover:shadow-lg transition-all duration-300 border border-slate-200 rounded-lg cursor-pointer group"
|
||||||
|
onClick={() => setShowActivityTypeManager(true)}
|
||||||
|
>
|
||||||
|
<CardContent className="p-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="p-3 bg-gradient-to-br from-blue-500 to-blue-600 rounded-lg shadow-md group-hover:shadow-lg transition-shadow">
|
||||||
|
<FileText className="w-6 h-6 text-white" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold text-slate-900 group-hover:text-blue-600 transition-colors">
|
||||||
|
Dealer Claim Activity Settings
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-slate-600 mt-1">
|
||||||
|
Manage activity types for dealer claim workflows
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-slate-400 group-hover:text-blue-600 transition-colors">
|
||||||
|
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<Card className="shadow-lg border-0 rounded-md">
|
||||||
|
<CardHeader className="border-b border-slate-100 py-4 sm:py-5">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setShowActivityTypeManager(false)}
|
||||||
|
className="gap-2 hover:bg-slate-100"
|
||||||
|
>
|
||||||
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
||||||
|
</svg>
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
<div className="p-2.5 bg-gradient-to-br from-slate-600 to-slate-700 rounded-md shadow-md">
|
||||||
|
<FileText className="w-5 h-5 text-white" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<CardTitle className="text-lg sm:text-xl font-semibold text-slate-900">Dealer Claim Activity Settings</CardTitle>
|
||||||
|
<CardDescription className="text-sm">
|
||||||
|
Manage activity types for dealer claim workflows
|
||||||
|
</CardDescription>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
</Card>
|
||||||
|
<ActivityTypeManager />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</TabsContent>
|
||||||
</div>
|
</div>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@ -38,6 +38,15 @@ export interface Holiday {
|
|||||||
appliesToLocations?: string[];
|
appliesToLocations?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ActivityType {
|
||||||
|
activityTypeId: string;
|
||||||
|
title: string;
|
||||||
|
itemCode?: string | null;
|
||||||
|
taxationType?: string | null;
|
||||||
|
sapRefNo?: string | null;
|
||||||
|
isActive?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get public configurations (accessible to all authenticated users)
|
* Get public configurations (accessible to all authenticated users)
|
||||||
* Returns non-sensitive configurations like DOCUMENT_POLICY, WORKFLOW_SHARING, TAT_SETTINGS
|
* Returns non-sensitive configurations like DOCUMENT_POLICY, WORKFLOW_SHARING, TAT_SETTINGS
|
||||||
@ -125,3 +134,54 @@ export const bulkImportHolidays = async (holidays: Partial<Holiday>[]): Promise<
|
|||||||
return response.data;
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all activity types (public endpoint - no auth required)
|
||||||
|
*/
|
||||||
|
export const getActivityTypes = async (): Promise<ActivityType[]> => {
|
||||||
|
const response = await apiClient.get('/config/activity-types');
|
||||||
|
return response.data.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all activity types (admin only - includes inactive)
|
||||||
|
*/
|
||||||
|
export const getAllActivityTypes = async (activeOnly?: boolean): Promise<ActivityType[]> => {
|
||||||
|
const params = activeOnly !== undefined ? { activeOnly: activeOnly.toString() } : {};
|
||||||
|
const response = await apiClient.get('/admin/activity-types', { params });
|
||||||
|
return response.data.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a single activity type by ID (admin only)
|
||||||
|
*/
|
||||||
|
export const getActivityTypeById = async (activityTypeId: string): Promise<ActivityType> => {
|
||||||
|
const response = await apiClient.get(`/admin/activity-types/${activityTypeId}`);
|
||||||
|
return response.data.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new activity type (admin only)
|
||||||
|
*/
|
||||||
|
export const createActivityType = async (activityType: Partial<ActivityType>): Promise<ActivityType> => {
|
||||||
|
const response = await apiClient.post('/admin/activity-types', activityType);
|
||||||
|
return response.data.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update an activity type (admin only)
|
||||||
|
*/
|
||||||
|
export const updateActivityType = async (
|
||||||
|
activityTypeId: string,
|
||||||
|
updates: Partial<ActivityType>
|
||||||
|
): Promise<ActivityType> => {
|
||||||
|
const response = await apiClient.put(`/admin/activity-types/${activityTypeId}`, updates);
|
||||||
|
return response.data.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete (deactivate) an activity type (admin only)
|
||||||
|
*/
|
||||||
|
export const deleteActivityType = async (activityTypeId: string): Promise<void> => {
|
||||||
|
await apiClient.delete(`/admin/activity-types/${activityTypeId}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user