metrics adn insihigts

This commit is contained in:
IGNY8 VPS (Salman)
2025-12-15 06:51:14 +00:00
parent cff00f87ff
commit c61cf7c39f
21 changed files with 749 additions and 129 deletions

View File

@@ -9,6 +9,7 @@ import { useSectorStore } from '../../store/sectorStore';
import SiteAndSectorSelector from './SiteAndSectorSelector';
import { trackLoading } from './LoadingStateMonitor';
import { useErrorHandler } from '../../hooks/useErrorHandler';
import { WorkflowInsights, WorkflowInsight } from './WorkflowInsights';
interface PageHeaderProps {
title: string;
@@ -22,6 +23,7 @@ interface PageHeaderProps {
};
hideSiteSector?: boolean; // Hide site/sector selector and info for global pages
navigation?: ReactNode; // Module navigation tabs
workflowInsights?: WorkflowInsight[]; // Actionable insights for current page
}
export default function PageHeader({
@@ -33,6 +35,7 @@ export default function PageHeader({
badge,
hideSiteSector = false,
navigation,
workflowInsights,
}: PageHeaderProps) {
const { activeSite } = useSiteStore();
const { activeSector, loadSectorsForSite } = useSectorStore();
@@ -98,77 +101,87 @@ export default function PageHeader({
};
return (
<div className={`flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 ${className}`}>
{/* Left side: Title, badge, and site/sector info */}
<div className="flex-1">
<div className="flex items-center gap-3">
{badge && (
<div className={`flex items-center justify-center w-10 h-10 rounded-xl ${badgeColors[badge.color]} flex-shrink-0`}>
{badge.icon && typeof badge.icon === 'object' && 'type' in badge.icon
? React.cloneElement(badge.icon as React.ReactElement, { className: 'text-white size-5' })
: badge.icon}
<div className={`flex flex-col gap-3 ${className}`}>
{/* Main header row - single row with 3 sections */}
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
{/* Left side: Title, badge, and site/sector info */}
<div className="flex-shrink-0">
<div className="flex items-center gap-4">
{badge && (
<div className={`flex items-center justify-center w-10 h-10 rounded-xl ${badgeColors[badge.color]} flex-shrink-0`}>
{badge.icon && typeof badge.icon === 'object' && 'type' in badge.icon
? React.cloneElement(badge.icon as React.ReactElement, { className: 'text-white size-5' })
: badge.icon}
</div>
)}
<h2 className="text-2xl font-bold text-gray-800 dark:text-white/90">{title}</h2>
</div>
{!hideSiteSector && (
<div className="flex items-center gap-3 mt-1">
{lastUpdated && (
<>
<p className="text-sm text-gray-500 dark:text-gray-400">
Last updated: {lastUpdated.toLocaleTimeString()}
</p>
</>
)}
{activeSite && (
<>
{lastUpdated && <span className="text-sm text-gray-400 dark:text-gray-600"></span>}
<p className="text-sm font-medium text-gray-700 dark:text-gray-300">
Site: <span className="text-brand-600 dark:text-brand-400">{activeSite.name}</span>
</p>
</>
)}
{activeSector && (
<>
<span className="text-sm text-gray-400 dark:text-gray-600"></span>
<p className="text-sm font-medium text-gray-700 dark:text-gray-300">
Sector: <span className="text-brand-600 dark:text-brand-400">{activeSector.name}</span>
</p>
</>
)}
{!activeSector && activeSite && (
<>
<span className="text-sm text-gray-400 dark:text-gray-600"></span>
<p className="text-sm font-medium text-gray-700 dark:text-gray-300">
Sector: <span className="text-brand-600 dark:text-brand-400">All Sectors</span>
</p>
</>
)}
</div>
)}
<h2 className="text-2xl font-bold text-gray-800 dark:text-white/90">{title}</h2>
</div>
{!hideSiteSector && (
<div className="flex items-center gap-3 mt-1">
{lastUpdated && (
<>
<p className="text-sm text-gray-500 dark:text-gray-400">
Last updated: {lastUpdated.toLocaleTimeString()}
</p>
</>
)}
{activeSite && (
<>
{lastUpdated && <span className="text-sm text-gray-400 dark:text-gray-600"></span>}
<p className="text-sm font-medium text-gray-700 dark:text-gray-300">
Site: <span className="text-brand-600 dark:text-brand-400">{activeSite.name}</span>
</p>
</>
)}
{activeSector && (
<>
<span className="text-sm text-gray-400 dark:text-gray-600"></span>
<p className="text-sm font-medium text-gray-700 dark:text-gray-300">
Sector: <span className="text-brand-600 dark:text-brand-400">{activeSector.name}</span>
</p>
</>
)}
{!activeSector && activeSite && (
<>
<span className="text-sm text-gray-400 dark:text-gray-600"></span>
<p className="text-sm font-medium text-gray-700 dark:text-gray-300">
Sector: <span className="text-brand-600 dark:text-brand-400">All Sectors</span>
</p>
</>
)}
</div>
)}
{hideSiteSector && lastUpdated && (
<div className="flex items-center gap-3 mt-1">
<p className="text-sm text-gray-500 dark:text-gray-400">
Last updated: {lastUpdated.toLocaleTimeString()}
</p>
</div>
)}
</div>
{/* Right side: Navigation bar stacked above site/sector selector */}
<div className="flex flex-col items-end gap-3">
{navigation && <div>{navigation}</div>}
<div className="flex items-center gap-3">
{!hideSiteSector && <SiteAndSectorSelector />}
{showRefresh && onRefresh && (
<button
onClick={onRefresh}
className="px-4 py-2 text-sm font-medium text-brand-500 hover:text-brand-600 border border-brand-200 rounded-lg hover:bg-brand-50 dark:border-brand-800 dark:hover:bg-brand-500/10 transition-colors"
>
Refresh
</button>
{hideSiteSector && lastUpdated && (
<div className="flex items-center gap-3 mt-1">
<p className="text-sm text-gray-500 dark:text-gray-400">
Last updated: {lastUpdated.toLocaleTimeString()}
</p>
</div>
)}
</div>
{/* Middle: Workflow insights - takes available space */}
{workflowInsights && workflowInsights.length > 0 && (
<div className="flex-1 flex items-center justify-center px-6">
<WorkflowInsights insights={workflowInsights} />
</div>
)}
{/* Right side: Navigation bar stacked above site/sector selector */}
<div className="flex flex-col items-end gap-3 flex-shrink-0">
{navigation && <div>{navigation}</div>}
<div className="flex items-center gap-3">
{!hideSiteSector && <SiteAndSectorSelector />}
{showRefresh && onRefresh && (
<button
onClick={onRefresh}
className="px-4 py-2 text-sm font-medium text-brand-500 hover:text-brand-600 border border-brand-200 rounded-lg hover:bg-brand-50 dark:border-brand-800 dark:hover:bg-brand-500/10 transition-colors"
>
Refresh
</button>
)}
</div>
</div>
</div>
</div>
);

View File

@@ -0,0 +1,92 @@
/**
* WorkflowInsights Component
* Shows actionable insights in a collapsible notification box
* Placed between page title and navigation tabs
*/
import React, { useState } from 'react';
import { CheckCircleIcon, AlertIcon, InfoIcon, BoltIcon, CloseIcon } from '../../icons';
export interface WorkflowInsight {
type: 'success' | 'warning' | 'info' | 'action';
message: string;
}
interface WorkflowInsightsProps {
insights: WorkflowInsight[];
className?: string;
}
export const WorkflowInsights: React.FC<WorkflowInsightsProps> = ({ insights, className = '' }) => {
const [isCollapsed, setIsCollapsed] = useState(false);
if (!insights || insights.length === 0 || isCollapsed) return null;
// Determine notification color based on most critical insight type
const hasCritical = insights.some(i => i.type === 'warning' || i.type === 'action');
const hasSuccess = insights.some(i => i.type === 'success');
const notificationColors = hasCritical
? {
bg: 'bg-amber-50 dark:bg-amber-500/10',
border: 'border-amber-300 dark:border-amber-700',
iconBg: 'bg-amber-100 dark:bg-amber-500/20',
}
: hasSuccess
? {
bg: 'bg-green-50 dark:bg-green-500/10',
border: 'border-green-300 dark:border-green-700',
iconBg: 'bg-green-100 dark:bg-green-500/20',
}
: {
bg: 'bg-blue-50 dark:bg-blue-500/10',
border: 'border-blue-300 dark:border-blue-700',
iconBg: 'bg-blue-100 dark:bg-blue-500/20',
};
const getIcon = (type: string) => {
switch (type) {
case 'success':
return <CheckCircleIcon className="w-5 h-5 text-green-600 dark:text-green-400" />;
case 'warning':
return <AlertIcon className="w-5 h-5 text-amber-600 dark:text-amber-400" />;
case 'action':
return <BoltIcon className="w-5 h-5 text-blue-600 dark:text-blue-400" />;
default:
return <InfoIcon className="w-5 h-5 text-blue-600 dark:text-blue-400" />;
}
};
return (
<div className={`relative ${className}`}>
<div className={`flex items-start gap-3 px-4 py-3 rounded-lg border ${notificationColors.bg} ${notificationColors.border} shadow-sm`}>
{/* Icon */}
<div className={`flex-shrink-0 p-1.5 rounded-md ${notificationColors.iconBg}`}>
{getIcon(insights[0].type)}
</div>
{/* Content */}
<div className="flex-1 space-y-2">
{insights.map((insight, index) => (
<div key={index} className="flex items-start gap-2">
{insights.length > 1 && (
<span className="text-gray-400 dark:text-gray-500 text-base font-normal mt-0.5"></span>
)}
<p className="text-base font-normal text-gray-700 dark:text-gray-300">
{insight.message}
</p>
</div>
))}
</div>
{/* Close button */}
<button
onClick={() => setIsCollapsed(true)}
className="flex-shrink-0 p-1 rounded hover:bg-gray-200/50 dark:hover:bg-gray-700/50 transition-colors"
aria-label="Dismiss"
>
<CloseIcon className="w-4 h-4 text-gray-500 dark:text-gray-400" />
</button>
</div>
</div>
);
};