382 lines
15 KiB
TypeScript
382 lines
15 KiB
TypeScript
// ================================================================
|
|
// frontend/src/pages/app/NewDashboardPage.tsx
|
|
// ================================================================
|
|
/**
|
|
* JTBD-Aligned Dashboard Page
|
|
*
|
|
* Complete redesign based on Jobs To Be Done methodology.
|
|
* Focused on answering: "What requires my attention right now?"
|
|
*
|
|
* Key principles:
|
|
* - Automation-first (show what system did)
|
|
* - Action-oriented (prioritize tasks)
|
|
* - Progressive disclosure (show 20% that matters 80%)
|
|
* - Mobile-first (one-handed operation)
|
|
* - Trust-building (explain system reasoning)
|
|
*/
|
|
|
|
import React, { useState, useEffect } from 'react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import { RefreshCw, ExternalLink, Plus, Sparkles } from 'lucide-react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { useTenant } from '../../stores/tenant.store';
|
|
import {
|
|
useBakeryHealthStatus,
|
|
useOrchestrationSummary,
|
|
useActionQueue,
|
|
useProductionTimeline,
|
|
useInsights,
|
|
useApprovePurchaseOrder,
|
|
useStartProductionBatch,
|
|
usePauseProductionBatch,
|
|
} from '../../api/hooks/newDashboard';
|
|
import { useRejectPurchaseOrder } from '../../api/hooks/purchase-orders';
|
|
import { HealthStatusCard } from '../../components/dashboard/HealthStatusCard';
|
|
import { ActionQueueCard } from '../../components/dashboard/ActionQueueCard';
|
|
import { OrchestrationSummaryCard } from '../../components/dashboard/OrchestrationSummaryCard';
|
|
import { ProductionTimelineCard } from '../../components/dashboard/ProductionTimelineCard';
|
|
import { InsightsGrid } from '../../components/dashboard/InsightsGrid';
|
|
import { PurchaseOrderDetailsModal } from '../../components/dashboard/PurchaseOrderDetailsModal';
|
|
import { UnifiedAddWizard } from '../../components/domain/unified-wizard';
|
|
import type { ItemType } from '../../components/domain/unified-wizard';
|
|
import { useDemoTour, shouldStartTour, clearTourStartPending } from '../../features/demo-onboarding';
|
|
|
|
export function NewDashboardPage() {
|
|
const navigate = useNavigate();
|
|
const { t } = useTranslation(['dashboard', 'common']);
|
|
const { currentTenant } = useTenant();
|
|
const tenantId = currentTenant?.id || '';
|
|
const { startTour } = useDemoTour();
|
|
const isDemoMode = localStorage.getItem('demo_mode') === 'true';
|
|
|
|
// Unified Add Wizard state
|
|
const [isAddWizardOpen, setIsAddWizardOpen] = useState(false);
|
|
const [addWizardError, setAddWizardError] = useState<string | null>(null);
|
|
|
|
// PO Details Modal state
|
|
const [selectedPOId, setSelectedPOId] = useState<string | null>(null);
|
|
const [isPOModalOpen, setIsPOModalOpen] = useState(false);
|
|
|
|
// Data fetching
|
|
const {
|
|
data: healthStatus,
|
|
isLoading: healthLoading,
|
|
refetch: refetchHealth,
|
|
} = useBakeryHealthStatus(tenantId);
|
|
|
|
const {
|
|
data: orchestrationSummary,
|
|
isLoading: orchestrationLoading,
|
|
refetch: refetchOrchestration,
|
|
} = useOrchestrationSummary(tenantId);
|
|
|
|
const {
|
|
data: actionQueue,
|
|
isLoading: actionQueueLoading,
|
|
refetch: refetchActionQueue,
|
|
} = useActionQueue(tenantId);
|
|
|
|
const {
|
|
data: productionTimeline,
|
|
isLoading: timelineLoading,
|
|
refetch: refetchTimeline,
|
|
} = useProductionTimeline(tenantId);
|
|
|
|
const {
|
|
data: insights,
|
|
isLoading: insightsLoading,
|
|
refetch: refetchInsights,
|
|
} = useInsights(tenantId);
|
|
|
|
// Mutations
|
|
const approvePO = useApprovePurchaseOrder();
|
|
const rejectPO = useRejectPurchaseOrder();
|
|
const startBatch = useStartProductionBatch();
|
|
const pauseBatch = usePauseProductionBatch();
|
|
|
|
// Handlers
|
|
const handleApprove = async (actionId: string) => {
|
|
try {
|
|
await approvePO.mutateAsync({ tenantId, poId: actionId });
|
|
// Refetch to update UI
|
|
refetchActionQueue();
|
|
refetchHealth();
|
|
} catch (error) {
|
|
console.error('Error approving PO:', error);
|
|
}
|
|
};
|
|
|
|
const handleReject = async (actionId: string, reason: string) => {
|
|
try {
|
|
await rejectPO.mutateAsync({ tenantId, poId: actionId, reason });
|
|
// Refetch to update UI
|
|
refetchActionQueue();
|
|
refetchHealth();
|
|
} catch (error) {
|
|
console.error('Error rejecting PO:', error);
|
|
}
|
|
};
|
|
|
|
const handleViewDetails = (actionId: string) => {
|
|
// Open modal to show PO details
|
|
setSelectedPOId(actionId);
|
|
setIsPOModalOpen(true);
|
|
};
|
|
|
|
const handleModify = (actionId: string) => {
|
|
// Navigate to procurement page for modification
|
|
navigate(`/app/operations/procurement`);
|
|
};
|
|
|
|
const handleStartBatch = async (batchId: string) => {
|
|
try {
|
|
await startBatch.mutateAsync({ tenantId, batchId });
|
|
refetchTimeline();
|
|
refetchHealth();
|
|
} catch (error) {
|
|
console.error('Error starting batch:', error);
|
|
}
|
|
};
|
|
|
|
const handlePauseBatch = async (batchId: string) => {
|
|
try {
|
|
await pauseBatch.mutateAsync({ tenantId, batchId });
|
|
refetchTimeline();
|
|
refetchHealth();
|
|
} catch (error) {
|
|
console.error('Error pausing batch:', error);
|
|
}
|
|
};
|
|
|
|
const handleRefreshAll = () => {
|
|
refetchHealth();
|
|
refetchOrchestration();
|
|
refetchActionQueue();
|
|
refetchTimeline();
|
|
refetchInsights();
|
|
};
|
|
|
|
const handleAddWizardComplete = (itemType: ItemType, data?: any) => {
|
|
console.log('Item created:', itemType, data);
|
|
// Refetch relevant data based on what was added
|
|
handleRefreshAll();
|
|
};
|
|
|
|
// Keyboard shortcut for Quick Add (Cmd/Ctrl + K)
|
|
useEffect(() => {
|
|
const handleKeyDown = (event: KeyboardEvent) => {
|
|
// Check for Cmd+K (Mac) or Ctrl+K (Windows/Linux)
|
|
if ((event.metaKey || event.ctrlKey) && event.key === 'k') {
|
|
event.preventDefault();
|
|
setIsAddWizardOpen(true);
|
|
}
|
|
};
|
|
|
|
window.addEventListener('keydown', handleKeyDown);
|
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
}, []);
|
|
|
|
// Demo tour auto-start logic
|
|
useEffect(() => {
|
|
console.log('[Dashboard] Demo mode:', isDemoMode);
|
|
console.log('[Dashboard] Should start tour:', shouldStartTour());
|
|
console.log('[Dashboard] SessionStorage demo_tour_should_start:', sessionStorage.getItem('demo_tour_should_start'));
|
|
console.log('[Dashboard] SessionStorage demo_tour_start_step:', sessionStorage.getItem('demo_tour_start_step'));
|
|
|
|
// Check if there's a tour intent from redirection (higher priority)
|
|
const shouldStartFromRedirect = sessionStorage.getItem('demo_tour_should_start') === 'true';
|
|
const redirectStartStep = parseInt(sessionStorage.getItem('demo_tour_start_step') || '0', 10);
|
|
|
|
if (isDemoMode && (shouldStartTour() || shouldStartFromRedirect)) {
|
|
console.log('[Dashboard] Starting tour in 1.5s...');
|
|
const timer = setTimeout(() => {
|
|
console.log('[Dashboard] Executing startTour()');
|
|
if (shouldStartFromRedirect) {
|
|
// Start tour from the specific step that was intended
|
|
startTour(redirectStartStep);
|
|
// Clear the redirect intent
|
|
sessionStorage.removeItem('demo_tour_should_start');
|
|
sessionStorage.removeItem('demo_tour_start_step');
|
|
} else {
|
|
// Start tour normally (from beginning or resume)
|
|
startTour();
|
|
clearTourStartPending();
|
|
}
|
|
}, 1500);
|
|
|
|
return () => clearTimeout(timer);
|
|
}
|
|
}, [isDemoMode, startTour]);
|
|
|
|
return (
|
|
<div className="min-h-screen pb-20 md:pb-8" style={{ backgroundColor: 'var(--bg-secondary)' }}>
|
|
{/* Mobile-optimized container */}
|
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between mb-6">
|
|
<div>
|
|
<h1 className="text-3xl md:text-4xl font-bold" style={{ color: 'var(--text-primary)' }}>{t('dashboard:title')}</h1>
|
|
<p className="mt-1" style={{ color: 'var(--text-secondary)' }}>{t('dashboard:subtitle')}</p>
|
|
</div>
|
|
|
|
{/* Action Buttons */}
|
|
<div className="flex items-center gap-3">
|
|
<button
|
|
onClick={handleRefreshAll}
|
|
className="flex items-center gap-2 px-4 py-2 rounded-lg font-semibold transition-colors duration-200"
|
|
style={{
|
|
backgroundColor: 'var(--bg-primary)',
|
|
borderColor: 'var(--border-primary)',
|
|
border: '1px solid',
|
|
color: 'var(--text-secondary)'
|
|
}}
|
|
>
|
|
<RefreshCw className="w-5 h-5" />
|
|
<span className="hidden sm:inline">{t('common:actions.refresh')}</span>
|
|
</button>
|
|
|
|
{/* Unified Add Button with Keyboard Shortcut */}
|
|
<button
|
|
onClick={() => setIsAddWizardOpen(true)}
|
|
className="group relative flex items-center gap-2 px-6 py-2.5 rounded-lg font-semibold transition-all duration-200 shadow-lg hover:shadow-xl hover:-translate-y-0.5 active:translate-y-0"
|
|
style={{
|
|
background: 'linear-gradient(135deg, var(--color-primary) 0%, var(--color-primary-dark) 100%)',
|
|
color: 'white'
|
|
}}
|
|
title={`Quick Add (${navigator.platform.includes('Mac') ? 'Cmd' : 'Ctrl'}+K)`}
|
|
>
|
|
<Plus className="w-5 h-5" />
|
|
<span className="hidden sm:inline">{t('common:actions.add')}</span>
|
|
<Sparkles className="w-4 h-4 opacity-80" />
|
|
{/* Keyboard shortcut badge - shown on hover */}
|
|
<span className="hidden lg:flex absolute -bottom-8 left-1/2 -translate-x-1/2 items-center gap-1 px-2 py-1 rounded text-xs font-mono opacity-0 group-hover:opacity-100 transition-opacity duration-200 whitespace-nowrap pointer-events-none" style={{ backgroundColor: 'var(--bg-primary)', color: 'var(--text-secondary)', boxShadow: '0 2px 8px rgba(0,0,0,0.1)' }}>
|
|
<kbd className="px-1.5 py-0.5 rounded text-xs font-semibold" style={{ backgroundColor: 'var(--bg-tertiary)', border: '1px solid var(--border-secondary)' }}>
|
|
{navigator.platform.includes('Mac') ? '⌘' : 'Ctrl'}
|
|
</kbd>
|
|
<span>+</span>
|
|
<kbd className="px-1.5 py-0.5 rounded text-xs font-semibold" style={{ backgroundColor: 'var(--bg-tertiary)', border: '1px solid var(--border-secondary)' }}>
|
|
K
|
|
</kbd>
|
|
</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Main Dashboard Layout */}
|
|
<div className="space-y-6">
|
|
{/* SECTION 1: Bakery Health Status */}
|
|
<div data-tour="dashboard-stats">
|
|
<HealthStatusCard healthStatus={healthStatus} loading={healthLoading} />
|
|
</div>
|
|
|
|
{/* SECTION 2: What Needs Your Attention (Action Queue) */}
|
|
<div data-tour="pending-po-approvals">
|
|
<ActionQueueCard
|
|
actionQueue={actionQueue}
|
|
loading={actionQueueLoading}
|
|
onApprove={handleApprove}
|
|
onReject={handleReject}
|
|
onViewDetails={handleViewDetails}
|
|
onModify={handleModify}
|
|
tenantId={tenantId}
|
|
/>
|
|
</div>
|
|
|
|
{/* SECTION 3: What the System Did for You (Orchestration Summary) */}
|
|
<div data-tour="real-time-alerts">
|
|
<OrchestrationSummaryCard
|
|
summary={orchestrationSummary}
|
|
loading={orchestrationLoading}
|
|
/>
|
|
</div>
|
|
|
|
{/* SECTION 4: Today's Production Timeline */}
|
|
<div data-tour="today-production">
|
|
<ProductionTimelineCard
|
|
timeline={productionTimeline}
|
|
loading={timelineLoading}
|
|
onStart={handleStartBatch}
|
|
onPause={handlePauseBatch}
|
|
/>
|
|
</div>
|
|
|
|
{/* SECTION 5: Quick Insights Grid */}
|
|
<div>
|
|
<h2 className="text-2xl font-bold mb-4" style={{ color: 'var(--text-primary)' }}>{t('dashboard:sections.key_metrics')}</h2>
|
|
<InsightsGrid insights={insights} loading={insightsLoading} />
|
|
</div>
|
|
|
|
{/* SECTION 6: Quick Action Links */}
|
|
<div className="rounded-xl shadow-md p-6" style={{ backgroundColor: 'var(--bg-primary)' }}>
|
|
<h2 className="text-xl font-bold mb-4" style={{ color: 'var(--text-primary)' }}>{t('dashboard:sections.quick_actions')}</h2>
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
<button
|
|
onClick={() => navigate('/app/operations/procurement')}
|
|
className="flex items-center justify-between p-4 rounded-lg transition-colors duration-200 group"
|
|
style={{ backgroundColor: 'var(--bg-tertiary)', borderLeft: '4px solid var(--color-info)' }}
|
|
>
|
|
<span className="font-semibold" style={{ color: 'var(--text-primary)' }}>{t('dashboard:quick_actions.view_orders')}</span>
|
|
<ExternalLink className="w-5 h-5 group-hover:translate-x-1 transition-transform duration-200" style={{ color: 'var(--color-info)' }} />
|
|
</button>
|
|
|
|
<button
|
|
onClick={() => navigate('/app/operations/production')}
|
|
className="flex items-center justify-between p-4 rounded-lg transition-colors duration-200 group"
|
|
style={{ backgroundColor: 'var(--bg-tertiary)', borderLeft: '4px solid var(--color-success)' }}
|
|
>
|
|
<span className="font-semibold" style={{ color: 'var(--text-primary)' }}>{t('dashboard:quick_actions.view_production')}</span>
|
|
<ExternalLink className="w-5 h-5 group-hover:translate-x-1 transition-transform duration-200" style={{ color: 'var(--color-success)' }} />
|
|
</button>
|
|
|
|
<button
|
|
onClick={() => navigate('/app/database/inventory')}
|
|
className="flex items-center justify-between p-4 rounded-lg transition-colors duration-200 group"
|
|
style={{ backgroundColor: 'var(--bg-tertiary)', borderLeft: '4px solid var(--color-secondary)' }}
|
|
>
|
|
<span className="font-semibold" style={{ color: 'var(--text-primary)' }}>{t('dashboard:quick_actions.view_inventory')}</span>
|
|
<ExternalLink className="w-5 h-5 group-hover:translate-x-1 transition-transform duration-200" style={{ color: 'var(--color-secondary)' }} />
|
|
</button>
|
|
|
|
<button
|
|
onClick={() => navigate('/app/database/suppliers')}
|
|
className="flex items-center justify-between p-4 rounded-lg transition-colors duration-200 group"
|
|
style={{ backgroundColor: 'var(--bg-tertiary)', borderLeft: '4px solid var(--color-warning)' }}
|
|
>
|
|
<span className="font-semibold" style={{ color: 'var(--text-primary)' }}>{t('dashboard:quick_actions.view_suppliers')}</span>
|
|
<ExternalLink className="w-5 h-5 group-hover:translate-x-1 transition-transform duration-200" style={{ color: 'var(--color-warning)' }} />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Mobile-friendly bottom padding */}
|
|
<div className="h-20 md:hidden"></div>
|
|
|
|
{/* Unified Add Wizard */}
|
|
<UnifiedAddWizard
|
|
isOpen={isAddWizardOpen}
|
|
onClose={() => setIsAddWizardOpen(false)}
|
|
onComplete={handleAddWizardComplete}
|
|
/>
|
|
|
|
{/* Purchase Order Details Modal */}
|
|
{selectedPOId && (
|
|
<PurchaseOrderDetailsModal
|
|
poId={selectedPOId}
|
|
tenantId={tenantId}
|
|
isOpen={isPOModalOpen}
|
|
onClose={() => {
|
|
setIsPOModalOpen(false);
|
|
setSelectedPOId(null);
|
|
}}
|
|
onApprove={handleApprove}
|
|
onModify={handleModify}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default NewDashboardPage;
|