system log table added and feew bugs coverd from the tracker
This commit is contained in:
parent
201bfa6a41
commit
ec70f1d3f1
1
package-lock.json
generated
1
package-lock.json
generated
@ -12222,7 +12222,6 @@
|
|||||||
"version": "2.3.2",
|
"version": "2.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||||
"dev": true,
|
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
|
|||||||
@ -47,6 +47,7 @@ import { DealerRelocationPage } from '@/features/relocation/pages/DealerRelocati
|
|||||||
import QuestionnaireBuilder from '@/components/admin/QuestionnaireBuilder';
|
import QuestionnaireBuilder from '@/components/admin/QuestionnaireBuilder';
|
||||||
import QuestionnaireList from '@/components/admin/QuestionnaireList';
|
import QuestionnaireList from '@/components/admin/QuestionnaireList';
|
||||||
import InterviewConfigManagement from '@/features/master/components/InterviewConfigManagement';
|
import InterviewConfigManagement from '@/features/master/components/InterviewConfigManagement';
|
||||||
|
import { SystemLogsPage } from '@/features/admin/pages/SystemLogsPage';
|
||||||
import { WorkNotesPage } from '@/features/onboarding/pages/WorkNotesPage';
|
import { WorkNotesPage } from '@/features/onboarding/pages/WorkNotesPage';
|
||||||
import { NotificationsPage } from '@/pages/NotificationsPage';
|
import { NotificationsPage } from '@/pages/NotificationsPage';
|
||||||
import { Toaster } from '@/components/ui/sonner';
|
import { Toaster } from '@/components/ui/sonner';
|
||||||
@ -275,6 +276,11 @@ export default function App() {
|
|||||||
? <InterviewConfigManagement />
|
? <InterviewConfigManagement />
|
||||||
: <Navigate to="/dashboard" />
|
: <Navigate to="/dashboard" />
|
||||||
} />
|
} />
|
||||||
|
<Route path="/system-logs" element={
|
||||||
|
hasRole(['Super Admin'])
|
||||||
|
? <SystemLogsPage />
|
||||||
|
: <Navigate to="/dashboard" />
|
||||||
|
} />
|
||||||
|
|
||||||
{/* HR/Finance Modules (Simplified for brevity, following pattern) */}
|
{/* HR/Finance Modules (Simplified for brevity, following pattern) */}
|
||||||
<Route path="/resignation" element={
|
<Route path="/resignation" element={
|
||||||
|
|||||||
@ -239,6 +239,21 @@ export const API = {
|
|||||||
getFddAssignment: (applicationId: string) => client.get(`/fdd/${applicationId}`),
|
getFddAssignment: (applicationId: string) => client.get(`/fdd/${applicationId}`),
|
||||||
assignFddAgency: (data: any) => client.post('/fdd/assign', data),
|
assignFddAgency: (data: any) => client.post('/fdd/assign', data),
|
||||||
flagNonResponsive: (data: any) => client.post('/flag', 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;
|
export default API;
|
||||||
|
|||||||
@ -197,7 +197,7 @@ export function ApprovalPoliciesPage() {
|
|||||||
<CardTitle className="text-lg font-semibold text-slate-800">Configured Stages</CardTitle>
|
<CardTitle className="text-lg font-semibold text-slate-800">Configured Stages</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="p-0">
|
<CardContent className="p-0">
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto custom-scrollbar-x-slim">
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader className="bg-slate-50/50">
|
<TableHeader className="bg-slate-50/50">
|
||||||
<TableRow>
|
<TableRow>
|
||||||
|
|||||||
@ -294,14 +294,14 @@ const QuestionnaireBuilder: React.FC = () => {
|
|||||||
onChange={(e) => updateQuestion(index, 'inputType', e.target.value as any)}
|
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"
|
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="email">Email Address</option>
|
||||||
<option value="textarea">Long Text (Textarea)</option>
|
<option value="textarea">Paragraph</option>
|
||||||
<option value="number">Numeric</option>
|
<option value="number">Numeric</option>
|
||||||
<option value="file">File Upload</option>
|
<option value="file">File Upload</option>
|
||||||
<option value="yesno">Yes / No</option>
|
<option value="yesno">Yes / No</option>
|
||||||
<option value="select">Multiple Choice (Dropdown)</option>
|
<option value="select">Options (Dropdown)</option>
|
||||||
<option value="radio">Multiple Choice (Radio)</option>
|
<option value="radio">Options (Radio)</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -14,7 +14,8 @@ import {
|
|||||||
RefreshCcw,
|
RefreshCcw,
|
||||||
MapPin,
|
MapPin,
|
||||||
ClipboardList,
|
ClipboardList,
|
||||||
ListChecks
|
ListChecks,
|
||||||
|
Activity
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useState, useRef, useCallback, useEffect } from 'react';
|
import { useState, useRef, useCallback, useEffect } from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
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: 'users', label: 'User Management', icon: Users });
|
||||||
menuItems.push({ id: 'questionnaires', label: 'Questionnaire Templates', icon: ClipboardList });
|
menuItems.push({ id: 'questionnaires', label: 'Questionnaire Templates', icon: ClipboardList });
|
||||||
menuItems.push({ id: 'interview-configs', label: 'Interview Configs', icon: ListChecks });
|
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) => {
|
const handleSearch = (e: React.FormEvent) => {
|
||||||
|
|||||||
@ -42,14 +42,18 @@ function ScrollBar({
|
|||||||
orientation === "vertical" &&
|
orientation === "vertical" &&
|
||||||
"h-full w-2.5 border-l border-l-transparent",
|
"h-full w-2.5 border-l border-l-transparent",
|
||||||
orientation === "horizontal" &&
|
orientation === "horizontal" &&
|
||||||
"h-2.5 flex-col border-t border-t-transparent",
|
"h-1 flex-col border-t border-t-transparent",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<ScrollAreaPrimitive.ScrollAreaThumb
|
<ScrollAreaPrimitive.ScrollAreaThumb
|
||||||
data-slot="scroll-area-thumb"
|
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>
|
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -8,7 +8,7 @@ function Table({ className, ...props }: React.ComponentProps<"table">) {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="table-container"
|
data-slot="table-container"
|
||||||
className="relative w-full overflow-x-auto"
|
className="relative w-full overflow-x-auto custom-scrollbar-x-slim"
|
||||||
>
|
>
|
||||||
<table
|
<table
|
||||||
data-slot="table"
|
data-slot="table"
|
||||||
|
|||||||
612
src/features/admin/pages/SystemLogsPage.tsx
Normal file
612
src/features/admin/pages/SystemLogsPage.tsx
Normal 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;
|
||||||
@ -701,7 +701,7 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
|
|||||||
<Card>
|
<Card>
|
||||||
<Tabs value={activeMainTab} onValueChange={setActiveMainTab} className="w-full">
|
<Tabs value={activeMainTab} onValueChange={setActiveMainTab} className="w-full">
|
||||||
<CardHeader className="pb-4">
|
<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">
|
<TabsList className="w-max min-w-full justify-start">
|
||||||
<TabsTrigger value="workflow">Workflow Progress</TabsTrigger>
|
<TabsTrigger value="workflow">Workflow Progress</TabsTrigger>
|
||||||
<TabsTrigger value="documents">Documents</TabsTrigger>
|
<TabsTrigger value="documents">Documents</TabsTrigger>
|
||||||
|
|||||||
@ -147,7 +147,7 @@ export function FDDDashboardPage() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto custom-scrollbar-x-slim">
|
||||||
<table className="w-full text-left border-collapse">
|
<table className="w-full text-left border-collapse">
|
||||||
<thead className="bg-slate-50 border-b border-slate-200">
|
<thead className="bg-slate-50 border-b border-slate-200">
|
||||||
<tr>
|
<tr>
|
||||||
|
|||||||
@ -404,7 +404,7 @@ const InterviewConfigManagement: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Items Area with Horizontal Guard */}
|
{/* 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">
|
<div className="min-w-[900px] border border-slate-100 rounded-xl">
|
||||||
<table className="w-full text-left border-collapse table-fixed">
|
<table className="w-full text-left border-collapse table-fixed">
|
||||||
<thead>
|
<thead>
|
||||||
@ -449,9 +449,9 @@ const InterviewConfigManagement: React.FC = () => {
|
|||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="select">Selection</SelectItem>
|
<SelectItem value="select">Options</SelectItem>
|
||||||
<SelectItem value="text">Text</SelectItem>
|
<SelectItem value="text">One Liner</SelectItem>
|
||||||
<SelectItem value="textarea">Comment</SelectItem>
|
<SelectItem value="textarea">Paragraph</SelectItem>
|
||||||
<SelectItem value="number">Numeric</SelectItem>
|
<SelectItem value="number">Numeric</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
@ -535,7 +535,7 @@ const InterviewConfigManagement: React.FC = () => {
|
|||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<div className="flex items-center justify-between border-b border-slate-200/50 pb-2 mb-2">
|
<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">
|
<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>
|
</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)}>
|
<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
|
<Plus className="w-3 h-3 mr-1.5" /> Append Option
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import {
|
|||||||
Calendar,
|
Calendar,
|
||||||
CheckCircle,
|
CheckCircle,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
|
ClipboardCheck,
|
||||||
Clock,
|
Clock,
|
||||||
GitBranch,
|
GitBranch,
|
||||||
Info,
|
Info,
|
||||||
@ -46,6 +47,14 @@ interface ApplicationDetailsSidebarProps {
|
|||||||
currentUser: any;
|
currentUser: any;
|
||||||
handleGenerateDealerCodes: () => void;
|
handleGenerateDealerCodes: () => void;
|
||||||
onOpenAssignArchitectureModal: () => 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;
|
activeInterviewForUser: any;
|
||||||
hasSubmittedFeedback: boolean;
|
hasSubmittedFeedback: boolean;
|
||||||
setSelectedInterviewForFeedback: (value: any) => void;
|
setSelectedInterviewForFeedback: (value: any) => void;
|
||||||
@ -80,6 +89,14 @@ export function ApplicationDetailsSidebar(props: ApplicationDetailsSidebarProps)
|
|||||||
currentUser,
|
currentUser,
|
||||||
handleGenerateDealerCodes,
|
handleGenerateDealerCodes,
|
||||||
onOpenAssignArchitectureModal,
|
onOpenAssignArchitectureModal,
|
||||||
|
onOpenAssignFdd,
|
||||||
|
showAssignFddModal,
|
||||||
|
setShowAssignFddModal,
|
||||||
|
fddAgencies,
|
||||||
|
selectedAgencyId,
|
||||||
|
setSelectedAgencyId,
|
||||||
|
isAssigningAgency,
|
||||||
|
handleAssignAgency,
|
||||||
activeInterviewForUser,
|
activeInterviewForUser,
|
||||||
hasSubmittedFeedback,
|
hasSubmittedFeedback,
|
||||||
setSelectedInterviewForFeedback,
|
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 && (
|
{activeInterviewForUser && !hasSubmittedFeedback && (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
|
|||||||
@ -126,7 +126,7 @@ export function ApplicationDetailsTabs(props: ApplicationDetailsTabsProps) {
|
|||||||
<Card data-testid="onboarding-details-tabs-container">
|
<Card data-testid="onboarding-details-tabs-container">
|
||||||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||||
<CardHeader className="pb-4 px-4 sm:px-6">
|
<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">
|
<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="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>
|
<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>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto custom-scrollbar-x-slim">
|
||||||
<Table data-testid="onboarding-documents-table">
|
<Table data-testid="onboarding-documents-table">
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
@ -581,7 +581,7 @@ export function ApplicationDetailsTabs(props: ApplicationDetailsTabsProps) {
|
|||||||
<TabsContent value="interviews" className="space-y-6" data-testid="onboarding-tab-content-interviews">
|
<TabsContent value="interviews" className="space-y-6" data-testid="onboarding-tab-content-interviews">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-slate-900 mb-4">Scheduled Interviews</h3>
|
<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">
|
<Table data-testid="onboarding-interviews-scheduled-table">
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
|
|||||||
@ -62,6 +62,7 @@ export function useApplicationDetailsUIState({ initialTab = 'questionnaire' }: U
|
|||||||
const [fddAgencies, setFddAgencies] = useState<any[]>([]);
|
const [fddAgencies, setFddAgencies] = useState<any[]>([]);
|
||||||
const [selectedAgencyId, setSelectedAgencyId] = useState('');
|
const [selectedAgencyId, setSelectedAgencyId] = useState('');
|
||||||
const [isAssigningAgency, setIsAssigningAgency] = useState(false);
|
const [isAssigningAgency, setIsAssigningAgency] = useState(false);
|
||||||
|
const [showAssignFddModal, setShowAssignFddModal] = useState(false);
|
||||||
const [isApproving, setIsApproving] = useState(false);
|
const [isApproving, setIsApproving] = useState(false);
|
||||||
const [isRejecting, setIsRejecting] = useState(false);
|
const [isRejecting, setIsRejecting] = useState(false);
|
||||||
const [ktMatrixScores, setKtMatrixScores] = useState<Record<string, number>>({});
|
const [ktMatrixScores, setKtMatrixScores] = useState<Record<string, number>>({});
|
||||||
@ -142,6 +143,7 @@ export function useApplicationDetailsUIState({ initialTab = 'questionnaire' }: U
|
|||||||
fddAgencies, setFddAgencies,
|
fddAgencies, setFddAgencies,
|
||||||
selectedAgencyId, setSelectedAgencyId,
|
selectedAgencyId, setSelectedAgencyId,
|
||||||
isAssigningAgency, setIsAssigningAgency,
|
isAssigningAgency, setIsAssigningAgency,
|
||||||
|
showAssignFddModal, setShowAssignFddModal,
|
||||||
isApproving, setIsApproving,
|
isApproving, setIsApproving,
|
||||||
isRejecting, setIsRejecting,
|
isRejecting, setIsRejecting,
|
||||||
ktMatrixScores, setKtMatrixScores,
|
ktMatrixScores, setKtMatrixScores,
|
||||||
|
|||||||
@ -105,6 +105,7 @@ export const ApplicationDetails = () => {
|
|||||||
fddAgencies, setFddAgencies,
|
fddAgencies, setFddAgencies,
|
||||||
selectedAgencyId, setSelectedAgencyId,
|
selectedAgencyId, setSelectedAgencyId,
|
||||||
isAssigningAgency, setIsAssigningAgency,
|
isAssigningAgency, setIsAssigningAgency,
|
||||||
|
showAssignFddModal, setShowAssignFddModal,
|
||||||
isApproving, setIsApproving,
|
isApproving, setIsApproving,
|
||||||
isRejecting, setIsRejecting,
|
isRejecting, setIsRejecting,
|
||||||
ktMatrixScores, setKtMatrixScores,
|
ktMatrixScores, setKtMatrixScores,
|
||||||
@ -498,6 +499,21 @@ export const ApplicationDetails = () => {
|
|||||||
currentUser={currentUser}
|
currentUser={currentUser}
|
||||||
handleGenerateDealerCodes={handleGenerateDealerCodes}
|
handleGenerateDealerCodes={handleGenerateDealerCodes}
|
||||||
onOpenAssignArchitectureModal={() => setShowAssignArchitectureModal(true)}
|
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}
|
activeInterviewForUser={activeInterviewForUser}
|
||||||
hasSubmittedFeedback={hasSubmittedFeedback}
|
hasSubmittedFeedback={hasSubmittedFeedback}
|
||||||
setSelectedInterviewForFeedback={setSelectedInterviewForFeedback}
|
setSelectedInterviewForFeedback={setSelectedInterviewForFeedback}
|
||||||
|
|||||||
@ -681,7 +681,7 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser }: Rel
|
|||||||
<Card>
|
<Card>
|
||||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
||||||
<CardHeader className="pb-4">
|
<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">
|
<TabsList className="w-max min-w-full justify-start">
|
||||||
<TabsTrigger value="workflow">Workflow Progress</TabsTrigger>
|
<TabsTrigger value="workflow">Workflow Progress</TabsTrigger>
|
||||||
<TabsTrigger value="documents">Documents</TabsTrigger>
|
<TabsTrigger value="documents">Documents</TabsTrigger>
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import { Textarea } from '@/components/ui/textarea';
|
|||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
||||||
|
import { Separator } from '@/components/ui/separator';
|
||||||
import { useState, useEffect, useMemo } from 'react';
|
import { useState, useEffect, useMemo } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { User as UserType } from '@/lib/mock-data';
|
import { User as UserType } from '@/lib/mock-data';
|
||||||
@ -479,149 +480,6 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
|||||||
</div>
|
</div>
|
||||||
</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">
|
<Tabs defaultValue="details" className="w-full">
|
||||||
<TabsList className="bg-slate-100 p-1">
|
<TabsList className="bg-slate-100 p-1">
|
||||||
<TabsTrigger value="details" className="data-[state=active]:bg-white">Details</TabsTrigger>
|
<TabsTrigger value="details" className="data-[state=active]:bg-white">Details</TabsTrigger>
|
||||||
@ -633,8 +491,55 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
|||||||
)}
|
)}
|
||||||
</TabsList>
|
</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 */}
|
{/* Details Tab */}
|
||||||
<TabsContent value="details" className="space-y-6">
|
<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>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Request Information</CardTitle>
|
<CardTitle>Request Information</CardTitle>
|
||||||
@ -706,51 +611,6 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
|||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</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>
|
</TabsContent>
|
||||||
|
|
||||||
{/* Progress Tab */}
|
{/* Progress Tab */}
|
||||||
@ -1073,6 +933,135 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
|||||||
</Card>
|
</Card>
|
||||||
</TabsContent>
|
</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>
|
</Tabs>
|
||||||
|
|
||||||
{/* Action Dialogs */}
|
{/* Action Dialogs */}
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import { Textarea } from '@/components/ui/textarea';
|
|||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
||||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
|
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
|
||||||
|
import { Separator } from '@/components/ui/separator';
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { User } from '@/lib/mock-data';
|
import { User } from '@/lib/mock-data';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
@ -580,157 +581,6 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
|||||||
</div>
|
</div>
|
||||||
</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">
|
<Tabs defaultValue="details" className="w-full">
|
||||||
<TabsList className="bg-slate-100 p-1">
|
<TabsList className="bg-slate-100 p-1">
|
||||||
<TabsTrigger value="details" className="data-[state=active]:bg-white">Details</TabsTrigger>
|
<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>
|
<TabsTrigger value="audit" className="data-[state=active]:bg-white">Audit Trail</TabsTrigger>
|
||||||
</TabsList>
|
</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 */}
|
{/* Details Tab */}
|
||||||
<TabsContent value="details" className="space-y-6">
|
<TabsContent value="details" className="space-y-6">
|
||||||
<Card>
|
<Card>
|
||||||
@ -1149,6 +1002,139 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</TabsContent>
|
</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>
|
</Tabs>
|
||||||
|
|
||||||
{/* Action Dialogs */}
|
{/* Action Dialogs */}
|
||||||
|
|||||||
@ -283,7 +283,7 @@ html {
|
|||||||
|
|
||||||
/* Thin, light horizontal scrollbar (e.g. tab strips with overflow-x) */
|
/* Thin, light horizontal scrollbar (e.g. tab strips with overflow-x) */
|
||||||
.custom-scrollbar-x::-webkit-scrollbar {
|
.custom-scrollbar-x::-webkit-scrollbar {
|
||||||
height: 4px;
|
height: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-scrollbar-x::-webkit-scrollbar-track {
|
.custom-scrollbar-x::-webkit-scrollbar-track {
|
||||||
@ -291,22 +291,22 @@ html {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.custom-scrollbar-x::-webkit-scrollbar-thumb {
|
.custom-scrollbar-x::-webkit-scrollbar-thumb {
|
||||||
background: #e2e8f0;
|
background: #f1f5f9;
|
||||||
border-radius: 9999px;
|
border-radius: 9999px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-scrollbar-x::-webkit-scrollbar-thumb:hover {
|
.custom-scrollbar-x::-webkit-scrollbar-thumb:hover {
|
||||||
background: #cbd5e1;
|
background: #e2e8f0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-scrollbar-x {
|
.custom-scrollbar-x {
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
scrollbar-color: #e2e8f0 transparent;
|
scrollbar-color: #f1f5f9 transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Extra-thin, subtle horizontal scrollbar (documents modal tables) */
|
/* Extra-thin, subtle horizontal scrollbar (documents modal tables) */
|
||||||
.custom-scrollbar-x-slim::-webkit-scrollbar {
|
.custom-scrollbar-x-slim::-webkit-scrollbar {
|
||||||
height: 3px;
|
height: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-scrollbar-x-slim::-webkit-scrollbar-track {
|
.custom-scrollbar-x-slim::-webkit-scrollbar-track {
|
||||||
@ -314,17 +314,17 @@ html {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.custom-scrollbar-x-slim::-webkit-scrollbar-thumb {
|
.custom-scrollbar-x-slim::-webkit-scrollbar-thumb {
|
||||||
background: #f1f5f9;
|
background: #f8fafc;
|
||||||
border-radius: 9999px;
|
border-radius: 9999px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-scrollbar-x-slim::-webkit-scrollbar-thumb:hover {
|
.custom-scrollbar-x-slim::-webkit-scrollbar-thumb:hover {
|
||||||
background: #e2e8f0;
|
background: #f1f5f9;
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-scrollbar-x-slim {
|
.custom-scrollbar-x-slim {
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
scrollbar-color: #f1f5f9 transparent;
|
scrollbar-color: #f8fafc transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Extra-thin, light vertical scrollbar (e.g. modals) */
|
/* Extra-thin, light vertical scrollbar (e.g. modals) */
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user