Dealer_Onboard_Frontend/src/features/master/components/DocumentConfigManagement.tsx

499 lines
29 KiB
TypeScript

import React, { useState, useEffect, useCallback, useRef } from 'react';
import { Card, CardContent, 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 { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Checkbox } from '@/components/ui/checkbox';
import { Badge } from '@/components/ui/badge';
import { Plus, Edit2, Trash2, ShieldCheck, Layers, Settings2, Search, ChevronLeft, ChevronRight, Loader2, Database } from 'lucide-react';
import { onboardingService } from '@/services/onboarding.service';
import { toast } from 'sonner';
export const DocumentConfigManagement: React.FC = () => {
const [configs, setConfigs] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [backgroundLoading, setBackgroundLoading] = useState(false);
const [showDialog, setShowDialog] = useState(false);
const [editingConfig, setEditingConfig] = useState<any>(null);
// Metadata from backend
const [modules, setModules] = useState<string[]>([]);
const [stagesMap, setStagesMap] = useState<Record<string, string[]>>({});
const [metadataLoading, setMetadataLoading] = useState(true);
// Pagination, Search & Module filter state
const [search, setSearch] = useState('');
const [page, setPage] = useState(1);
const [limit] = useState(10);
const [selectedModule, setSelectedModule] = useState('');
const [pagination, setPagination] = useState({ total: 0, pages: 1 });
const isFirstLoad = useRef(true);
const [formData, setFormData] = useState({
documentType: '',
stageCode: 'General',
allowedRoles: [] as string[],
isMandatory: false,
isActive: true,
module: ''
});
const ROLE_LIST = [
'DD-ZM', 'RBM', 'DD', 'ZBH', 'DD Lead', 'DD Head', 'NBH', 'DD Admin',
'Legal Admin', 'Super Admin', 'DD AM', 'FDD', 'DDL', 'Finance',
'Finance Admin', 'Dealer', 'ARCHITECTURE'
];
// Fetch Metadata (Modules & Stages) from Backend
useEffect(() => {
const fetchMetadata = async () => {
try {
const res = await onboardingService.getDocumentConfigMetadata();
if (res) {
setModules(res.modules || []);
setStagesMap(res.stages || {});
if (res.modules?.length > 0) {
setSelectedModule(res.modules[0]);
setFormData(prev => ({ ...prev, module: res.modules[0] }));
}
}
} catch (error) {
toast.error('Failed to load system metadata');
} finally {
setMetadataLoading(false);
}
};
fetchMetadata();
}, []);
const fetchConfigs = useCallback(async () => {
if (!selectedModule) return;
if (isFirstLoad.current) {
setLoading(true);
} else {
setBackgroundLoading(true);
}
try {
const res: any = await onboardingService.getDocumentConfigs({
search,
page,
limit,
module: selectedModule,
isAdminView: true
});
if (res && res.pagination) {
setConfigs(res.data || []);
setPagination(res.pagination);
} else if (Array.isArray(res)) {
setConfigs(res);
setPagination({ total: res.length, pages: 1 });
}
} catch (error) {
console.error('Fetch Configs Error:', error);
if (!isFirstLoad.current) {
toast.error('Failed to sync configuration database');
}
} finally {
setLoading(false);
setBackgroundLoading(false);
isFirstLoad.current = false;
}
}, [search, page, limit, selectedModule]);
useEffect(() => {
const handler = setTimeout(() => {
fetchConfigs();
}, 300);
return () => clearTimeout(handler);
}, [fetchConfigs]);
const handleSave = async () => {
try {
if (editingConfig) {
await onboardingService.updateDocumentConfig(editingConfig.id, formData);
toast.success('Configuration updated');
} else {
await onboardingService.createDocumentConfig(formData);
toast.success('Configuration created');
}
setShowDialog(false);
fetchConfigs();
} catch (error) {
toast.error('Failed to save configuration');
}
};
const handleDelete = async (id: string) => {
if (!window.confirm('Are you sure you want to delete this configuration?')) return;
try {
await onboardingService.deleteDocumentConfig(id);
toast.success('Configuration deleted');
fetchConfigs();
} catch (error) {
toast.error('Failed to delete configuration');
}
};
const openCreate = () => {
setEditingConfig(null);
setFormData({
documentType: '',
stageCode: 'General',
allowedRoles: [],
isMandatory: false,
isActive: true,
module: selectedModule
});
setShowDialog(true);
};
const openEdit = (config: any) => {
setEditingConfig(config);
setFormData({
documentType: config.documentType,
stageCode: config.stageCode,
allowedRoles: config.allowedRoles || [],
isMandatory: config.isMandatory as boolean,
isActive: config.isActive as boolean,
module: config.module || 'ONBOARDING'
});
setShowDialog(true);
};
const toggleRole = (role: string) => {
setFormData(prev => ({
...prev,
allowedRoles: prev.allowedRoles.includes(role)
? prev.allowedRoles.filter(r => r !== role)
: [...prev.allowedRoles, role]
}));
};
if (metadataLoading) {
return (
<div className="h-96 flex flex-col items-center justify-center gap-4">
<Database className="w-10 h-10 text-re-red animate-bounce" />
<p className="text-slate-500 font-bold uppercase tracking-widest text-xs">Connecting to Governance Engine...</p>
</div>
);
}
return (
<Card className="border-slate-200 shadow-sm overflow-hidden bg-white">
<CardHeader className="bg-slate-50/80 border-b border-slate-200 py-4 relative">
{backgroundLoading && (
<div className="absolute top-0 left-0 right-0 h-1 bg-red-100 overflow-hidden">
<div className="h-full bg-re-red animate-[loading_1.5s_infinite_linear]" style={{width: '30%', transformOrigin: 'left'}} />
<style>{`
@keyframes loading {
0% { left: -30%; }
100% { left: 100%; }
}
`}</style>
</div>
)}
<div className="flex flex-row items-center justify-between mb-4">
<div className="flex items-center gap-3">
<div className="p-2 bg-white rounded-xl shadow-sm ring-1 ring-slate-200 text-re-red">
<Layers className="w-5 h-5" />
</div>
<div>
<CardTitle className="text-lg font-bold text-slate-800">Governance Matrix</CardTitle>
<p className="text-[11px] text-slate-500 font-bold uppercase tracking-wider">Baseline Document Rules (Synced from Backend)</p>
</div>
</div>
<Button onClick={openCreate} className="bg-slate-900 hover:bg-black text-white rounded-xl h-9 px-4 flex gap-2 font-bold transition-all active:scale-95 shadow-md uppercase text-[10px]">
<Plus className="w-4 h-4" /> Add Policy
</Button>
</div>
<div className="flex gap-4">
<div className="w-64">
<Select value={selectedModule} onValueChange={(val) => { setSelectedModule(val); setPage(1); }}>
<SelectTrigger className="h-10 rounded-xl bg-white border-slate-200 focus:ring-red-500 font-bold text-slate-700 shadow-sm">
<SelectValue placeholder="Target Module" />
</SelectTrigger>
<SelectContent className="rounded-xl shadow-2xl border-none">
{modules.map(m => (
<SelectItem key={m} value={m} className="font-bold text-xs py-2.5 uppercase">
{m.replace(/_/g, ' ')}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
<Input
placeholder="Search policies, stages or documents..."
value={search}
onChange={(e) => { setSearch(e.target.value); setPage(1); }}
className="pl-10 h-10 rounded-xl bg-white border-slate-200 focus:ring-red-500 shadow-sm font-medium"
/>
</div>
</div>
</CardHeader>
<CardContent className="p-0 min-h-[400px] relative">
{loading ? (
<div className="absolute inset-0 z-10 bg-white/60 backdrop-blur-[1px] flex flex-col items-center justify-center gap-3">
<Loader2 className="w-8 h-8 text-re-red animate-spin" />
<span className="text-slate-500 text-sm font-bold animate-pulse">Syncing Policies...</span>
</div>
) : null}
<Table>
<TableHeader className="bg-slate-50/50">
<TableRow className="border-none">
<TableHead className="font-bold text-slate-500 text-[11px] uppercase tracking-wider">Policy Detail</TableHead>
<TableHead className="font-bold text-slate-500 text-[11px] uppercase tracking-wider">Process Stage</TableHead>
<TableHead className="font-bold text-slate-500 text-[11px] uppercase tracking-wider">Module</TableHead>
<TableHead className="font-bold text-slate-500 text-[11px] uppercase tracking-wider">Stakeholders</TableHead>
<TableHead className="font-bold text-slate-500 text-[11px] uppercase tracking-wider">Compliance Rules</TableHead>
<TableHead className="text-right font-bold text-slate-500 text-[11px] uppercase tracking-wider">Action</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{!loading && configs.length === 0 ? (
<TableRow>
<TableCell colSpan={6} className="h-96 text-center text-slate-500">
<div className="flex flex-col items-center gap-2 opacity-40">
<Search className="w-12 h-12 mb-2" />
<p className="font-bold text-xl">No policies found for {selectedModule}</p>
<p className="text-sm font-medium">Try adjusting your filters or search term</p>
</div>
</TableCell>
</TableRow>
) : configs.map((config) => (
<TableRow key={config.id} className="hover:bg-slate-50/80 transition-colors group h-14">
<TableCell>
<div className="font-bold text-slate-900 group-hover:text-re-red-hover transition-colors uppercase text-[12px]">{config.documentType}</div>
</TableCell>
<TableCell>
<Badge variant="outline" className="bg-blue-50/50 border-blue-100 text-blue-700 font-bold px-2 py-0.5 whitespace-nowrap text-[10px] rounded-md uppercase">
{config.stageCode}
</Badge>
</TableCell>
<TableCell>
<div className="text-[10px] font-bold text-slate-400 uppercase tracking-tighter">{config.module?.replace(/_/g, ' ')}</div>
</TableCell>
<TableCell>
<div className="flex flex-wrap gap-1 max-w-[200px]">
{config.allowedRoles?.length > 0 ? (
<>
{config.allowedRoles.slice(0, 2).map((role: string) => (
<Badge key={role} variant="secondary" className="bg-white text-slate-600 text-[9px] border-slate-200 font-bold uppercase">
{role}
</Badge>
))}
{config.allowedRoles.length > 2 && (
<Badge variant="outline" className="text-[9px] text-slate-400 font-bold border-dashed">+{config.allowedRoles.length - 2}</Badge>
)}
</>
) : (
<Badge variant="secondary" className="bg-slate-50 text-slate-400 text-[9px] border-slate-100 uppercase">Inherited</Badge>
)}
</div>
</TableCell>
<TableCell>
<div className="flex gap-2">
{config.isMandatory && (
<Badge className="bg-re-red text-white border-transparent text-[10px] font-bold h-5 px-1.5 rounded-sm uppercase tracking-tighter">BLOCKING</Badge>
)}
{!config.isActive && (
<Badge className="bg-slate-200 text-slate-500 border-transparent text-[10px] h-5 px-1.5 rounded-sm uppercase tracking-tighter">DORMANT</Badge>
)}
{config.isActive && !config.isMandatory && (
<Badge className="bg-emerald-100 text-emerald-700 border-emerald-200 text-[10px] h-5 px-1.5 rounded-sm font-black uppercase tracking-tighter">OPTIONAL</Badge>
)}
</div>
</TableCell>
<TableCell className="text-right">
<div className="flex justify-end gap-1">
<Button variant="ghost" size="icon" onClick={() => openEdit(config)} className="h-8 w-8 text-slate-400 hover:text-blue-600 hover:bg-blue-50 rounded-lg transition-transform active:scale-90">
<Edit2 className="w-4 h-4" />
</Button>
<Button variant="ghost" size="icon" onClick={() => handleDelete(config.id)} className="h-8 w-8 text-slate-400 hover:text-re-red hover:bg-red-50 rounded-lg transition-transform active:scale-90">
<Trash2 className="w-4 h-4" />
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
{/* Pagination Controls */}
<div className="flex items-center justify-between px-6 py-4 bg-slate-50/50 border-t border-slate-200 mt-auto">
<div className="text-[11px] text-slate-500 font-bold uppercase tracking-tight">
Dataset Index <span className="text-slate-900 border-b border-slate-300 mx-1">{configs.length > 0 ? (page - 1) * limit + 1 : 0} - {Math.min(page * limit, pagination.total)}</span> Total Found <span className="text-re-red-hover font-extrabold ml-1">{pagination.total}</span>
</div>
<div className="flex gap-3 items-center">
<Button
variant="outline"
size="sm"
disabled={page === 1}
onClick={() => { setPage(p => Math.max(1, p - 1)); window.scrollTo({ top: 0, behavior: 'smooth' }); }}
className="h-9 px-3 rounded-xl border-slate-200 bg-white hover:bg-slate-50 transition-all font-bold shadow-sm disabled:opacity-30 uppercase text-[10px]"
>
<ChevronLeft className="w-4 h-4 mr-1 text-slate-600" /> Prev
</Button>
<div className="flex items-center px-4 h-9 bg-white border border-slate-200 rounded-xl text-xs font-extrabold text-slate-800 shadow-inner">
<span className="text-re-red">{page}</span> <span className="mx-2 text-slate-300">/</span> {pagination.pages}
</div>
<Button
variant="outline"
size="sm"
disabled={page >= pagination.pages}
onClick={() => { setPage(p => Math.min(pagination.pages, p + 1)); window.scrollTo({ top: 0, behavior: 'smooth' }); }}
className="h-9 px-3 rounded-xl border-slate-200 bg-white hover:bg-slate-50 transition-all font-bold shadow-sm disabled:opacity-30 uppercase text-[10px]"
>
Next <ChevronRight className="w-4 h-4 ml-1 text-slate-600" />
</Button>
</div>
</div>
</CardContent>
<Dialog open={showDialog} onOpenChange={setShowDialog}>
<DialogContent className="max-w-2xl rounded-2xl border-none shadow-2xl p-0 overflow-hidden ring-1 ring-black/5">
<DialogHeader className="bg-slate-900 text-white p-7">
<div className="flex items-center gap-4">
<div className="p-3 bg-white/10 rounded-2xl backdrop-blur-md ring-1 ring-white/20">
<Settings2 className="w-7 h-7 text-red-400" />
</div>
<div>
<DialogTitle className="text-2xl font-black tracking-tight uppercase">
{editingConfig ? 'Modify Policy' : 'Publish Rule'}
</DialogTitle>
<p className="text-xs text-slate-400 font-bold tracking-widest uppercase mt-1">Configuring {formData.module} Lifecycle</p>
</div>
</div>
</DialogHeader>
<div className="p-7 space-y-6 bg-white overflow-y-auto max-h-[70vh]">
<div className="grid grid-cols-2 gap-6">
<div className="space-y-2">
<Label className="text-slate-900 font-black px-1 text-[11px] uppercase tracking-wider">Process Stream</Label>
<Select
value={formData.module}
onValueChange={(val) => setFormData(prev => ({ ...prev, module: val, stageCode: 'General' }))}
>
<SelectTrigger className="h-12 rounded-xl border-slate-200 focus:ring-red-500 shadow-sm bg-slate-50 font-black text-xs uppercase">
<SelectValue placeholder="Module" />
</SelectTrigger>
<SelectContent className="rounded-xl border-none shadow-2xl">
{modules.map(m => (
<SelectItem key={m} value={m} className="py-3 px-4 rounded-lg focus:bg-red-50 font-black text-[10px] uppercase">
{m.replace(/_/g, ' ')}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label className="text-slate-900 font-black px-1 text-[11px] uppercase tracking-wider">Milestone Stage</Label>
<Select
value={formData.stageCode}
onValueChange={(val) => setFormData(prev => ({ ...prev, stageCode: val }))}
>
<SelectTrigger className="h-12 rounded-xl border-slate-200 focus:ring-red-500 shadow-sm bg-white font-black text-xs uppercase">
<SelectValue placeholder="Select Stage" />
</SelectTrigger>
<SelectContent className="rounded-xl border-none shadow-2xl">
{(stagesMap[formData.module] || ['General']).map(stage => (
<SelectItem key={stage} value={stage} className="py-3 px-4 rounded-lg focus:bg-red-50 font-black text-[10px] uppercase">{stage}</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div className="space-y-2">
<Label className="text-slate-900 font-black px-1 text-[11px] uppercase tracking-wider">Document Label Identifier</Label>
<Input
value={formData.documentType}
onChange={(e) => setFormData(prev => ({ ...prev, documentType: e.target.value }))}
placeholder="e.g., PAN Card, Blueprint"
className="h-12 rounded-xl border-slate-200 focus:ring-red-500 shadow-sm font-black text-sm uppercase placeholder:font-bold placeholder:text-slate-300"
/>
</div>
<div className="space-y-4 p-5 bg-slate-50 rounded-2xl border border-slate-100 shadow-inner">
<Label className="text-slate-900 font-black flex items-center gap-2 mb-2 text-[11px] uppercase tracking-wider">
<ShieldCheck className="w-4 h-4 text-re-red" /> Visibility Matrix
</Label>
<div className="grid grid-cols-3 gap-3">
{ROLE_LIST.map((role: string) => (
<div
key={role}
className={`flex items-center space-x-2 p-3 rounded-xl border transition-all cursor-pointer group active:scale-95 ${formData.allowedRoles.includes(role) ? 'bg-red-50 border-red-300 shadow-sm' : 'bg-white border-slate-200 hover:border-red-200 hover:shadow-sm'}`}
onClick={() => toggleRole(role)}
>
<Checkbox
id={`role-${role}`}
checked={formData.allowedRoles.includes(role)}
onCheckedChange={() => toggleRole(role)}
className="w-4 h-4 data-[state=checked]:bg-re-red data-[state=checked]:border-re-red rounded"
/>
<Label htmlFor={`role-${role}`} className={`text-[10px] font-black cursor-pointer uppercase truncate ${formData.allowedRoles.includes(role) ? 'text-red-800' : 'text-slate-500 group-hover:text-re-red-hover'}`}>{role}</Label>
</div>
))}
</div>
</div>
<div className="flex gap-6 mt-4">
<div
className={`flex items-center space-x-4 p-4 rounded-2xl border transition-all cursor-pointer flex-1 group ${formData.isMandatory ? 'bg-red-50 border-red-200 shadow-sm' : 'bg-slate-50 border-slate-100 hover:bg-red-50/30'}`}
onClick={() => setFormData(prev => ({ ...prev, isMandatory: !prev.isMandatory }))}
>
<Checkbox
id="mandatory"
checked={formData.isMandatory}
onCheckedChange={(checked) => setFormData(prev => ({ ...prev, isMandatory: !!checked }))}
className="w-5 h-5 border-slate-300 data-[state=checked]:bg-re-red data-[state=checked]:border-re-red rounded-md"
/>
<div className="space-y-0.5">
<Label htmlFor="mandatory" className="text-xs font-black text-slate-800 cursor-pointer group-hover:text-red-900 transition-colors uppercase tracking-tight">Mandatory Policy</Label>
<p className="text-[10px] text-slate-400 font-bold uppercase tracking-tighter">Blocking Next Stage Action</p>
</div>
</div>
<div
className={`flex items-center space-x-4 p-4 rounded-2xl border transition-all cursor-pointer flex-1 group ${formData.isActive ? 'bg-emerald-50 border-emerald-200 shadow-sm' : 'bg-slate-50 border-slate-100 hover:bg-emerald-50/30'}`}
onClick={() => setFormData(prev => ({ ...prev, isActive: !prev.isActive }))}
>
<Checkbox
id="active"
checked={formData.isActive}
onCheckedChange={(checked) => setFormData(prev => ({ ...prev, isActive: !!checked }))}
className="w-5 h-5 border-slate-300 data-[state=checked]:bg-emerald-600 data-[state=checked]:border-emerald-600 rounded-md"
/>
<div className="space-y-0.5">
<Label htmlFor="active" className="text-xs font-black text-slate-800 cursor-pointer group-hover:text-emerald-900 transition-colors uppercase tracking-tight">Active Policy</Label>
<p className="text-[10px] text-slate-400 font-bold uppercase tracking-tighter">Visible To Active Streams</p>
</div>
</div>
</div>
</div>
<DialogFooter className="bg-slate-50 p-7 flex gap-4 border-t">
<Button variant="ghost" onClick={() => setShowDialog(false)} className="flex-1 h-12 rounded-xl font-black uppercase text-slate-400 hover:text-slate-600 hover:bg-slate-100 text-xs">
Discard
</Button>
<Button onClick={handleSave} className="flex-1 h-12 rounded-xl bg-slate-900 hover:bg-black text-white font-black text-xs uppercase shadow-xl transition-all active:scale-95">
{editingConfig ? 'Update Policy Database' : 'Publish New Policy'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</Card>
);
};