Fixed change management rendering error
This commit is contained in:
@@ -23,6 +23,7 @@ import {
|
|||||||
import { useAuth } from '../../auth/AuthProvider';
|
import { useAuth } from '../../auth/AuthProvider';
|
||||||
import { BrandLogo } from '../ui/BrandLogo';
|
import { BrandLogo } from '../ui/BrandLogo';
|
||||||
import { HelpTooltip } from '../ui/HelpTooltip';
|
import { HelpTooltip } from '../ui/HelpTooltip';
|
||||||
|
import { ErrorBoundary } from '../ui/ErrorBoundary';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { getUsers } from '../../services/api';
|
import { getUsers } from '../../services/api';
|
||||||
import { getUserEmail, registerUserEmail } from '../../lib/formatters';
|
import { getUserEmail, registerUserEmail } from '../../lib/formatters';
|
||||||
@@ -395,7 +396,9 @@ export const AppLayout: React.FC = () => {
|
|||||||
<div className="absolute top-1/2 -right-24 w-80 h-80 bg-indigo-600/10 rounded-full blur-3xl pointer-events-none" />
|
<div className="absolute top-1/2 -right-24 w-80 h-80 bg-indigo-600/10 rounded-full blur-3xl pointer-events-none" />
|
||||||
|
|
||||||
<div className="relative z-10">
|
<div className="relative z-10">
|
||||||
<Outlet />
|
<ErrorBoundary>
|
||||||
|
<Outlet />
|
||||||
|
</ErrorBoundary>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import React, { Component, ErrorInfo, ReactNode } from 'react';
|
||||||
|
import { AlertTriangle, RefreshCw, Home } from 'lucide-react';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children: ReactNode;
|
||||||
|
fallbackTitle?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
hasError: boolean;
|
||||||
|
error: Error | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ErrorBoundary extends React.Component<Props, State> {
|
||||||
|
declare props: Props;
|
||||||
|
declare state: State;
|
||||||
|
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
hasError: false,
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static getDerivedStateFromError(error: Error): State {
|
||||||
|
return { hasError: true, error };
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
||||||
|
console.error('[PFM Error Boundary Caught]', error, errorInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
public handleReset = () => {
|
||||||
|
(this as any).setState({ hasError: false, error: null });
|
||||||
|
};
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
if (this.state.hasError) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-[400px] flex items-center justify-center p-6">
|
||||||
|
<div className="max-w-md w-full bg-[#0f1523]/90 backdrop-blur-md rounded-2xl border border-rose-500/30 p-6 text-center shadow-2xl">
|
||||||
|
<div className="w-12 h-12 rounded-2xl bg-rose-500/10 border border-rose-500/20 text-rose-400 flex items-center justify-center mx-auto mb-4">
|
||||||
|
<AlertTriangle className="w-6 h-6" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-base font-bold text-white mb-2">
|
||||||
|
{this.props.fallbackTitle || 'Display Module Encountered an Issue'}
|
||||||
|
</h3>
|
||||||
|
<p className="text-xs text-slate-400 mb-6 leading-relaxed">
|
||||||
|
An unexpected display error occurred while rendering this view. You can reload the view or return to the executive dashboard.
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center justify-center gap-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={this.handleReset}
|
||||||
|
className="inline-flex items-center gap-1.5 px-4 py-2 rounded-xl text-xs font-semibold bg-slate-800 hover:bg-slate-700 text-white border border-slate-700 transition cursor-pointer"
|
||||||
|
>
|
||||||
|
<RefreshCw className="w-3.5 h-3.5 text-blue-400" />
|
||||||
|
<span>Reload View</span>
|
||||||
|
</button>
|
||||||
|
<a
|
||||||
|
href="/dashboard"
|
||||||
|
className="inline-flex items-center gap-1.5 px-4 py-2 rounded-xl text-xs font-semibold bg-blue-600 hover:bg-blue-500 text-white transition cursor-pointer shadow-md shadow-blue-500/20"
|
||||||
|
>
|
||||||
|
<Home className="w-3.5 h-3.5" />
|
||||||
|
<span>Dashboard</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.props.children;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { ApprovalStatus, Priority, ActivityStatus } from '../../types';
|
||||||
|
import { getApprovalStatusBadge, getPriorityBadge, getActivityStatusBadge } from '../../lib/formatters';
|
||||||
|
|
||||||
|
export const ApprovalStatusBadge: React.FC<{ status: ApprovalStatus | number | null | undefined }> = ({ status }) => {
|
||||||
|
const badge = getApprovalStatusBadge(status ?? ApprovalStatus.Pending);
|
||||||
|
return (
|
||||||
|
<span className={`inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full text-[11px] font-semibold border whitespace-nowrap ${badge.className}`}>
|
||||||
|
{badge.dot && <span className={`w-1.5 h-1.5 rounded-full ${badge.dot}`} />}
|
||||||
|
<span>{badge.label}</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PriorityBadge: React.FC<{ priority: Priority | number | null | undefined }> = ({ priority }) => {
|
||||||
|
const badge = getPriorityBadge(priority ?? Priority.Medium);
|
||||||
|
return (
|
||||||
|
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-[11px] font-semibold border whitespace-nowrap ${badge.className}`}>
|
||||||
|
<span>{badge.label}</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ActivityStatusBadge: React.FC<{ status: ActivityStatus | number | string | boolean | null | undefined }> = ({ status }) => {
|
||||||
|
const badge = getActivityStatusBadge(status);
|
||||||
|
return (
|
||||||
|
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-[11px] font-semibold border whitespace-nowrap ${badge.className}`}>
|
||||||
|
<span>{badge.label}</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -38,6 +38,7 @@ import { HelpTooltip } from '../components/ui/HelpTooltip';
|
|||||||
import { ExportButton } from '../components/ui/ExportButton';
|
import { ExportButton } from '../components/ui/ExportButton';
|
||||||
import { ExportColumn } from '../utils/exportUtils';
|
import { ExportColumn } from '../utils/exportUtils';
|
||||||
import { useTableSort, SortableTh } from '../utils/tableSort';
|
import { useTableSort, SortableTh } from '../utils/tableSort';
|
||||||
|
import { ApprovalStatusBadge } from '../components/ui/StatusBadge';
|
||||||
import { Beneficiary, ComplianceItem, Outcome, ApprovalStatus } from '../types';
|
import { Beneficiary, ComplianceItem, Outcome, ApprovalStatus } from '../types';
|
||||||
import { getApprovalStatusBadge, formatDate, APPROVAL_STATUS_OPTIONS } from '../lib/formatters';
|
import { getApprovalStatusBadge, formatDate, APPROVAL_STATUS_OPTIONS } from '../lib/formatters';
|
||||||
|
|
||||||
@@ -727,7 +728,9 @@ export const BeneficiariesPage: React.FC = () => {
|
|||||||
<td className="py-3.5 px-4 text-slate-300 max-w-xs truncate" title={c.requirements || ''}>
|
<td className="py-3.5 px-4 text-slate-300 max-w-xs truncate" title={c.requirements || ''}>
|
||||||
{c.requirements || 'None'}
|
{c.requirements || 'None'}
|
||||||
</td>
|
</td>
|
||||||
<td className="py-3.5 px-4 whitespace-nowrap">{getApprovalStatusBadge(c.status)}</td>
|
<td className="py-3.5 px-4 whitespace-nowrap">
|
||||||
|
<ApprovalStatusBadge status={c.status} />
|
||||||
|
</td>
|
||||||
<td className="py-3.5 px-4 whitespace-nowrap text-slate-400">{formatDate(c.createdAt)}</td>
|
<td className="py-3.5 px-4 whitespace-nowrap text-slate-400">{formatDate(c.createdAt)}</td>
|
||||||
<td className="py-3.5 px-5 text-right whitespace-nowrap">
|
<td className="py-3.5 px-5 text-right whitespace-nowrap">
|
||||||
<button
|
<button
|
||||||
@@ -946,7 +949,9 @@ export const BeneficiariesPage: React.FC = () => {
|
|||||||
<p className="font-semibold text-white">{ci.title}</p>
|
<p className="font-semibold text-white">{ci.title}</p>
|
||||||
<p className="text-[11px] text-slate-400 truncate max-w-sm">{ci.requirements}</p>
|
<p className="text-[11px] text-slate-400 truncate max-w-sm">{ci.requirements}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="shrink-0">{getApprovalStatusBadge(ci.status)}</div>
|
<div className="shrink-0">
|
||||||
|
<ApprovalStatusBadge status={ci.status} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import { useToast } from '../components/ui/Toast';
|
|||||||
import { ExportButton } from '../components/ui/ExportButton';
|
import { ExportButton } from '../components/ui/ExportButton';
|
||||||
import { ExportColumn } from '../utils/exportUtils';
|
import { ExportColumn } from '../utils/exportUtils';
|
||||||
import { useTableSort, SortableTh } from '../utils/tableSort';
|
import { useTableSort, SortableTh } from '../utils/tableSort';
|
||||||
|
import { ApprovalStatusBadge, PriorityBadge } from '../components/ui/StatusBadge';
|
||||||
import { Issue, ChangeRequest, ApprovalStatus, ActivityStatus, Priority } from '../types';
|
import { Issue, ChangeRequest, ApprovalStatus, ActivityStatus, Priority } from '../types';
|
||||||
import {
|
import {
|
||||||
getApprovalStatusBadge,
|
getApprovalStatusBadge,
|
||||||
@@ -484,7 +485,7 @@ export const ChangeManagementPage: React.FC = () => {
|
|||||||
<div className="flex items-center gap-1.5 text-xs text-slate-400">
|
<div className="flex items-center gap-1.5 text-xs text-slate-400">
|
||||||
<FolderKanban className="w-3.5 h-3.5 text-amber-400" />
|
<FolderKanban className="w-3.5 h-3.5 text-amber-400" />
|
||||||
<select
|
<select
|
||||||
value={selectedProjectId}
|
value={effectiveProjectId ? String(effectiveProjectId) : ''}
|
||||||
onChange={(e) => setSelectedProjectId(e.target.value)}
|
onChange={(e) => setSelectedProjectId(e.target.value)}
|
||||||
className="bg-slate-900 border border-slate-800 rounded-xl px-3 py-1.5 text-xs text-white focus:outline-none focus:border-amber-500 max-w-[180px] truncate"
|
className="bg-slate-900 border border-slate-800 rounded-xl px-3 py-1.5 text-xs text-white focus:outline-none focus:border-amber-500 max-w-[180px] truncate"
|
||||||
>
|
>
|
||||||
@@ -587,7 +588,9 @@ export const ChangeManagementPage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="py-3.5 px-4 whitespace-nowrap">{getPriorityBadge(iss.priority)}</td>
|
<td className="py-3.5 px-4 whitespace-nowrap">
|
||||||
|
<PriorityBadge priority={iss.priority} />
|
||||||
|
</td>
|
||||||
<td className="py-3.5 px-4 whitespace-nowrap text-slate-300">
|
<td className="py-3.5 px-4 whitespace-nowrap text-slate-300">
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<User className="w-3.5 h-3.5 text-slate-400" />
|
<User className="w-3.5 h-3.5 text-slate-400" />
|
||||||
@@ -678,7 +681,7 @@ export const ChangeManagementPage: React.FC = () => {
|
|||||||
<div className="flex items-center gap-1.5 text-xs text-slate-400">
|
<div className="flex items-center gap-1.5 text-xs text-slate-400">
|
||||||
<FolderKanban className="w-3.5 h-3.5 text-amber-400" />
|
<FolderKanban className="w-3.5 h-3.5 text-amber-400" />
|
||||||
<select
|
<select
|
||||||
value={selectedProjectId}
|
value={effectiveProjectId ? String(effectiveProjectId) : ''}
|
||||||
onChange={(e) => setSelectedProjectId(e.target.value)}
|
onChange={(e) => setSelectedProjectId(e.target.value)}
|
||||||
className="bg-slate-900 border border-slate-800 rounded-xl px-3 py-1.5 text-xs text-white focus:outline-none focus:border-amber-500 max-w-[180px] truncate"
|
className="bg-slate-900 border border-slate-800 rounded-xl px-3 py-1.5 text-xs text-white focus:outline-none focus:border-amber-500 max-w-[180px] truncate"
|
||||||
>
|
>
|
||||||
@@ -784,7 +787,9 @@ export const ChangeManagementPage: React.FC = () => {
|
|||||||
<td className="py-3.5 px-4 whitespace-nowrap font-mono font-semibold text-slate-200">
|
<td className="py-3.5 px-4 whitespace-nowrap font-mono font-semibold text-slate-200">
|
||||||
{formatZAR(cr.revisedBudget || 0)}
|
{formatZAR(cr.revisedBudget || 0)}
|
||||||
</td>
|
</td>
|
||||||
<td className="py-3.5 px-4 whitespace-nowrap">{getApprovalStatusBadge(cr.status)}</td>
|
<td className="py-3.5 px-4 whitespace-nowrap">
|
||||||
|
<ApprovalStatusBadge status={cr.status} />
|
||||||
|
</td>
|
||||||
<td className="py-3.5 px-5 text-right whitespace-nowrap space-x-2">
|
<td className="py-3.5 px-5 text-right whitespace-nowrap space-x-2">
|
||||||
{cr.status === ApprovalStatus.Approved ? (
|
{cr.status === ApprovalStatus.Approved ? (
|
||||||
<span className="text-xs font-semibold text-emerald-400 bg-emerald-500/10 px-2.5 py-1 rounded-lg border border-emerald-500/20">
|
<span className="text-xs font-semibold text-emerald-400 bg-emerald-500/10 px-2.5 py-1 rounded-lg border border-emerald-500/20">
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import { useToast } from '../components/ui/Toast';
|
|||||||
import { ExportButton } from '../components/ui/ExportButton';
|
import { ExportButton } from '../components/ui/ExportButton';
|
||||||
import { ExportColumn } from '../utils/exportUtils';
|
import { ExportColumn } from '../utils/exportUtils';
|
||||||
import { useTableSort, SortableTh } from '../utils/tableSort';
|
import { useTableSort, SortableTh } from '../utils/tableSort';
|
||||||
|
import { ApprovalStatusBadge } from '../components/ui/StatusBadge';
|
||||||
import { SiteVisit, ApprovalStatus } from '../types';
|
import { SiteVisit, ApprovalStatus } from '../types';
|
||||||
import {
|
import {
|
||||||
getApprovalStatusBadge,
|
getApprovalStatusBadge,
|
||||||
@@ -407,7 +408,9 @@ export const SiteVisitsPage: React.FC = () => {
|
|||||||
<td className="py-3.5 px-4 text-slate-300 max-w-sm truncate" title={v.findings || ''}>
|
<td className="py-3.5 px-4 text-slate-300 max-w-sm truncate" title={v.findings || ''}>
|
||||||
{v.findings || 'No findings recorded.'}
|
{v.findings || 'No findings recorded.'}
|
||||||
</td>
|
</td>
|
||||||
<td className="py-3.5 px-4 whitespace-nowrap">{getApprovalStatusBadge(v.status)}</td>
|
<td className="py-3.5 px-4 whitespace-nowrap">
|
||||||
|
<ApprovalStatusBadge status={v.status} />
|
||||||
|
</td>
|
||||||
<td className="py-3.5 px-5 text-right whitespace-nowrap space-x-2">
|
<td className="py-3.5 px-5 text-right whitespace-nowrap space-x-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -559,7 +562,9 @@ export const SiteVisitsPage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>{getApprovalStatusBadge(v.status)}</div>
|
<div>
|
||||||
|
<ApprovalStatusBadge status={v.status} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-slate-900/70 p-3.5 rounded-xl border border-slate-800/60 text-xs text-slate-300 leading-relaxed max-h-36 overflow-y-auto">
|
<div className="bg-slate-900/70 p-3.5 rounded-xl border border-slate-800/60 text-xs text-slate-300 leading-relaxed max-h-36 overflow-y-auto">
|
||||||
@@ -690,7 +695,9 @@ export const SiteVisitsPage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="text-[10px] uppercase font-semibold text-slate-400 block">Status</span>
|
<span className="text-[10px] uppercase font-semibold text-slate-400 block">Status</span>
|
||||||
<div className="mt-0.5">{getApprovalStatusBadge(selectedVisitForDetail.status)}</div>
|
<div className="mt-0.5">
|
||||||
|
<ApprovalStatusBadge status={selectedVisitForDetail.status} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="text-[10px] uppercase font-semibold text-slate-400 block">Inspector</span>
|
<span className="text-[10px] uppercase font-semibold text-slate-400 block">Inspector</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user