Fixed dshboard render crash
This commit is contained in:
@@ -63,6 +63,7 @@ import {
|
|||||||
EvidenceType,
|
EvidenceType,
|
||||||
Notification,
|
Notification,
|
||||||
NotificationStatus,
|
NotificationStatus,
|
||||||
|
NotificationPlatform,
|
||||||
} from '../../types';
|
} from '../../types';
|
||||||
import { formatZAR, formatCompactZAR, getUserEmail } from '../../lib/formatters';
|
import { formatZAR, formatCompactZAR, getUserEmail } from '../../lib/formatters';
|
||||||
import { MessageViewer } from '../notifications/MessageViewer';
|
import { MessageViewer } from '../notifications/MessageViewer';
|
||||||
@@ -462,7 +463,7 @@ export function DashboardPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getMessageRecipientEmail = (notification: Notification | null): string => {
|
const getMessageRecipientEmail = (notification: Notification | null): string => {
|
||||||
if (!notification) return user?.profile?.email || (user as any)?.email || 'Partner User';
|
if (!notification) return (user?.email || (user as any)?.profile?.email || '') || (user as any)?.email || 'Partner User';
|
||||||
const dest = notification.destination || '';
|
const dest = notification.destination || '';
|
||||||
let rawTo = '';
|
let rawTo = '';
|
||||||
if (dest.includes('To:')) {
|
if (dest.includes('To:')) {
|
||||||
@@ -486,13 +487,13 @@ export function DashboardPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return user?.profile?.email || (user as any)?.email || 'Partner User';
|
return (user?.email || (user as any)?.profile?.email || '') || (user as any)?.email || 'Partner User';
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleReplyToMessage = (notification: Notification) => {
|
const handleReplyToMessage = (notification: Notification) => {
|
||||||
const senderEmail = getMessageSenderEmail(notification).trim().toLowerCase();
|
const senderEmail = getMessageSenderEmail(notification).trim().toLowerCase();
|
||||||
const currentUserEmail = (
|
const currentUserEmail = (
|
||||||
user?.profile?.email ||
|
(user?.email || (user as any)?.profile?.email || '') ||
|
||||||
(user as any)?.email ||
|
(user as any)?.email ||
|
||||||
''
|
''
|
||||||
).trim().toLowerCase();
|
).trim().toLowerCase();
|
||||||
@@ -746,7 +747,7 @@ export function DashboardPage() {
|
|||||||
className="p-3 hover:bg-white/[0.03] transition-colors cursor-pointer text-left"
|
className="p-3 hover:bg-white/[0.03] transition-colors cursor-pointer text-left"
|
||||||
>
|
>
|
||||||
<p className="text-xs font-semibold text-slate-100 truncate">{item.subject}</p>
|
<p className="text-xs font-semibold text-slate-100 truncate">{item.subject}</p>
|
||||||
<p className="text-[11px] text-slate-400 line-clamp-2 mt-0.5">{stripMarkdown(item.message)}</p>
|
<p className="text-[11px] text-slate-400 line-clamp-2 mt-0.5">{stripMarkdown(item.message || '')}</p>
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
@@ -778,10 +779,10 @@ export function DashboardPage() {
|
|||||||
className="flex items-center gap-2 p-1.5 pl-2 rounded-xl bg-slate-900/80 border border-white/[0.06] hover:border-white/[0.12] transition-colors cursor-pointer"
|
className="flex items-center gap-2 p-1.5 pl-2 rounded-xl bg-slate-900/80 border border-white/[0.06] hover:border-white/[0.12] transition-colors cursor-pointer"
|
||||||
>
|
>
|
||||||
<div className="w-6 h-6 rounded-lg bg-gradient-to-tr from-emerald-600 via-teal-600 to-cyan-600 flex items-center justify-center text-white text-[11px] font-bold">
|
<div className="w-6 h-6 rounded-lg bg-gradient-to-tr from-emerald-600 via-teal-600 to-cyan-600 flex items-center justify-center text-white text-[11px] font-bold">
|
||||||
{getInitials(user?.profile?.name || user?.profile?.email || 'Partner')}
|
{getInitials((user?.name || (user as any)?.profile?.name || '') || (user?.email || (user as any)?.profile?.email || '') || 'Partner')}
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xs font-medium text-slate-100 hidden md:inline max-w-[120px] truncate">
|
<span className="text-xs font-medium text-slate-100 hidden md:inline max-w-[120px] truncate">
|
||||||
{user?.profile?.name || 'Partner Account'}
|
{(user?.name || (user as any)?.profile?.name || '') || 'Partner Account'}
|
||||||
</span>
|
</span>
|
||||||
<ChevronDown className="w-3.5 h-3.5 text-slate-400" />
|
<ChevronDown className="w-3.5 h-3.5 text-slate-400" />
|
||||||
</button>
|
</button>
|
||||||
@@ -789,8 +790,8 @@ export function DashboardPage() {
|
|||||||
{showUserDropdown && (
|
{showUserDropdown && (
|
||||||
<div className="absolute right-0 mt-2 w-56 rounded-2xl bg-slate-900/95 border border-white/10 shadow-2xl backdrop-blur-xl p-2 z-50 text-xs">
|
<div className="absolute right-0 mt-2 w-56 rounded-2xl bg-slate-900/95 border border-white/10 shadow-2xl backdrop-blur-xl p-2 z-50 text-xs">
|
||||||
<div className="px-3 py-2 border-b border-white/[0.06]">
|
<div className="px-3 py-2 border-b border-white/[0.06]">
|
||||||
<p className="font-semibold text-slate-100 truncate">{user?.profile?.name || 'Partner User'}</p>
|
<p className="font-semibold text-slate-100 truncate">{(user?.name || (user as any)?.profile?.name || '') || 'Partner User'}</p>
|
||||||
<p className="text-[11px] text-slate-400 truncate">{user?.profile?.email || ''}</p>
|
<p className="text-[11px] text-slate-400 truncate">{(user?.email || (user as any)?.profile?.email || '') || ''}</p>
|
||||||
{partnerSummary.primaryInvitedOrg && (
|
{partnerSummary.primaryInvitedOrg && (
|
||||||
<p className="text-[10px] text-emerald-400 mt-1 truncate">
|
<p className="text-[10px] text-emerald-400 mt-1 truncate">
|
||||||
{partnerSummary.primaryInvitedOrg.name}
|
{partnerSummary.primaryInvitedOrg.name}
|
||||||
|
|||||||
@@ -58,42 +58,28 @@ export const GovernanceInfoModal: React.FC<GovernanceInfoModalProps> = ({
|
|||||||
// 3. Fetch rules
|
// 3. Fetch rules
|
||||||
const { data: governanceRules = [], isLoading: isLoadingRules } = useQuery<Rule[]>({
|
const { data: governanceRules = [], isLoading: isLoadingRules } = useQuery<Rule[]>({
|
||||||
queryKey: ['governance-rules'],
|
queryKey: ['governance-rules'],
|
||||||
queryFn: () => getRules(1, 100),
|
queryFn: () => getRules({ page: 1, pageSize: 100 }),
|
||||||
enabled: isOpen,
|
enabled: isOpen,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!isOpen || typeof document === 'undefined') return null;
|
if (!isOpen || typeof document === 'undefined') return null;
|
||||||
|
|
||||||
const renderComplianceStatus = (status?: number) => {
|
const renderComplianceStatus = (status?: number) => {
|
||||||
switch (status) {
|
const s = Number(status);
|
||||||
case ComplianceStatus.Compliant:
|
if (s === ComplianceStatus.Met || s === ComplianceStatus.Exceeded) {
|
||||||
return (
|
return (
|
||||||
<span className="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-[10px] font-bold bg-emerald-500/15 text-emerald-300 border border-emerald-500/30">
|
<span className="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-[10px] font-bold bg-emerald-500/15 text-emerald-300 border border-emerald-500/30">
|
||||||
<CheckCircle2 className="w-3 h-3 text-emerald-400" />
|
<CheckCircle2 className="w-3 h-3 text-emerald-400" />
|
||||||
<span>Compliant</span>
|
<span>{s === ComplianceStatus.Exceeded ? 'Exceeded' : 'Compliant'}</span>
|
||||||
</span>
|
|
||||||
);
|
|
||||||
case ComplianceStatus.UnderReview:
|
|
||||||
return (
|
|
||||||
<span className="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-[10px] font-bold bg-amber-500/15 text-amber-300 border border-amber-500/30">
|
|
||||||
<Clock className="w-3 h-3 text-amber-400" />
|
|
||||||
<span>Under Review</span>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
case ComplianceStatus.NonCompliant:
|
|
||||||
return (
|
|
||||||
<span className="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-[10px] font-bold bg-rose-500/15 text-rose-300 border border-rose-500/30">
|
|
||||||
<AlertTriangle className="w-3 h-3 text-rose-400" />
|
|
||||||
<span>Non-Compliant</span>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
default:
|
|
||||||
return (
|
|
||||||
<span className="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-[10px] font-bold bg-slate-800 text-slate-300 border border-white/10">
|
|
||||||
<span>Pending Submission</span>
|
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
return (
|
||||||
|
<span className="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-[10px] font-bold bg-rose-500/15 text-rose-300 border border-rose-500/30">
|
||||||
|
<AlertTriangle className="w-3 h-3 text-rose-400" />
|
||||||
|
<span>Action Required</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return createPortal(
|
return createPortal(
|
||||||
@@ -201,20 +187,13 @@ export const GovernanceInfoModal: React.FC<GovernanceInfoModalProps> = ({
|
|||||||
</span>
|
</span>
|
||||||
{renderComplianceStatus(item.status)}
|
{renderComplianceStatus(item.status)}
|
||||||
</div>
|
</div>
|
||||||
{item.dueDate && (
|
|
||||||
<div className="text-[11px] text-slate-400 flex items-center gap-1 font-mono">
|
<div className="text-[11px] text-slate-400 flex items-center gap-1 font-mono">
|
||||||
<Calendar className="w-3.5 h-3.5 text-slate-500" />
|
<Calendar className="w-3.5 h-3.5 text-slate-500" />
|
||||||
<span>Due: {formatDate(item.dueDate)}</span>
|
<span>Created: {formatDate(item.createdAt)}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{item.description && (
|
|
||||||
<p className="text-xs text-slate-300 leading-relaxed">{item.description}</p>
|
|
||||||
)}
|
|
||||||
{item.notes && (
|
|
||||||
<div className="text-[11px] text-emerald-400/90 bg-emerald-950/40 border border-emerald-500/20 p-2.5 rounded-xl font-mono">
|
|
||||||
Note: {item.notes}
|
|
||||||
</div>
|
</div>
|
||||||
|
{item.requirements && (
|
||||||
|
<p className="text-xs text-slate-300 leading-relaxed">{item.requirements}</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
@@ -291,20 +270,13 @@ export const GovernanceInfoModal: React.FC<GovernanceInfoModalProps> = ({
|
|||||||
<h4 className="font-semibold text-sm text-slate-100">
|
<h4 className="font-semibold text-sm text-slate-100">
|
||||||
{rule.name || `Rule #${idx + 1}`}
|
{rule.name || `Rule #${idx + 1}`}
|
||||||
</h4>
|
</h4>
|
||||||
{rule.category && (
|
|
||||||
<span className="text-[10px] font-mono px-2 py-0.5 rounded-md bg-slate-800 text-slate-300 border border-white/10">
|
<span className="text-[10px] font-mono px-2 py-0.5 rounded-md bg-slate-800 text-slate-300 border border-white/10">
|
||||||
{rule.category}
|
v{rule.version || 1}
|
||||||
</span>
|
</span>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
{rule.description && (
|
{rule.description && (
|
||||||
<p className="text-xs text-slate-300 leading-relaxed">{rule.description}</p>
|
<p className="text-xs text-slate-300 leading-relaxed">{rule.description}</p>
|
||||||
)}
|
)}
|
||||||
{rule.condition && (
|
|
||||||
<div className="text-[11px] text-slate-400 bg-slate-900 border border-white/5 p-2 rounded-xl font-mono">
|
|
||||||
Condition: {rule.condition}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -27,6 +27,9 @@ import { EvidenceUploadModal } from './EvidenceUploadModal';
|
|||||||
import { GovernanceInfoModal } from './GovernanceInfoModal';
|
import { GovernanceInfoModal } from './GovernanceInfoModal';
|
||||||
import {
|
import {
|
||||||
FolderKanban,
|
FolderKanban,
|
||||||
|
AlertTriangle,
|
||||||
|
ShieldAlert,
|
||||||
|
Scale,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
ChevronUp,
|
ChevronUp,
|
||||||
Calendar,
|
Calendar,
|
||||||
@@ -66,6 +69,12 @@ interface ProjectCardProps {
|
|||||||
item: EnrichedPartnerProject;
|
item: EnrichedPartnerProject;
|
||||||
isExpanded?: boolean;
|
isExpanded?: boolean;
|
||||||
onToggleExpand?: () => void;
|
onToggleExpand?: () => void;
|
||||||
|
onUploadEvidence?: (target: any) => void;
|
||||||
|
onSubmitActivityForApproval?: (
|
||||||
|
activity: Activity,
|
||||||
|
milestone?: Milestone | null,
|
||||||
|
evidences?: Evidence[]
|
||||||
|
) => Promise<void>;
|
||||||
onOpenSecureFile: (type: 'contract' | 'evidence', id: string | number) => void;
|
onOpenSecureFile: (type: 'contract' | 'evidence', id: string | number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -226,6 +235,19 @@ export const ProjectCard: React.FC<ProjectCardProps> = ({
|
|||||||
return new Date(a.dueAt || 0).getTime() - new Date(b.dueAt || 0).getTime();
|
return new Date(a.dueAt || 0).getTime() - new Date(b.dueAt || 0).getTime();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Requirement #5: Milestone and Project Late / At Risk calculations
|
||||||
|
const nowTime = Date.now();
|
||||||
|
const hasOverdueMilestone = effectiveMilestones.some(m => {
|
||||||
|
const d = m.dueAt ? new Date(m.dueAt).getTime() : null;
|
||||||
|
const fulfilled = Number(m.status) === ApprovalStatus.Approved;
|
||||||
|
return d !== null && d < nowTime && !fulfilled;
|
||||||
|
});
|
||||||
|
const hasAtRiskMilestone = !hasOverdueMilestone && effectiveMilestones.some(m => {
|
||||||
|
const d = m.dueAt ? new Date(m.dueAt).getTime() : null;
|
||||||
|
const fulfilled = Number(m.status) === ApprovalStatus.Approved;
|
||||||
|
return d !== null && d >= nowTime && (d - nowTime <= 3 * 24 * 60 * 60 * 1000) && !fulfilled;
|
||||||
|
});
|
||||||
|
|
||||||
const getEvidenceForActivity = (activityId: string | number): Evidence[] => {
|
const getEvidenceForActivity = (activityId: string | number): Evidence[] => {
|
||||||
const targetNum = toIntegerId(activityId);
|
const targetNum = toIntegerId(activityId);
|
||||||
return effectiveEvidence
|
return effectiveEvidence
|
||||||
|
|||||||
+1
-1
@@ -254,7 +254,7 @@ export const PartnerMessageComposerModal: React.FC<PartnerMessageComposerModalPr
|
|||||||
ref={editorRef}
|
ref={editorRef}
|
||||||
contentEditable
|
contentEditable
|
||||||
className="w-full min-h-[160px] max-h-[300px] p-4 text-xs sm:text-sm text-slate-100 leading-relaxed overflow-y-auto focus:outline-none [scrollbar-width:thin] [scrollbar-color:#334155_transparent] prose prose-invert max-w-none [&_blockquote]:border-l-4 [&_blockquote]:border-emerald-500 [&_blockquote]:pl-3 [&_blockquote]:italic [&_ul]:list-disc [&_ul]:pl-5 [&_ol]:list-decimal [&_ol]:pl-5"
|
className="w-full min-h-[160px] max-h-[300px] p-4 text-xs sm:text-sm text-slate-100 leading-relaxed overflow-y-auto focus:outline-none [scrollbar-width:thin] [scrollbar-color:#334155_transparent] prose prose-invert max-w-none [&_blockquote]:border-l-4 [&_blockquote]:border-emerald-500 [&_blockquote]:pl-3 [&_blockquote]:italic [&_ul]:list-disc [&_ul]:pl-5 [&_ol]:list-decimal [&_ol]:pl-5"
|
||||||
placeholder="Write your message here..."
|
data-placeholder="Write your message here..."
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -300,7 +300,7 @@ export function useInAppNotifications() {
|
|||||||
const sentItem: Notification = {
|
const sentItem: Notification = {
|
||||||
id: `sent_${Date.now()}`,
|
id: `sent_${Date.now()}`,
|
||||||
organisationId: orgId,
|
organisationId: orgId,
|
||||||
recipient: payload.recipientId,
|
recipient: payload.recipientId || payload.recipient || "",
|
||||||
platform: targetPlatform,
|
platform: targetPlatform,
|
||||||
status: NotificationStatus.Sent,
|
status: NotificationStatus.Sent,
|
||||||
subject: payload.subject,
|
subject: payload.subject,
|
||||||
|
|||||||
Reference in New Issue
Block a user