documents type added fdd and finance team detailscreen made to navigate tocommon application detail screen
This commit is contained in:
parent
c37ca50d4c
commit
1578a1708e
@ -15,7 +15,6 @@ import { FinanceDashboard } from './components/dashboard/FinanceDashboard';
|
||||
import { DealerDashboard } from './components/dashboard/DealerDashboard';
|
||||
import { ProspectiveDashboardPage } from './components/dashboard/ProspectiveDashboardPage';
|
||||
import { FDDDashboardPage } from './components/dashboard/FDDDashboardPage';
|
||||
import { FDDApplicationDetails } from './components/applications/FDDApplicationDetails';
|
||||
import { ApplicationsPage } from './components/applications/ApplicationsPage';
|
||||
import { AllApplicationsPage } from './components/applications/AllApplicationsPage';
|
||||
import { OpportunityRequestsPage } from './components/applications/OpportunityRequestsPage';
|
||||
@ -30,7 +29,7 @@ import { FnFDetails } from './components/applications/FnFDetails';
|
||||
import { FinanceOnboardingPage } from './components/applications/FinanceOnboardingPage';
|
||||
import { FinanceFnFPage } from './components/applications/FinanceFnFPage';
|
||||
import { FinancePaymentDetailsPage } from './components/applications/FinancePaymentDetailsPage';
|
||||
import { FinanceFddDetailPage } from './components/applications/FinanceFddDetailPage';
|
||||
|
||||
import { FinanceFnFDetailsPage } from './components/applications/FinanceFnFDetailsPage';
|
||||
import { MasterPage } from './components/applications/MasterPage';
|
||||
import { UserManagementPage } from './components/admin/UserManagementPage';
|
||||
@ -229,7 +228,6 @@ export default function App() {
|
||||
|
||||
{/* FDD Routes - Integrated into Layout */}
|
||||
<Route path="/fdd-dashboard" element={<FDDDashboardPage />} />
|
||||
<Route path="/fdd-dashboard/application/:id" element={<FDDApplicationDetails />} />
|
||||
|
||||
{/* Admin/Lead Routes */}
|
||||
<Route path="/opportunity-requests" element={<OpportunityRequestsPage onViewDetails={(id) => navigate(`/applications/${id}`)} />} />
|
||||
@ -260,7 +258,7 @@ export default function App() {
|
||||
|
||||
<Route path="/finance-onboarding" element={<FinanceOnboardingPage onViewPaymentDetails={(id) => navigate(`/finance-onboarding/${id}`)} onViewAuditDetails={(id) => navigate(`/finance-audit/${id}`)} />} />
|
||||
<Route path="/finance-onboarding/:id" element={<FinancePaymentDetailsPage applicationId={window.location.pathname.split('/').pop() || ''} onBack={() => navigate('/finance-onboarding')} />} />
|
||||
<Route path="/finance-audit/:id" element={<FinanceFddDetailPage applicationId={window.location.pathname.split('/').pop() || ''} onBack={() => navigate('/finance-onboarding')} />} />
|
||||
<Route path="/finance-audit/:id" element={<ApplicationDetails />} />
|
||||
|
||||
<Route path="/finance-fnf" element={<FinanceFnFPage onViewFnFDetails={(id) => navigate(`/finance-fnf/${id}`)} />} />
|
||||
<Route path="/finance-fnf/:id" element={<FinanceFnFDetailsPage fnfId={window.location.pathname.split('/').pop() || ''} onBack={() => navigate('/finance-fnf')} />} />
|
||||
|
||||
@ -38,6 +38,7 @@ export const API = {
|
||||
getApplications: () => client.get('/onboarding/applications'),
|
||||
shortlistApplications: (data: any) => client.post('/onboarding/applications/shortlist', data),
|
||||
getApplicationById: (id: string) => client.get(`/onboarding/applications/${id}`),
|
||||
updateApplication: (id: string, data: any) => client.put(`/onboarding/applications/${id}`, data),
|
||||
getLatestQuestionnaire: () => client.get('/questionnaire/latest'),
|
||||
createQuestionnaireVersion: (data: any) => client.post('/questionnaire/version', data),
|
||||
submitQuestionnaireResponse: (data: any) => client.post('/questionnaire/response', data),
|
||||
@ -56,6 +57,11 @@ export const API = {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
}),
|
||||
getDocuments: (id: string) => client.get(`/onboarding/applications/${id}/documents`),
|
||||
getDocumentConfigMetadata: () => client.get('/onboarding/document-configs/metadata'),
|
||||
getDocumentConfigs: (params?: any) => client.get('/onboarding/document-configs', params),
|
||||
createDocumentConfig: (data: any) => client.post('/onboarding/document-configs', data),
|
||||
updateDocumentConfig: (id: string, data: any) => client.put(`/onboarding/document-configs/${id}`, data),
|
||||
deleteDocumentConfig: (id: string) => client.delete(`/onboarding/document-configs/${id}`),
|
||||
|
||||
// Public Questionnaire
|
||||
getPublicQuestionnaire: (appId: string) => axios.get(`http://localhost:5000/api/questionnaire/public/${appId}`), // Direct axios to bypass interceptors if client has auth
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -3,7 +3,7 @@ import { useSelector } from 'react-redux';
|
||||
import {
|
||||
Tabs, TabsContent, TabsList, TabsTrigger
|
||||
} from '../ui/tabs';
|
||||
import { Globe, Shield, Clock, Mail, MapPin, SlidersHorizontal, Settings } from 'lucide-react';
|
||||
import { Globe, Shield, Clock, Mail, MapPin, SlidersHorizontal, Settings, FileText } from 'lucide-react';
|
||||
import { Badge } from '../ui/badge';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
@ -32,6 +32,7 @@ import { RegionDialog } from './MasterPage/RegionDialog';
|
||||
import { TemplateDialog } from './MasterPage/TemplateDialog';
|
||||
import { LocationDialog } from './MasterPage/LocationDialog';
|
||||
import { SecurityDepositMaster } from './MasterPage/SecurityDepositMaster';
|
||||
import { DocumentConfigManagement } from './MasterPage/DocumentConfigManagement';
|
||||
import { ApprovalPoliciesPage } from '../admin/ApprovalPoliciesPage';
|
||||
import { RootState } from '../../store';
|
||||
|
||||
@ -441,7 +442,7 @@ export const MasterPage: React.FC = () => {
|
||||
</div>
|
||||
) : (
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-6">
|
||||
<TabsList className="grid w-full grid-cols-6 h-auto sticky top-0 z-10 bg-white/80 backdrop-blur-sm border shadow-sm rounded-xl p-1">
|
||||
<TabsList className="grid w-full grid-cols-7 h-auto sticky top-0 z-10 bg-white/80 backdrop-blur-sm border shadow-sm rounded-xl p-1">
|
||||
<TabsTrigger value="hierarchy" className="flex items-center gap-2 py-3 rounded-lg data-[state=active]:bg-amber-600 data-[state=active]:text-white">
|
||||
<Globe className="w-4 h-4" /> Organisation
|
||||
</TabsTrigger>
|
||||
@ -460,6 +461,9 @@ export const MasterPage: React.FC = () => {
|
||||
<TabsTrigger value="approvals" className="flex items-center gap-2 py-3 rounded-lg data-[state=active]:bg-amber-600 data-[state=active]:text-white transition-all transform hover:scale-[1.02]">
|
||||
<SlidersHorizontal className="w-4 h-4" /> Approvals
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="documents" className="flex items-center gap-2 py-3 rounded-lg data-[state=active]:bg-amber-600 data-[state=active]:text-white transition-all transform hover:scale-[1.02]">
|
||||
<FileText className="w-4 h-4" /> Docs Config
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="settings" className="flex items-center gap-2 py-3 rounded-lg data-[state=active]:bg-amber-600 data-[state=active]:text-white transition-all transform hover:scale-[1.02]">
|
||||
<Settings className="w-4 h-4" /> App Settings
|
||||
</TabsTrigger>
|
||||
@ -576,6 +580,10 @@ export const MasterPage: React.FC = () => {
|
||||
<ApprovalPoliciesPage />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="documents" className="animate-in fade-in duration-300">
|
||||
<DocumentConfigManagement />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="settings" className="animate-in fade-in duration-300">
|
||||
<SecurityDepositMaster />
|
||||
</TabsContent>
|
||||
|
||||
@ -0,0 +1,497 @@
|
||||
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '../../ui/card';
|
||||
import { Button } from '../../ui/button';
|
||||
import { Input } from '../../ui/input';
|
||||
import { Label } from '../../ui/label';
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../../ui/table';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '../../ui/dialog';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../../ui/select';
|
||||
import { Checkbox } from '../../ui/checkbox';
|
||||
import { Badge } from '../../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-amber-600 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-amber-100 overflow-hidden">
|
||||
<div className="h-full bg-amber-600 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-amber-600">
|
||||
<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-amber-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-amber-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-amber-600 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-amber-700 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-red-600 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-red-600 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-amber-700 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-amber-600">{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-amber-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-amber-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-amber-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-amber-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-amber-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-amber-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-amber-600" /> 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-amber-50 border-amber-300 shadow-sm' : 'bg-white border-slate-200 hover:border-amber-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-amber-600 data-[state=checked]:border-amber-600 rounded"
|
||||
/>
|
||||
<Label htmlFor={`role-${role}`} className={`text-[10px] font-black cursor-pointer uppercase truncate ${formData.allowedRoles.includes(role) ? 'text-amber-800' : 'text-slate-500 group-hover:text-amber-700'}`}>{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-red-600 data-[state=checked]:border-red-600 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>
|
||||
);
|
||||
};
|
||||
@ -162,7 +162,7 @@ export function FDDDashboardPage() {
|
||||
<tr
|
||||
key={app.id}
|
||||
className="hover:bg-slate-50 transition-colors cursor-pointer group"
|
||||
onClick={() => navigate(`/fdd-dashboard/application/${app.id}`)}
|
||||
onClick={() => navigate(`/applications/${app.id}`)}
|
||||
>
|
||||
<td className="px-6 py-4">
|
||||
<div className="flex items-center gap-3">
|
||||
|
||||
@ -82,6 +82,7 @@ export interface Application {
|
||||
address?: string;
|
||||
pincode?: string;
|
||||
locationType?: string;
|
||||
constitutionType?: string;
|
||||
pastExperience: string;
|
||||
status: ApplicationStatus;
|
||||
questionnaireMarks?: number;
|
||||
|
||||
@ -129,5 +129,36 @@ export const onboardingService = {
|
||||
const response: any = await API.getSystemConfigs(params);
|
||||
if (!response.ok) throw new Error(response.data?.message || 'Failed to fetch system configurations');
|
||||
return response.data?.data || response.data;
|
||||
},
|
||||
getDocumentConfigMetadata: async () => {
|
||||
const response: any = await API.getDocumentConfigMetadata();
|
||||
if (!response.ok) throw new Error(response.data?.message || 'Failed to fetch metadata');
|
||||
return response.data?.data || response.data;
|
||||
},
|
||||
getDocumentConfigs: async (params?: any) => {
|
||||
const response: any = await API.getDocumentConfigs(params);
|
||||
if (!response.ok) throw new Error(response.data?.message || 'Failed to fetch document configurations');
|
||||
// Return full response so DocumentConfigManagement can access pagination
|
||||
return response.data;
|
||||
},
|
||||
createDocumentConfig: async (data: any) => {
|
||||
const response: any = await API.createDocumentConfig(data);
|
||||
if (!response.ok) throw new Error(response.data?.message || 'Failed to create document configuration');
|
||||
return response.data?.data || response.data;
|
||||
},
|
||||
updateDocumentConfig: async (id: string, data: any) => {
|
||||
const response: any = await API.updateDocumentConfig(id, data);
|
||||
if (!response.ok) throw new Error(response.data?.message || 'Failed to update document configuration');
|
||||
return response.data?.data || response.data;
|
||||
},
|
||||
deleteDocumentConfig: async (id: string) => {
|
||||
const response: any = await API.deleteDocumentConfig(id);
|
||||
if (!response.ok) throw new Error(response.data?.message || 'Failed to delete document configuration');
|
||||
return response.data;
|
||||
},
|
||||
updateApplication: async (id: string, data: any) => {
|
||||
const response: any = await API.updateApplication(id, data);
|
||||
if (!response.ok) throw new Error(response.data?.message || 'Failed to update application');
|
||||
return response.data;
|
||||
}
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user