system log table added and feew bugs coverd from the tracker

This commit is contained in:
Laxman 2026-05-13 20:45:33 +05:30
parent 201bfa6a41
commit ec70f1d3f1
20 changed files with 1065 additions and 368 deletions

1
package-lock.json generated
View File

@ -12222,7 +12222,6 @@
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,

View File

@ -47,6 +47,7 @@ import { DealerRelocationPage } from '@/features/relocation/pages/DealerRelocati
import QuestionnaireBuilder from '@/components/admin/QuestionnaireBuilder';
import QuestionnaireList from '@/components/admin/QuestionnaireList';
import InterviewConfigManagement from '@/features/master/components/InterviewConfigManagement';
import { SystemLogsPage } from '@/features/admin/pages/SystemLogsPage';
import { WorkNotesPage } from '@/features/onboarding/pages/WorkNotesPage';
import { NotificationsPage } from '@/pages/NotificationsPage';
import { Toaster } from '@/components/ui/sonner';
@ -275,6 +276,11 @@ export default function App() {
? <InterviewConfigManagement />
: <Navigate to="/dashboard" />
} />
<Route path="/system-logs" element={
hasRole(['Super Admin'])
? <SystemLogsPage />
: <Navigate to="/dashboard" />
} />
{/* HR/Finance Modules (Simplified for brevity, following pattern) */}
<Route path="/resignation" element={

View File

@ -239,6 +239,21 @@ export const API = {
getFddAssignment: (applicationId: string) => client.get(`/fdd/${applicationId}`),
assignFddAgency: (data: any) => client.post('/fdd/assign', data),
flagNonResponsive: (data: any) => client.post('/flag', data),
// System Audit Logs (segregated `system_audit_logs` table — Super Admin only)
getSystemAuditLogs: (params?: {
module?: string;
entityType?: string;
entityId?: string;
action?: string;
userId?: string;
search?: string;
dateFrom?: string;
dateTo?: string;
page?: number;
limit?: number;
}) => client.get('/audit/system-logs', params),
getSystemAuditSummary: () => client.get('/audit/system-summary'),
};
export default API;

View File

@ -197,7 +197,7 @@ export function ApprovalPoliciesPage() {
<CardTitle className="text-lg font-semibold text-slate-800">Configured Stages</CardTitle>
</CardHeader>
<CardContent className="p-0">
<div className="overflow-x-auto">
<div className="overflow-x-auto custom-scrollbar-x-slim">
<Table>
<TableHeader className="bg-slate-50/50">
<TableRow>

View File

@ -294,14 +294,14 @@ const QuestionnaireBuilder: React.FC = () => {
onChange={(e) => updateQuestion(index, 'inputType', e.target.value as any)}
className="w-full border border-slate-300 p-2.5 rounded-lg focus:ring-2 focus:ring-red-500 outline-none bg-white"
>
<option value="text">Text Input</option>
<option value="text">One Liner</option>
<option value="email">Email Address</option>
<option value="textarea">Long Text (Textarea)</option>
<option value="textarea">Paragraph</option>
<option value="number">Numeric</option>
<option value="file">File Upload</option>
<option value="yesno">Yes / No</option>
<option value="select">Multiple Choice (Dropdown)</option>
<option value="radio">Multiple Choice (Radio)</option>
<option value="select">Options (Dropdown)</option>
<option value="radio">Options (Radio)</option>
</select>
</div>

View File

@ -14,7 +14,8 @@ import {
RefreshCcw,
MapPin,
ClipboardList,
ListChecks
ListChecks,
Activity
} from 'lucide-react';
import { useState, useRef, useCallback, useEffect } from 'react';
import ReactDOM from 'react-dom';
@ -115,6 +116,7 @@ export function Sidebar({ onLogout }: SidebarProps) {
menuItems.push({ id: 'users', label: 'User Management', icon: Users });
menuItems.push({ id: 'questionnaires', label: 'Questionnaire Templates', icon: ClipboardList });
menuItems.push({ id: 'interview-configs', label: 'Interview Configs', icon: ListChecks });
menuItems.push({ id: 'system-logs', label: 'System Logs', icon: Activity });
}
const handleSearch = (e: React.FormEvent) => {

View File

@ -42,14 +42,18 @@ function ScrollBar({
orientation === "vertical" &&
"h-full w-2.5 border-l border-l-transparent",
orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent",
"h-1 flex-col border-t border-t-transparent",
className,
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb
data-slot="scroll-area-thumb"
className="bg-border relative flex-1 rounded-full"
className={cn(
"relative flex-1 rounded-full",
orientation === "vertical" && "bg-border",
orientation === "horizontal" && "bg-slate-200/70"
)}
/>
</ScrollAreaPrimitive.ScrollAreaScrollbar>
);

View File

@ -8,7 +8,7 @@ function Table({ className, ...props }: React.ComponentProps<"table">) {
return (
<div
data-slot="table-container"
className="relative w-full overflow-x-auto"
className="relative w-full overflow-x-auto custom-scrollbar-x-slim"
>
<table
data-slot="table"

View File

@ -0,0 +1,612 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import {
Activity,
RefreshCw,
Search,
ChevronLeft,
ChevronRight,
Eye,
User as UserIcon,
Filter
} from 'lucide-react';
import { toast } from 'sonner';
import { API } from '@/api/API';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Input } from '@/components/ui/input';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue
} from '@/components/ui/select';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle
} from '@/components/ui/dialog';
type SystemAuditLog = {
id: string;
module: string;
moduleLabel: string;
action: string;
actionLabel: string;
entityType: string;
entityId: string | null;
entityLabel: string | null;
description: string;
actor: {
id: string | null;
name: string;
role: string | null;
email: string | null;
};
oldData: any;
newData: any;
metadata: any;
ipAddress: string | null;
userAgent: string | null;
timestamp: string;
};
type Summary = {
totalEntries: number;
byModule: { module: string; moduleLabel: string; total: number }[];
lastActivity: SystemAuditLog | null;
};
const MODULE_OPTIONS = [
{ value: '__all__', label: 'All Modules' },
{ value: 'QUESTIONNAIRE', label: 'Questionnaire' },
{ value: 'INTERVIEW_CONFIG', label: 'Interview Configuration' },
{ value: 'SYSTEM_CONFIG', label: 'System Configuration' },
{ value: 'SLA_CONFIG', label: 'SLA Configuration' },
{ value: 'EMAIL_TEMPLATE', label: 'Email Template' },
{ value: 'MASTER_HIERARCHY', label: 'Master Hierarchy' },
{ value: 'ROLE_ASSIGNMENT', label: 'Role Assignment' },
{ value: 'USER_ADMIN', label: 'User Administration' },
{ value: 'DEALER_MAPPING', label: 'Dealer Mapping' }
];
const ACTION_OPTIONS = [
{ value: '__all__', label: 'All Actions' },
{ value: 'CREATED', label: 'Created' },
{ value: 'UPDATED', label: 'Updated' },
{ value: 'DELETED', label: 'Deleted' },
{ value: 'ACTIVATED', label: 'Activated' },
{ value: 'DEACTIVATED', label: 'Deactivated' },
{ value: 'INITIALIZED', label: 'Initialized' },
{ value: 'SUBMITTED', label: 'Submitted' },
{ value: 'ASSIGNED', label: 'Assigned' },
{ value: 'UNASSIGNED', label: 'Unassigned' },
{ value: 'REORDERED', label: 'Reordered' }
];
const ACTION_BADGE_CLASS: Record<string, string> = {
CREATED: 'bg-emerald-100 text-emerald-700 border-emerald-200',
UPDATED: 'bg-blue-100 text-blue-700 border-blue-200',
DELETED: 'bg-rose-100 text-rose-700 border-rose-200',
ACTIVATED: 'bg-emerald-100 text-emerald-700 border-emerald-200',
DEACTIVATED: 'bg-slate-200 text-slate-700 border-slate-300',
INITIALIZED: 'bg-amber-100 text-amber-700 border-amber-200',
SUBMITTED: 'bg-indigo-100 text-indigo-700 border-indigo-200',
ASSIGNED: 'bg-violet-100 text-violet-700 border-violet-200',
UNASSIGNED: 'bg-slate-200 text-slate-700 border-slate-300',
REORDERED: 'bg-sky-100 text-sky-700 border-sky-200'
};
const formatTimestamp = (ts: string) => {
try {
const d = new Date(ts);
return d.toLocaleString('en-IN', {
day: '2-digit',
month: 'short',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
} catch {
return ts;
}
};
const JsonBlock = ({ value }: { value: any }) => {
if (value === null || value === undefined) {
return <p className="text-xs italic text-slate-400"></p>;
}
return (
<pre className="text-xs bg-slate-900 text-slate-100 rounded-lg p-3 overflow-auto max-h-72 whitespace-pre-wrap break-words">
{JSON.stringify(value, null, 2)}
</pre>
);
};
export const SystemLogsPage: React.FC = () => {
const [logs, setLogs] = useState<SystemAuditLog[]>([]);
const [summary, setSummary] = useState<Summary | null>(null);
const [loading, setLoading] = useState(false);
const [page, setPage] = useState(1);
const [limit] = useState(25);
const [totalPages, setTotalPages] = useState(1);
const [totalEntries, setTotalEntries] = useState(0);
const [moduleFilter, setModuleFilter] = useState<string>('__all__');
const [actionFilter, setActionFilter] = useState<string>('__all__');
const [search, setSearch] = useState('');
const [dateFrom, setDateFrom] = useState('');
const [dateTo, setDateTo] = useState('');
const [appliedSearch, setAppliedSearch] = useState('');
const [selected, setSelected] = useState<SystemAuditLog | null>(null);
const queryParams = useMemo(
() => ({
module: moduleFilter !== '__all__' ? moduleFilter : undefined,
action: actionFilter !== '__all__' ? actionFilter : undefined,
search: appliedSearch || undefined,
dateFrom: dateFrom || undefined,
dateTo: dateTo || undefined,
page,
limit
}),
[moduleFilter, actionFilter, appliedSearch, dateFrom, dateTo, page, limit]
);
const fetchLogs = useCallback(async () => {
setLoading(true);
try {
const res = (await API.getSystemAuditLogs(queryParams)) as any;
const body = res?.data;
if (res?.ok && body?.success) {
setLogs(body.data || []);
setTotalPages(body.pagination?.totalPages || 1);
setTotalEntries(body.pagination?.total || 0);
} else {
toast.error(body?.message || 'Unable to load system logs');
setLogs([]);
}
} catch (err) {
console.error('[SystemLogsPage] fetchLogs error:', err);
toast.error('Failed to load system logs');
setLogs([]);
} finally {
setLoading(false);
}
}, [queryParams]);
const fetchSummary = useCallback(async () => {
try {
const res = (await API.getSystemAuditSummary()) as any;
const body = res?.data;
if (res?.ok && body?.success) {
setSummary(body.data || null);
}
} catch (err) {
console.error('[SystemLogsPage] fetchSummary error:', err);
}
}, []);
useEffect(() => {
fetchLogs();
}, [fetchLogs]);
useEffect(() => {
fetchSummary();
}, [fetchSummary]);
const applySearch = () => {
setPage(1);
setAppliedSearch(search.trim());
};
const clearFilters = () => {
setModuleFilter('__all__');
setActionFilter('__all__');
setSearch('');
setAppliedSearch('');
setDateFrom('');
setDateTo('');
setPage(1);
};
return (
<div className="space-y-6 max-w-7xl mx-auto">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
<div>
<h1 className="text-2xl font-bold text-slate-900 flex items-center gap-2">
<Activity className="w-6 h-6 text-re-red" />
System Activity Logs
</h1>
<p className="text-slate-500">
Configuration-level changes across questionnaires, interview configs, master hierarchy, system / SLA configs, and role assignments.
</p>
</div>
<Button
variant="outline"
onClick={() => {
fetchLogs();
fetchSummary();
}}
disabled={loading}
>
<RefreshCw className={`w-4 h-4 mr-2 ${loading ? 'animate-spin' : ''}`} />
Refresh
</Button>
</div>
{/* Summary */}
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-5 gap-3">
<Card className="border-slate-200">
<CardHeader className="pb-2">
<CardTitle className="text-xs font-bold text-slate-500 uppercase tracking-wider">
Total Events
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-2xl font-bold text-slate-900">{summary?.totalEntries ?? '—'}</p>
<p className="text-[11px] text-slate-400 mt-1">Lifetime</p>
</CardContent>
</Card>
{(summary?.byModule || []).slice(0, 4).map((row) => (
<Card key={row.module} className="border-slate-200">
<CardHeader className="pb-2">
<CardTitle className="text-xs font-bold text-slate-500 uppercase tracking-wider">
{row.moduleLabel}
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-2xl font-bold text-slate-900">{row.total}</p>
<button
onClick={() => {
setModuleFilter(row.module);
setPage(1);
}}
className="text-[11px] text-re-red hover:underline mt-1"
>
View module
</button>
</CardContent>
</Card>
))}
</div>
{/* Filters */}
<Card className="border-slate-200">
<CardContent className="pt-6">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-6 gap-3 items-end">
<div className="lg:col-span-2">
<label className="text-xs font-semibold text-slate-600 mb-1 block flex items-center gap-1">
<Search className="w-3 h-3" /> Search
</label>
<div className="flex gap-2">
<Input
placeholder="Entity name, description, or actor…"
value={search}
onChange={(e) => setSearch(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') applySearch();
}}
/>
<Button variant="outline" onClick={applySearch}>
Go
</Button>
</div>
</div>
<div>
<label className="text-xs font-semibold text-slate-600 mb-1 block">Module</label>
<Select
value={moduleFilter}
onValueChange={(v) => {
setModuleFilter(v);
setPage(1);
}}
>
<SelectTrigger>
<SelectValue placeholder="All Modules" />
</SelectTrigger>
<SelectContent>
{MODULE_OPTIONS.map((m) => (
<SelectItem key={m.value} value={m.value}>
{m.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<label className="text-xs font-semibold text-slate-600 mb-1 block">Action</label>
<Select
value={actionFilter}
onValueChange={(v) => {
setActionFilter(v);
setPage(1);
}}
>
<SelectTrigger>
<SelectValue placeholder="All Actions" />
</SelectTrigger>
<SelectContent>
{ACTION_OPTIONS.map((a) => (
<SelectItem key={a.value} value={a.value}>
{a.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<label className="text-xs font-semibold text-slate-600 mb-1 block">From</label>
<Input
type="date"
value={dateFrom}
onChange={(e) => {
setDateFrom(e.target.value);
setPage(1);
}}
/>
</div>
<div>
<label className="text-xs font-semibold text-slate-600 mb-1 block">To</label>
<Input
type="date"
value={dateTo}
onChange={(e) => {
setDateTo(e.target.value);
setPage(1);
}}
/>
</div>
</div>
<div className="flex items-center justify-between mt-4">
<div className="text-xs text-slate-500 flex items-center gap-2">
<Filter className="w-3 h-3" />
Showing {logs.length} of {totalEntries} matching event(s)
</div>
<Button variant="ghost" size="sm" onClick={clearFilters}>
Reset filters
</Button>
</div>
</CardContent>
</Card>
{/* Logs table */}
<Card className="border-slate-200 overflow-hidden">
<div className="overflow-x-auto custom-scrollbar-x-slim">
<table className="w-full text-sm">
<thead className="bg-slate-50 text-slate-600">
<tr className="text-left">
<th className="px-4 py-3 font-semibold">When</th>
<th className="px-4 py-3 font-semibold">Module</th>
<th className="px-4 py-3 font-semibold">Action</th>
<th className="px-4 py-3 font-semibold">Entity</th>
<th className="px-4 py-3 font-semibold">Actor</th>
<th className="px-4 py-3 font-semibold">Description</th>
<th className="px-4 py-3 font-semibold w-10"></th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{loading && (
<tr>
<td colSpan={7} className="px-4 py-10 text-center text-slate-400">
<RefreshCw className="w-5 h-5 inline-block mr-2 animate-spin" />
Loading system logs
</td>
</tr>
)}
{!loading && logs.length === 0 && (
<tr>
<td colSpan={7} className="px-4 py-10 text-center text-slate-400">
No events match the current filters.
</td>
</tr>
)}
{!loading &&
logs.map((log) => (
<tr
key={log.id}
className="hover:bg-slate-50/70 cursor-pointer"
onClick={() => setSelected(log)}
>
<td className="px-4 py-3 whitespace-nowrap text-slate-700">
{formatTimestamp(log.timestamp)}
</td>
<td className="px-4 py-3 whitespace-nowrap">
<Badge variant="outline" className="border-slate-300 text-slate-700">
{log.moduleLabel}
</Badge>
</td>
<td className="px-4 py-3 whitespace-nowrap">
<Badge
variant="outline"
className={
ACTION_BADGE_CLASS[log.action] ||
'bg-slate-100 text-slate-700 border-slate-200'
}
>
{log.actionLabel}
</Badge>
</td>
<td className="px-4 py-3 text-slate-800">
<div className="font-medium truncate max-w-[260px]" title={log.entityLabel || ''}>
{log.entityLabel || `${log.entityType}`}
</div>
{log.entityId && (
<div className="text-[10px] font-mono text-slate-400 truncate max-w-[260px]">
{log.entityId}
</div>
)}
</td>
<td className="px-4 py-3 whitespace-nowrap">
<div className="flex items-center gap-2">
<div className="w-7 h-7 rounded-full bg-slate-100 text-slate-600 flex items-center justify-center text-xs font-semibold">
{(log.actor?.name || 'S').charAt(0).toUpperCase()}
</div>
<div>
<div className="text-slate-800 leading-tight">{log.actor?.name || 'System'}</div>
{log.actor?.role && (
<div className="text-[10px] text-slate-500 uppercase tracking-wider">
{log.actor.role}
</div>
)}
</div>
</div>
</td>
<td className="px-4 py-3 text-slate-600">
<div className="truncate max-w-[360px]" title={log.description}>
{log.description}
</div>
</td>
<td className="px-4 py-3">
<Button
size="icon"
variant="ghost"
className="h-8 w-8"
onClick={(e) => {
e.stopPropagation();
setSelected(log);
}}
title="View details"
>
<Eye className="w-4 h-4 text-slate-500" />
</Button>
</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Pagination */}
<div className="flex items-center justify-between px-4 py-3 border-t border-slate-100 bg-slate-50/50">
<div className="text-xs text-slate-500">
Page {page} of {totalPages}
</div>
<div className="flex items-center gap-2">
<Button
size="sm"
variant="outline"
disabled={loading || page <= 1}
onClick={() => setPage((p) => Math.max(1, p - 1))}
>
<ChevronLeft className="w-4 h-4 mr-1" />
Prev
</Button>
<Button
size="sm"
variant="outline"
disabled={loading || page >= totalPages}
onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
>
Next
<ChevronRight className="w-4 h-4 ml-1" />
</Button>
</div>
</div>
</Card>
{/* Detail dialog */}
<Dialog open={!!selected} onOpenChange={(open) => !open && setSelected(null)}>
<DialogContent className="max-w-3xl max-h-[85vh] overflow-y-auto">
{selected && (
<>
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Activity className="w-5 h-5 text-re-red" />
{selected.moduleLabel} · {selected.actionLabel}
</DialogTitle>
<DialogDescription className="text-slate-600">
{selected.description}
</DialogDescription>
</DialogHeader>
<div className="grid grid-cols-2 gap-4 text-sm mt-4">
<div>
<p className="text-xs font-bold text-slate-500 uppercase tracking-wider mb-1">
Entity
</p>
<p className="text-slate-800">{selected.entityLabel || selected.entityType}</p>
{selected.entityId && (
<p className="text-[11px] font-mono text-slate-400 mt-0.5">
{selected.entityId}
</p>
)}
</div>
<div>
<p className="text-xs font-bold text-slate-500 uppercase tracking-wider mb-1">
Actor
</p>
<p className="text-slate-800 flex items-center gap-2">
<UserIcon className="w-4 h-4 text-slate-400" />
{selected.actor?.name || 'System'}
</p>
{selected.actor?.role && (
<p className="text-[11px] text-slate-500 uppercase tracking-wider mt-0.5">
{selected.actor.role}
</p>
)}
{selected.actor?.email && (
<p className="text-[11px] text-slate-500 mt-0.5">
{selected.actor.email}
</p>
)}
</div>
<div>
<p className="text-xs font-bold text-slate-500 uppercase tracking-wider mb-1">
When
</p>
<p className="text-slate-800">{formatTimestamp(selected.timestamp)}</p>
</div>
<div>
<p className="text-xs font-bold text-slate-500 uppercase tracking-wider mb-1">
Source
</p>
<p className="text-slate-800">{selected.ipAddress || '—'}</p>
<p className="text-[11px] text-slate-500 truncate" title={selected.userAgent || ''}>
{selected.userAgent || ''}
</p>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mt-4">
<div>
<p className="text-xs font-bold text-slate-500 uppercase tracking-wider mb-2">
Previous values
</p>
<JsonBlock value={selected.oldData} />
</div>
<div>
<p className="text-xs font-bold text-slate-500 uppercase tracking-wider mb-2">
New values
</p>
<JsonBlock value={selected.newData} />
</div>
</div>
{selected.metadata && (
<div className="mt-4">
<p className="text-xs font-bold text-slate-500 uppercase tracking-wider mb-2">
Metadata
</p>
<JsonBlock value={selected.metadata} />
</div>
)}
</>
)}
</DialogContent>
</Dialog>
</div>
);
};
export default SystemLogsPage;

View File

@ -701,7 +701,7 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
<Card>
<Tabs value={activeMainTab} onValueChange={setActiveMainTab} className="w-full">
<CardHeader className="pb-4">
<div className="overflow-x-auto -mx-6 px-6">
<div className="overflow-x-auto custom-scrollbar-x-slim -mx-6 px-6">
<TabsList className="w-max min-w-full justify-start">
<TabsTrigger value="workflow">Workflow Progress</TabsTrigger>
<TabsTrigger value="documents">Documents</TabsTrigger>

View File

@ -147,7 +147,7 @@ export function FDDDashboardPage() {
</p>
</div>
) : (
<div className="overflow-x-auto">
<div className="overflow-x-auto custom-scrollbar-x-slim">
<table className="w-full text-left border-collapse">
<thead className="bg-slate-50 border-b border-slate-200">
<tr>

View File

@ -404,7 +404,7 @@ const InterviewConfigManagement: React.FC = () => {
</div>
{/* Items Area with Horizontal Guard */}
<div className="overflow-x-auto pb-4 -mx-1 px-1">
<div className="overflow-x-auto custom-scrollbar-x-slim pb-4 -mx-1 px-1">
<div className="min-w-[900px] border border-slate-100 rounded-xl">
<table className="w-full text-left border-collapse table-fixed">
<thead>
@ -449,9 +449,9 @@ const InterviewConfigManagement: React.FC = () => {
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="select">Selection</SelectItem>
<SelectItem value="text">Text</SelectItem>
<SelectItem value="textarea">Comment</SelectItem>
<SelectItem value="select">Options</SelectItem>
<SelectItem value="text">One Liner</SelectItem>
<SelectItem value="textarea">Paragraph</SelectItem>
<SelectItem value="number">Numeric</SelectItem>
</SelectContent>
</Select>
@ -535,7 +535,7 @@ const InterviewConfigManagement: React.FC = () => {
<div className="flex flex-col gap-3">
<div className="flex items-center justify-between border-b border-slate-200/50 pb-2 mb-2">
<p className="text-[10px] font-black text-slate-400 uppercase tracking-widest flex items-center gap-2">
<div className="w-1 h-3 bg-red-400 rounded-full" /> Selection Choices Profile
<div className="w-1 h-3 bg-red-400 rounded-full" /> Options
</p>
<Button variant="ghost" size="sm" className="h-7 px-3 text-[10px] font-bold uppercase text-slate-600 hover:bg-white border border-transparent hover:border-slate-200" onClick={() => addOption(index)}>
<Plus className="w-3 h-3 mr-1.5" /> Append Option

View File

@ -3,6 +3,7 @@ import {
Calendar,
CheckCircle,
ChevronDown,
ClipboardCheck,
Clock,
GitBranch,
Info,
@ -46,6 +47,14 @@ interface ApplicationDetailsSidebarProps {
currentUser: any;
handleGenerateDealerCodes: () => void;
onOpenAssignArchitectureModal: () => void;
onOpenAssignFdd: () => void;
showAssignFddModal: boolean;
setShowAssignFddModal: (value: boolean) => void;
fddAgencies: any[];
selectedAgencyId: string;
setSelectedAgencyId: (value: string) => void;
isAssigningAgency: boolean;
handleAssignAgency: () => void;
activeInterviewForUser: any;
hasSubmittedFeedback: boolean;
setSelectedInterviewForFeedback: (value: any) => void;
@ -80,6 +89,14 @@ export function ApplicationDetailsSidebar(props: ApplicationDetailsSidebarProps)
currentUser,
handleGenerateDealerCodes,
onOpenAssignArchitectureModal,
onOpenAssignFdd,
showAssignFddModal,
setShowAssignFddModal,
fddAgencies,
selectedAgencyId,
setSelectedAgencyId,
isAssigningAgency,
handleAssignAgency,
activeInterviewForUser,
hasSubmittedFeedback,
setSelectedInterviewForFeedback,
@ -287,6 +304,55 @@ export function ApplicationDetailsSidebar(props: ApplicationDetailsSidebarProps)
</>
)}
{isAdmin &&
['Level 3 Approved', 'Level 3 Recommended', 'FDD Verification', 'FDD In Progress'].includes(application.status) &&
(!application.fddAssignments || application.fddAssignments.length === 0) && (
<Dialog open={showAssignFddModal} onOpenChange={setShowAssignFddModal}>
<Button
variant="outline"
className="w-full border-purple-200 hover:bg-purple-50 text-purple-700"
onClick={onOpenAssignFdd}
data-testid="onboarding-details-assign-fdd"
>
<ClipboardCheck className="w-4 h-4 mr-2" />
Assign FDD
</Button>
<DialogContent data-testid="onboarding-details-assign-fdd-modal">
<DialogHeader>
<DialogTitle>Assign FDD Agency</DialogTitle>
<DialogDescription>
Select an FDD partner agency to perform the financial due diligence audit for this application.
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div>
<Label>FDD Agency</Label>
<Select value={selectedAgencyId} onValueChange={setSelectedAgencyId}>
<SelectTrigger className="mt-2" data-testid="onboarding-details-assign-fdd-select">
<SelectValue placeholder={fddAgencies?.length ? 'Choose partner agency...' : 'No agencies available'} />
</SelectTrigger>
<SelectContent>
{(fddAgencies || []).map((agency: any) => (
<SelectItem key={agency.id} value={agency.id}>
{agency.fullName || agency.name} ({agency.email})
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<Button
className="w-full bg-amber-600 hover:bg-amber-700 font-bold h-11"
onClick={handleAssignAgency}
disabled={isAssigningAgency || !selectedAgencyId}
data-testid="onboarding-details-assign-fdd-submit"
>
{isAssigningAgency ? 'Assigning...' : 'Assign Agency'}
</Button>
</div>
</DialogContent>
</Dialog>
)}
{activeInterviewForUser && !hasSubmittedFeedback && (
<DropdownMenu>
<DropdownMenuTrigger asChild>

View File

@ -126,7 +126,7 @@ export function ApplicationDetailsTabs(props: ApplicationDetailsTabsProps) {
<Card data-testid="onboarding-details-tabs-container">
<Tabs value={activeTab} onValueChange={setActiveTab}>
<CardHeader className="pb-4 px-4 sm:px-6">
<div className="overflow-x-auto custom-scrollbar-x -mx-4 px-4 sm:-mx-6 sm:px-6">
<div className="overflow-x-auto custom-scrollbar-x-slim -mx-4 px-4 sm:-mx-6 sm:px-6">
<TabsList className="w-max min-w-full justify-start h-11 bg-slate-100/80 p-1" data-testid="onboarding-tabs-list">
<TabsTrigger value="questionnaire" className="min-w-[120px]" data-testid="onboarding-tab-trigger-questionnaire">Questionnaire</TabsTrigger>
<TabsTrigger value="progress" className="min-w-[80px]" data-testid="onboarding-tab-trigger-progress">Progress</TabsTrigger>
@ -525,7 +525,7 @@ export function ApplicationDetailsTabs(props: ApplicationDetailsTabsProps) {
</Button>
</div>
<div className="overflow-x-auto">
<div className="overflow-x-auto custom-scrollbar-x-slim">
<Table data-testid="onboarding-documents-table">
<TableHeader>
<TableRow>
@ -581,7 +581,7 @@ export function ApplicationDetailsTabs(props: ApplicationDetailsTabsProps) {
<TabsContent value="interviews" className="space-y-6" data-testid="onboarding-tab-content-interviews">
<div>
<h3 className="text-slate-900 mb-4">Scheduled Interviews</h3>
<div className="overflow-x-auto">
<div className="overflow-x-auto custom-scrollbar-x-slim">
<Table data-testid="onboarding-interviews-scheduled-table">
<TableHeader>
<TableRow>

View File

@ -62,6 +62,7 @@ export function useApplicationDetailsUIState({ initialTab = 'questionnaire' }: U
const [fddAgencies, setFddAgencies] = useState<any[]>([]);
const [selectedAgencyId, setSelectedAgencyId] = useState('');
const [isAssigningAgency, setIsAssigningAgency] = useState(false);
const [showAssignFddModal, setShowAssignFddModal] = useState(false);
const [isApproving, setIsApproving] = useState(false);
const [isRejecting, setIsRejecting] = useState(false);
const [ktMatrixScores, setKtMatrixScores] = useState<Record<string, number>>({});
@ -142,6 +143,7 @@ export function useApplicationDetailsUIState({ initialTab = 'questionnaire' }: U
fddAgencies, setFddAgencies,
selectedAgencyId, setSelectedAgencyId,
isAssigningAgency, setIsAssigningAgency,
showAssignFddModal, setShowAssignFddModal,
isApproving, setIsApproving,
isRejecting, setIsRejecting,
ktMatrixScores, setKtMatrixScores,

View File

@ -105,6 +105,7 @@ export const ApplicationDetails = () => {
fddAgencies, setFddAgencies,
selectedAgencyId, setSelectedAgencyId,
isAssigningAgency, setIsAssigningAgency,
showAssignFddModal, setShowAssignFddModal,
isApproving, setIsApproving,
isRejecting, setIsRejecting,
ktMatrixScores, setKtMatrixScores,
@ -498,6 +499,21 @@ export const ApplicationDetails = () => {
currentUser={currentUser}
handleGenerateDealerCodes={handleGenerateDealerCodes}
onOpenAssignArchitectureModal={() => setShowAssignArchitectureModal(true)}
onOpenAssignFdd={() => {
setSelectedAgencyId('');
fetchFddAgencies();
setShowAssignFddModal(true);
}}
showAssignFddModal={showAssignFddModal}
setShowAssignFddModal={setShowAssignFddModal}
fddAgencies={fddAgencies}
selectedAgencyId={selectedAgencyId}
setSelectedAgencyId={setSelectedAgencyId}
isAssigningAgency={isAssigningAgency}
handleAssignAgency={async () => {
await handleAssignAgency();
setShowAssignFddModal(false);
}}
activeInterviewForUser={activeInterviewForUser}
hasSubmittedFeedback={hasSubmittedFeedback}
setSelectedInterviewForFeedback={setSelectedInterviewForFeedback}

View File

@ -681,7 +681,7 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser }: Rel
<Card>
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
<CardHeader className="pb-4">
<div className="overflow-x-auto -mx-6 px-6">
<div className="overflow-x-auto custom-scrollbar-x-slim -mx-6 px-6">
<TabsList className="w-max min-w-full justify-start">
<TabsTrigger value="workflow">Workflow Progress</TabsTrigger>
<TabsTrigger value="documents">Documents</TabsTrigger>

View File

@ -9,6 +9,7 @@ import { Textarea } from '@/components/ui/textarea';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Input } from '@/components/ui/input';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { Separator } from '@/components/ui/separator';
import { useState, useEffect, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { User as UserType } from '@/lib/mock-data';
@ -479,149 +480,6 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
</div>
</div>
{/* Action Bar - Professional Layout */}
<Card className="border-slate-200 shadow-sm">
<CardContent className="pt-6">
<div className="flex flex-col gap-4">
{/* Primary Actions Row */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="text-sm text-slate-600 mr-2">Workflow Actions:</span>
{/* Debug for PPT button visibility */}
{(() => {
const roleNormalized = String(currentUser?.roleCode || currentUser?.role || '').trim().toUpperCase();
const isDDLeadUser = roleNormalized === 'DD LEAD' || roleNormalized === 'DD_LEAD';
const isDDLeadStageCurrent = ['DD Lead', 'DD Lead Review', 'DDL Review'].includes(resignationData?.currentStage);
if (isDDLeadUser && isDDLeadStageCurrent) {
return (
<Button
size="sm"
variant="outline"
className="text-amber-700 border-amber-300 hover:bg-amber-50 shadow-sm"
onClick={() => {
setUploadDocType('PPT Presentation');
setUploadStage('DD Lead');
setShowUploadDialog(true);
}}
>
<Upload className="w-4 h-4 mr-2" />
Upload PPT
</Button>
);
}
return null;
})()}
{permissions.canApprove && (
<Button
size="sm"
disabled={isSubmitting}
className="bg-green-600 hover:bg-green-700 transition-all hover:shadow-md"
onClick={() => handleAction('approve')}
>
{isSubmitting && actionDialog.type === 'approve' ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <Check className="w-4 h-4 mr-2" />}
Approve
</Button>
)}
{permissions.canSendBack && (
<Button
size="sm"
variant="outline"
disabled={isSubmitting}
className="hover:bg-slate-50 transition-all font-bold"
onClick={() => handleAction('sendBack')}
>
{isSubmitting && actionDialog.type === 'sendBack' ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <RotateCcw className="w-4 h-4 mr-2" />}
Send Back
</Button>
)}
{permissions.canWithdraw && (
<Button
size="sm"
variant="outline"
disabled={isSubmitting}
className="text-red-600 border-red-300 hover:bg-red-50 transition-all font-bold"
onClick={() => handleAction('withdrawal')}
>
{isSubmitting && actionDialog.type === 'withdrawal' ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <X className="w-4 h-4 mr-2" />}
Withdrawal
</Button>
)}
{permissions.canRevoke && (
<Button
size="sm"
variant="outline"
disabled={isSubmitting}
className="text-orange-600 border-orange-300 hover:bg-orange-50 transition-all font-bold"
onClick={() => handleAction('revoke')}
>
{isSubmitting && actionDialog.type === 'revoke' ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <Ban className="w-4 h-4 mr-2" />}
Revoke
</Button>
)}
</div>
{/* Secondary Actions */}
<div className="flex items-center gap-2">
{permissions.canPushToFnF && (
<Button
size="sm"
variant="outline"
disabled={isSubmitting}
className="text-amber-600 border-blue-300 hover:bg-blue-50 transition-all"
onClick={() => handleAction('pushfnf')}
>
{isSubmitting && actionDialog.type === 'pushfnf' ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <Send className="w-4 h-4 mr-2" />}
Push to F&F
</Button>
)}
{permissions.canAssign && (
<Button
size="sm"
variant="outline"
disabled={isSubmitting}
className="hover:bg-slate-50 transition-all"
onClick={() => handleAction('assign')}
>
{isSubmitting && actionDialog.type === 'assign' ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <UserPlus className="w-4 h-4 mr-2" />}
Assign User
</Button>
)}
</div>
</div>
{/* Work Notes Button - Independent Section */}
<div className="flex items-center justify-between pt-4 border-t border-slate-200">
<div className="flex items-center gap-2">
<MessageSquare className="w-4 h-4 text-slate-500" />
<span className="text-sm text-slate-600">Communication & Notes</span>
</div>
<Button
size="sm"
variant="outline"
className="relative hover:bg-amber-50 hover:border-amber-300 hover:text-amber-700 transition-all shadow-sm"
onClick={() => navigate(`/worknotes/resignation/${resignationId}`, {
state: {
applicationName: resignationData?.outlet?.name || 'Resignation',
registrationNumber: resignationData?.resignationId || '',
participants: resignationData?.participants || []
}
})}
>
<MessageSquare className="w-4 h-4 mr-2" />
View Work Notes
{resignationData?.worknotes?.length > 0 && (
<Badge className="ml-2 bg-amber-600 hover:bg-amber-700 text-white h-5 px-2">
{resignationData.worknotes.length}
</Badge>
)}
</Button>
</div>
</div>
</CardContent>
</Card>
{/* Tabs */}
<Tabs defaultValue="details" className="w-full">
<TabsList className="bg-slate-100 p-1">
<TabsTrigger value="details" className="data-[state=active]:bg-white">Details</TabsTrigger>
@ -633,8 +491,55 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
)}
</TabsList>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mt-6">
<div className="lg:col-span-2 space-y-6">
{/* Details Tab */}
<TabsContent value="details" className="space-y-6">
<Card>
<CardHeader>
<CardTitle>Resignation Details</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<Label className="text-slate-600">Resignation Type</Label>
<p>{resignationData?.resignationType}</p>
</div>
<div>
<Label className="text-slate-600">Reason</Label>
<p>{resignationData?.reason}</p>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<Label className="text-slate-600">Last Operational Date (Sales)</Label>
<p>{resignationData?.lastOperationalDateSales ? formatDateTime(resignationData.lastOperationalDateSales, 'date') : 'N/A'}</p>
</div>
<div>
<Label className="text-slate-600">Last Operational Date (Services)</Label>
<p>{resignationData?.lastOperationalDateServices ? formatDateTime(resignationData.lastOperationalDateServices, 'date') : 'N/A'}</p>
</div>
</div>
<div>
<Label className="text-slate-600">Additional Info / Dealer Voice</Label>
<p>{resignationData?.additionalInfo || 'No additional info provided'}</p>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<Label className="text-slate-600">Submitted On</Label>
<p>{resignationData?.submittedOn ? formatDateTime(resignationData.submittedOn) : 'N/A'}</p>
</div>
<div>
<Label className="text-slate-600">Current Stage</Label>
<p>{resignationData?.currentStage}</p>
</div>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Request Information</CardTitle>
@ -706,51 +611,6 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Resignation Details</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<Label className="text-slate-600">Resignation Type</Label>
<p>{resignationData?.resignationType}</p>
</div>
<div>
<Label className="text-slate-600">Reason</Label>
<p>{resignationData?.reason}</p>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<Label className="text-slate-600">Last Operational Date (Sales)</Label>
<p>{resignationData?.lastOperationalDateSales ? formatDateTime(resignationData.lastOperationalDateSales, 'date') : 'N/A'}</p>
</div>
<div>
<Label className="text-slate-600">Last Operational Date (Services)</Label>
<p>{resignationData?.lastOperationalDateServices ? formatDateTime(resignationData.lastOperationalDateServices, 'date') : 'N/A'}</p>
</div>
</div>
<div>
<Label className="text-slate-600">Additional Info / Dealer Voice</Label>
<p>{resignationData?.additionalInfo || 'No additional info provided'}</p>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<Label className="text-slate-600">Submitted On</Label>
<p>{resignationData?.submittedOn ? formatDateTime(resignationData.submittedOn) : 'N/A'}</p>
</div>
<div>
<Label className="text-slate-600">Current Stage</Label>
<p>{resignationData?.currentStage}</p>
</div>
</div>
</div>
</CardContent>
</Card>
</TabsContent>
{/* Progress Tab */}
@ -1073,6 +933,135 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
</Card>
</TabsContent>
)}
</div>
{/* Sidebar */}
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle>Actions</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{(() => {
const roleNormalized = String(currentUser?.roleCode || currentUser?.role || '').trim().toUpperCase();
const isDDLeadUser = roleNormalized === 'DD LEAD' || roleNormalized === 'DD_LEAD';
const isDDLeadStageCurrent = ['DD Lead', 'DD Lead Review', 'DDL Review'].includes(resignationData?.currentStage);
if (isDDLeadUser && isDDLeadStageCurrent) {
return (
<Button
variant="outline"
className="w-full text-amber-700 border-amber-300 hover:bg-amber-50"
onClick={() => {
setUploadDocType('PPT Presentation');
setUploadStage('DD Lead');
setShowUploadDialog(true);
}}
>
<Upload className="w-4 h-4 mr-2" />
Upload PPT
</Button>
);
}
return null;
})()}
{permissions.canApprove && (
<Button
disabled={isSubmitting}
className="w-full bg-green-600 hover:bg-green-700 font-bold"
onClick={() => handleAction('approve')}
>
{isSubmitting && actionDialog.type === 'approve' ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <Check className="w-4 h-4 mr-2" />}
Approve
</Button>
)}
{permissions.canSendBack && (
<Button
variant="outline"
disabled={isSubmitting}
className="w-full font-bold"
onClick={() => handleAction('sendBack')}
>
{isSubmitting && actionDialog.type === 'sendBack' ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <RotateCcw className="w-4 h-4 mr-2" />}
Send Back
</Button>
)}
{permissions.canWithdraw && (
<Button
variant="outline"
disabled={isSubmitting}
className="w-full text-red-600 border-red-300 hover:bg-red-50 font-bold"
onClick={() => handleAction('withdrawal')}
>
{isSubmitting && actionDialog.type === 'withdrawal' ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <X className="w-4 h-4 mr-2" />}
Withdrawal
</Button>
)}
{permissions.canRevoke && (
<Button
variant="outline"
disabled={isSubmitting}
className="w-full text-orange-600 border-orange-300 hover:bg-orange-50 font-bold"
onClick={() => handleAction('revoke')}
>
{isSubmitting && actionDialog.type === 'revoke' ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <Ban className="w-4 h-4 mr-2" />}
Revoke
</Button>
)}
{permissions.canPushToFnF && (
<Button
variant="outline"
disabled={isSubmitting}
className="w-full text-amber-700 border-amber-300 hover:bg-amber-50"
onClick={() => handleAction('pushfnf')}
>
{isSubmitting && actionDialog.type === 'pushfnf' ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <Send className="w-4 h-4 mr-2" />}
Push to F&F
</Button>
)}
{permissions.canAssign && (
<Button
variant="outline"
disabled={isSubmitting}
className="w-full"
onClick={() => handleAction('assign')}
>
{isSubmitting && actionDialog.type === 'assign' ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <UserPlus className="w-4 h-4 mr-2" />}
Assign User
</Button>
)}
<Separator />
<Button
variant="outline"
className="w-full"
onClick={() => navigate(`/worknotes/resignation/${resignationId}`, {
state: {
applicationName: resignationData?.outlet?.name || 'Resignation',
registrationNumber: resignationData?.resignationId || '',
participants: resignationData?.participants || []
}
})}
>
<MessageSquare className="w-4 h-4 mr-2" />
View Work Notes
{resignationData?.worknotes?.length > 0 && (
<Badge className="ml-auto bg-amber-600 hover:bg-amber-700 text-white h-5 px-2">
{resignationData.worknotes.length}
</Badge>
)}
</Button>
</CardContent>
</Card>
</div>
</div>
</Tabs>
{/* Action Dialogs */}

View File

@ -9,6 +9,7 @@ import { Textarea } from '@/components/ui/textarea';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { Separator } from '@/components/ui/separator';
import { useState, useEffect } from 'react';
import { User } from '@/lib/mock-data';
import { toast } from 'sonner';
@ -580,157 +581,6 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
</div>
</div>
{/* Action Bar - Professional Layout */}
<Card className="border-amber-200 shadow-sm">
<CardContent className="pt-6">
<div className="flex flex-col gap-4">
{(request.currentStage === 'Evaluation of Dealer SCN Response' || request.currentStage === 'Personal Hearing') && (
<Alert className="mb-4 bg-blue-50 border-blue-200">
<AlertTitle className="text-blue-800 text-sm font-semibold">Joint Review Stage</AlertTitle>
<AlertDescription className="text-blue-700 text-xs">
This stage requires a joint evaluation of the SCN response by the <strong>DD-Lead, ZBH, RBM, and DD-Head</strong>.
The case will only advance to NBH Final Approval once all four stakeholders have recorded their review.
</AlertDescription>
</Alert>
)}
{/* Primary Actions Row */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="text-sm text-slate-600 mr-2">Termination Actions:</span>
{currentUser?.role !== 'Dealer' && (
<>
{!permissions.canFinalize && (
<>
{permissions.canApprove && (
<Button
size="sm"
className="bg-green-600 hover:bg-green-700 transition-all hover:shadow-md"
onClick={() => handleAction('approve')}
>
<Check className="w-4 h-4 mr-2" />
Approve
</Button>
)}
{permissions.canIssueSCN && (
<Button
size="sm"
className="bg-purple-600 hover:bg-purple-700 transition-all shadow-sm"
onClick={() => setShowSCNDialog(true)}
>
<AlertTriangle className="w-4 h-4 mr-2" />
Issue SCN
</Button>
)}
{permissions.canUploadSCNResponse && (
<Button
size="sm"
className="bg-amber-600 hover:bg-amber-700 transition-all shadow-sm"
onClick={() => {
setScnFile(null);
setShowSCNDialog(true);
}}
>
<FileText className="w-4 h-4 mr-2" />
Upload SCN Response
</Button>
)}
{permissions.canApprove && (
<Button
size="sm"
variant="outline"
className="hover:bg-slate-50 transition-all"
onClick={() => handleAction('sendBack')}
>
<RotateCcw className="w-4 h-4 mr-2" />
Send Back
</Button>
)}
</>
)}
{permissions.canHold && (
<Button
size="sm"
variant="outline"
className="border-orange-200 text-orange-700 hover:bg-orange-50"
onClick={() => handleAction('hold')}
>
<PauseCircle className="w-4 h-4 mr-2" />
Hold Decision
</Button>
)}
{permissions.canFinalize && (
<Button
size="sm"
className="bg-indigo-600 hover:bg-indigo-700 transition-all shadow-sm"
onClick={() => setShowFinalizeDialog(true)}
>
<ShieldCheck className="w-4 h-4 mr-2" />
Final Authorization
</Button>
)}
</>
)}
</div>
{/* Secondary Actions */}
<div className="flex items-center gap-2">
{permissions.canPushToFnF && (
<Button
size="sm"
variant="outline"
className="text-blue-600 border-blue-300 hover:bg-blue-50 transition-all"
onClick={() => handleAction('pushfnf')}
>
<Send className="w-4 h-4 mr-2" />
Push to F&F
</Button>
)}
{!permissions.isFinalState && (
<Button
size="sm"
variant="outline"
className="hover:bg-slate-50 transition-all"
onClick={() => handleAction('assign')}
>
<UserPlus className="w-4 h-4 mr-2" />
Assign User
</Button>
)}
</div>
</div>
{/* Work Notes Button - Independent Section */}
<div className="flex items-center justify-between pt-4 border-t border-amber-200">
<div className="flex items-center gap-2">
<MessageSquare className="w-4 h-4 text-slate-500" />
<span className="text-sm text-slate-600">Communication & Notes</span>
</div>
<Button
size="sm"
variant="outline"
className="relative hover:bg-amber-50 hover:border-amber-300 hover:text-amber-700 transition-all shadow-sm"
onClick={() => navigate(`/worknotes/termination/${terminationId}`, {
state: {
applicationName: request?.dealer?.businessName || 'Termination',
registrationNumber: terminationId || '',
participants: request?.participants || []
}
})}
>
<MessageSquare className="w-4 h-4 mr-2" />
View Work Notes
{workNotesCount > 0 && (
<Badge className="ml-2 bg-amber-600 hover:bg-amber-700 text-white h-5 px-2">
{workNotesCount}
</Badge>
)}
</Button>
</div>
</div>
</CardContent>
</Card>
{/* Tabs */}
<Tabs defaultValue="details" className="w-full">
<TabsList className="bg-slate-100 p-1">
<TabsTrigger value="details" className="data-[state=active]:bg-white">Details</TabsTrigger>
@ -739,6 +589,9 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
<TabsTrigger value="audit" className="data-[state=active]:bg-white">Audit Trail</TabsTrigger>
</TabsList>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mt-6">
<div className="lg:col-span-2 space-y-6">
{/* Details Tab */}
<TabsContent value="details" className="space-y-6">
<Card>
@ -1149,6 +1002,139 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
</CardContent>
</Card>
</TabsContent>
</div>
{/* Sidebar */}
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle>Actions</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{(request.currentStage === 'Evaluation of Dealer SCN Response' || request.currentStage === 'Personal Hearing') && (
<Alert className="mb-2 bg-blue-50 border-blue-200">
<AlertTitle className="text-blue-800 text-sm font-semibold">Joint Review Stage</AlertTitle>
<AlertDescription className="text-blue-700 text-xs">
Joint evaluation by <strong>DD-Lead, ZBH, RBM, and DD-Head</strong> required before advancing to NBH Final Approval.
</AlertDescription>
</Alert>
)}
{currentUser?.role !== 'Dealer' && (
<>
{!permissions.canFinalize && (
<>
{permissions.canApprove && (
<Button
className="w-full bg-green-600 hover:bg-green-700 font-bold"
onClick={() => handleAction('approve')}
>
<Check className="w-4 h-4 mr-2" />
Approve
</Button>
)}
{permissions.canIssueSCN && (
<Button
className="w-full bg-purple-600 hover:bg-purple-700"
onClick={() => setShowSCNDialog(true)}
>
<AlertTriangle className="w-4 h-4 mr-2" />
Issue SCN
</Button>
)}
{permissions.canUploadSCNResponse && (
<Button
className="w-full bg-amber-600 hover:bg-amber-700"
onClick={() => {
setScnFile(null);
setShowSCNDialog(true);
}}
>
<FileText className="w-4 h-4 mr-2" />
Upload SCN Response
</Button>
)}
{permissions.canApprove && (
<Button
variant="outline"
className="w-full"
onClick={() => handleAction('sendBack')}
>
<RotateCcw className="w-4 h-4 mr-2" />
Send Back
</Button>
)}
</>
)}
{permissions.canHold && (
<Button
variant="outline"
className="w-full border-orange-200 text-orange-700 hover:bg-orange-50"
onClick={() => handleAction('hold')}
>
<PauseCircle className="w-4 h-4 mr-2" />
Hold Decision
</Button>
)}
{permissions.canFinalize && (
<Button
className="w-full bg-indigo-600 hover:bg-indigo-700"
onClick={() => setShowFinalizeDialog(true)}
>
<ShieldCheck className="w-4 h-4 mr-2" />
Final Authorization
</Button>
)}
</>
)}
{permissions.canPushToFnF && (
<Button
variant="outline"
className="w-full text-blue-600 border-blue-300 hover:bg-blue-50"
onClick={() => handleAction('pushfnf')}
>
<Send className="w-4 h-4 mr-2" />
Push to F&F
</Button>
)}
{!permissions.isFinalState && (
<Button
variant="outline"
className="w-full"
onClick={() => handleAction('assign')}
>
<UserPlus className="w-4 h-4 mr-2" />
Assign User
</Button>
)}
<Separator />
<Button
variant="outline"
className="w-full"
onClick={() => navigate(`/worknotes/termination/${terminationId}`, {
state: {
applicationName: request?.dealer?.businessName || 'Termination',
registrationNumber: terminationId || '',
participants: request?.participants || []
}
})}
>
<MessageSquare className="w-4 h-4 mr-2" />
View Work Notes
{workNotesCount > 0 && (
<Badge className="ml-auto bg-amber-600 hover:bg-amber-700 text-white h-5 px-2">
{workNotesCount}
</Badge>
)}
</Button>
</CardContent>
</Card>
</div>
</div>
</Tabs>
{/* Action Dialogs */}

View File

@ -283,7 +283,7 @@ html {
/* Thin, light horizontal scrollbar (e.g. tab strips with overflow-x) */
.custom-scrollbar-x::-webkit-scrollbar {
height: 4px;
height: 2px;
}
.custom-scrollbar-x::-webkit-scrollbar-track {
@ -291,22 +291,22 @@ html {
}
.custom-scrollbar-x::-webkit-scrollbar-thumb {
background: #e2e8f0;
background: #f1f5f9;
border-radius: 9999px;
}
.custom-scrollbar-x::-webkit-scrollbar-thumb:hover {
background: #cbd5e1;
background: #e2e8f0;
}
.custom-scrollbar-x {
scrollbar-width: thin;
scrollbar-color: #e2e8f0 transparent;
scrollbar-color: #f1f5f9 transparent;
}
/* Extra-thin, subtle horizontal scrollbar (documents modal tables) */
.custom-scrollbar-x-slim::-webkit-scrollbar {
height: 3px;
height: 2px;
}
.custom-scrollbar-x-slim::-webkit-scrollbar-track {
@ -314,17 +314,17 @@ html {
}
.custom-scrollbar-x-slim::-webkit-scrollbar-thumb {
background: #f1f5f9;
background: #f8fafc;
border-radius: 9999px;
}
.custom-scrollbar-x-slim::-webkit-scrollbar-thumb:hover {
background: #e2e8f0;
background: #f1f5f9;
}
.custom-scrollbar-x-slim {
scrollbar-width: thin;
scrollbar-color: #f1f5f9 transparent;
scrollbar-color: #f8fafc transparent;
}
/* Extra-thin, light vertical scrollbar (e.g. modals) */