2025-10-12 18:47:33 +02:00
|
|
|
import React, { useEffect } from 'react';
|
2025-09-22 11:04:03 +02:00
|
|
|
import { useNavigate } from 'react-router-dom';
|
|
|
|
|
import { useTranslation } from 'react-i18next';
|
2025-08-28 10:41:04 +02:00
|
|
|
import { PageHeader } from '../../components/layout';
|
2025-09-19 16:17:04 +02:00
|
|
|
import StatsGrid from '../../components/ui/Stats/StatsGrid';
|
|
|
|
|
import RealTimeAlerts from '../../components/domain/dashboard/RealTimeAlerts';
|
2025-10-21 19:50:07 +02:00
|
|
|
import PendingPOApprovals from '../../components/domain/dashboard/PendingPOApprovals';
|
|
|
|
|
import TodayProduction from '../../components/domain/dashboard/TodayProduction';
|
2025-09-22 11:04:03 +02:00
|
|
|
import { useTenant } from '../../stores/tenant.store';
|
2025-10-12 18:47:33 +02:00
|
|
|
import { useDemoTour, shouldStartTour, clearTourStartPending } from '../../features/demo-onboarding';
|
2025-10-21 19:50:07 +02:00
|
|
|
import { useDashboardStats } from '../../api/hooks/dashboard';
|
2025-09-19 16:17:04 +02:00
|
|
|
import {
|
|
|
|
|
AlertTriangle,
|
|
|
|
|
Clock,
|
2025-09-22 16:10:08 +02:00
|
|
|
Euro,
|
2025-10-21 19:50:07 +02:00
|
|
|
Package
|
2025-09-19 16:17:04 +02:00
|
|
|
} from 'lucide-react';
|
2025-08-28 10:41:04 +02:00
|
|
|
|
|
|
|
|
const DashboardPage: React.FC = () => {
|
2025-09-22 11:04:03 +02:00
|
|
|
const { t } = useTranslation();
|
|
|
|
|
const navigate = useNavigate();
|
2025-10-21 19:50:07 +02:00
|
|
|
const { availableTenants, currentTenant } = useTenant();
|
2025-10-12 18:47:33 +02:00
|
|
|
const { startTour } = useDemoTour();
|
|
|
|
|
const isDemoMode = localStorage.getItem('demo_mode') === 'true';
|
|
|
|
|
|
2025-10-21 19:50:07 +02:00
|
|
|
// Fetch real dashboard statistics
|
|
|
|
|
const { data: dashboardStats, isLoading: isLoadingStats, error: statsError } = useDashboardStats(
|
|
|
|
|
currentTenant?.id || '',
|
|
|
|
|
{
|
|
|
|
|
enabled: !!currentTenant?.id,
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
2025-10-12 18:47:33 +02:00
|
|
|
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'));
|
|
|
|
|
|
|
|
|
|
if (isDemoMode && shouldStartTour()) {
|
|
|
|
|
console.log('[Dashboard] Starting tour in 1.5s...');
|
|
|
|
|
const timer = setTimeout(() => {
|
|
|
|
|
console.log('[Dashboard] Executing startTour()');
|
|
|
|
|
startTour();
|
|
|
|
|
clearTourStartPending();
|
|
|
|
|
}, 1500);
|
|
|
|
|
|
|
|
|
|
return () => clearTimeout(timer);
|
|
|
|
|
}
|
|
|
|
|
}, [isDemoMode, startTour]);
|
2025-09-22 11:04:03 +02:00
|
|
|
|
2025-10-21 19:50:07 +02:00
|
|
|
const handleViewAllProcurement = () => {
|
|
|
|
|
navigate('/app/operations/procurement');
|
2025-09-22 11:04:03 +02:00
|
|
|
};
|
|
|
|
|
|
2025-10-21 19:50:07 +02:00
|
|
|
const handleViewAllProduction = () => {
|
|
|
|
|
navigate('/app/operations/production');
|
|
|
|
|
};
|
2025-08-28 10:41:04 +02:00
|
|
|
|
2025-09-19 16:17:04 +02:00
|
|
|
const handleOrderItem = (itemId: string) => {
|
|
|
|
|
console.log('Ordering item:', itemId);
|
2025-10-21 19:50:07 +02:00
|
|
|
navigate('/app/operations/procurement');
|
2025-08-28 10:41:04 +02:00
|
|
|
};
|
|
|
|
|
|
2025-10-21 19:50:07 +02:00
|
|
|
const handleStartBatch = (batchId: string) => {
|
|
|
|
|
console.log('Starting production batch:', batchId);
|
2025-08-28 10:41:04 +02:00
|
|
|
};
|
|
|
|
|
|
2025-10-21 19:50:07 +02:00
|
|
|
const handlePauseBatch = (batchId: string) => {
|
|
|
|
|
console.log('Pausing production batch:', batchId);
|
2025-09-19 16:17:04 +02:00
|
|
|
};
|
2025-08-28 10:41:04 +02:00
|
|
|
|
2025-09-19 16:17:04 +02:00
|
|
|
const handleViewDetails = (id: string) => {
|
|
|
|
|
console.log('Viewing details for:', id);
|
|
|
|
|
};
|
2025-08-28 10:41:04 +02:00
|
|
|
|
2025-10-21 19:50:07 +02:00
|
|
|
const handleApprovePO = (poId: string) => {
|
|
|
|
|
console.log('Approved PO:', poId);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleRejectPO = (poId: string) => {
|
|
|
|
|
console.log('Rejected PO:', poId);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleViewPODetails = (poId: string) => {
|
|
|
|
|
console.log('Viewing PO details:', poId);
|
|
|
|
|
navigate(`/app/suppliers/purchase-orders/${poId}`);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleViewAllPOs = () => {
|
|
|
|
|
navigate('/app/operations/procurement');
|
2025-09-19 16:17:04 +02:00
|
|
|
};
|
2025-08-28 10:41:04 +02:00
|
|
|
|
2025-10-21 19:50:07 +02:00
|
|
|
// Build stats from real API data
|
|
|
|
|
const criticalStats = React.useMemo(() => {
|
|
|
|
|
if (!dashboardStats) {
|
|
|
|
|
// Return loading/empty state
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Format currency values
|
|
|
|
|
const formatCurrency = (value: number): string => {
|
|
|
|
|
return `${dashboardStats.salesCurrency}${value.toLocaleString('en-US', {
|
|
|
|
|
minimumFractionDigits: 0,
|
|
|
|
|
maximumFractionDigits: 0,
|
|
|
|
|
})}`;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Determine trend direction
|
|
|
|
|
const getTrendDirection = (value: number): 'up' | 'down' | 'neutral' => {
|
|
|
|
|
if (value > 0) return 'up';
|
|
|
|
|
if (value < 0) return 'down';
|
|
|
|
|
return 'neutral';
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Build subtitle for sales
|
|
|
|
|
const salesChange = dashboardStats.salesToday * (dashboardStats.salesTrend / 100);
|
|
|
|
|
const salesSubtitle = salesChange > 0
|
|
|
|
|
? `+${formatCurrency(salesChange)} ${t('dashboard:messages.more_than_yesterday', 'more than yesterday')}`
|
|
|
|
|
: salesChange < 0
|
|
|
|
|
? `${formatCurrency(Math.abs(salesChange))} ${t('dashboard:messages.less_than_yesterday', 'less than yesterday')}`
|
|
|
|
|
: t('dashboard:messages.same_as_yesterday', 'Same as yesterday');
|
|
|
|
|
|
|
|
|
|
// Build subtitle for products
|
|
|
|
|
const productsChange = Math.round(dashboardStats.productsSoldToday * (dashboardStats.productsSoldTrend / 100));
|
|
|
|
|
const productsSubtitle = productsChange !== 0
|
|
|
|
|
? `${productsChange > 0 ? '+' : ''}${productsChange} ${t('dashboard:messages.more_units', 'units')}`
|
|
|
|
|
: t('dashboard:messages.same_as_yesterday', 'Same as yesterday');
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
{
|
|
|
|
|
title: t('dashboard:stats.sales_today', 'Sales Today'),
|
|
|
|
|
value: formatCurrency(dashboardStats.salesToday),
|
|
|
|
|
icon: Euro,
|
|
|
|
|
variant: 'success' as const,
|
|
|
|
|
trend: {
|
|
|
|
|
value: Math.abs(dashboardStats.salesTrend),
|
|
|
|
|
direction: getTrendDirection(dashboardStats.salesTrend),
|
|
|
|
|
label: t('dashboard:trends.vs_yesterday', '% vs yesterday')
|
|
|
|
|
},
|
|
|
|
|
subtitle: salesSubtitle
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
title: t('dashboard:stats.pending_orders', 'Pending Orders'),
|
|
|
|
|
value: dashboardStats.pendingOrders.toString(),
|
|
|
|
|
icon: Clock,
|
|
|
|
|
variant: dashboardStats.pendingOrders > 10 ? ('warning' as const) : ('info' as const),
|
|
|
|
|
trend: dashboardStats.ordersTrend !== 0 ? {
|
|
|
|
|
value: Math.abs(dashboardStats.ordersTrend),
|
|
|
|
|
direction: getTrendDirection(dashboardStats.ordersTrend),
|
|
|
|
|
label: t('dashboard:trends.vs_yesterday', '% vs yesterday')
|
|
|
|
|
} : undefined,
|
|
|
|
|
subtitle: dashboardStats.pendingOrders > 0
|
|
|
|
|
? t('dashboard:messages.require_attention', 'Require attention')
|
|
|
|
|
: t('dashboard:messages.all_caught_up', 'All caught up!')
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
title: t('dashboard:stats.products_sold', 'Products Sold'),
|
|
|
|
|
value: dashboardStats.productsSoldToday.toString(),
|
|
|
|
|
icon: Package,
|
|
|
|
|
variant: 'info' as const,
|
|
|
|
|
trend: dashboardStats.productsSoldTrend !== 0 ? {
|
|
|
|
|
value: Math.abs(dashboardStats.productsSoldTrend),
|
|
|
|
|
direction: getTrendDirection(dashboardStats.productsSoldTrend),
|
|
|
|
|
label: t('dashboard:trends.vs_yesterday', '% vs yesterday')
|
|
|
|
|
} : undefined,
|
|
|
|
|
subtitle: productsSubtitle
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
title: t('dashboard:stats.stock_alerts', 'Critical Stock'),
|
|
|
|
|
value: dashboardStats.criticalStock.toString(),
|
|
|
|
|
icon: AlertTriangle,
|
|
|
|
|
variant: dashboardStats.criticalStock > 0 ? ('error' as const) : ('success' as const),
|
|
|
|
|
trend: undefined, // Stock alerts don't have historical trends
|
|
|
|
|
subtitle: dashboardStats.criticalStock > 0
|
|
|
|
|
? t('dashboard:messages.action_required', 'Action required')
|
|
|
|
|
: t('dashboard:messages.stock_healthy', 'Stock levels healthy')
|
|
|
|
|
}
|
|
|
|
|
];
|
|
|
|
|
}, [dashboardStats, t]);
|
|
|
|
|
|
|
|
|
|
|
2025-08-28 10:41:04 +02:00
|
|
|
return (
|
2025-09-19 16:17:04 +02:00
|
|
|
<div className="space-y-6 p-4 sm:p-6">
|
2025-08-28 10:41:04 +02:00
|
|
|
<PageHeader
|
2025-09-22 11:04:03 +02:00
|
|
|
title={t('dashboard:title', 'Dashboard')}
|
|
|
|
|
description={t('dashboard:subtitle', 'Overview of your bakery operations')}
|
2025-08-28 10:41:04 +02:00
|
|
|
/>
|
|
|
|
|
|
2025-09-19 16:17:04 +02:00
|
|
|
{/* Critical Metrics using StatsGrid */}
|
2025-10-12 18:47:33 +02:00
|
|
|
<div data-tour="dashboard-stats">
|
2025-10-21 19:50:07 +02:00
|
|
|
{isLoadingStats ? (
|
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
|
|
|
|
|
{[1, 2, 3, 4].map((i) => (
|
|
|
|
|
<div
|
|
|
|
|
key={i}
|
|
|
|
|
className="h-32 bg-[var(--bg-secondary)] border border-[var(--border-primary)] rounded-lg animate-pulse"
|
|
|
|
|
/>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
) : statsError ? (
|
|
|
|
|
<div className="mb-6 p-4 bg-[var(--color-error)]/10 border border-[var(--color-error)]/20 rounded-lg">
|
|
|
|
|
<p className="text-[var(--color-error)] text-sm">
|
|
|
|
|
{t('dashboard:errors.failed_to_load_stats', 'Failed to load dashboard statistics. Please try again.')}
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<StatsGrid
|
|
|
|
|
stats={criticalStats}
|
|
|
|
|
columns={4}
|
|
|
|
|
gap="lg"
|
|
|
|
|
className="mb-6"
|
|
|
|
|
/>
|
|
|
|
|
)}
|
2025-10-12 18:47:33 +02:00
|
|
|
</div>
|
2025-08-28 10:41:04 +02:00
|
|
|
|
2025-10-21 19:50:07 +02:00
|
|
|
{/* Dashboard Content - Four Main Sections */}
|
2025-09-19 16:17:04 +02:00
|
|
|
<div className="space-y-6">
|
2025-10-21 19:50:07 +02:00
|
|
|
{/* 1. Real-time Alerts */}
|
2025-10-12 18:47:33 +02:00
|
|
|
<div data-tour="real-time-alerts">
|
|
|
|
|
<RealTimeAlerts />
|
|
|
|
|
</div>
|
2025-09-19 16:17:04 +02:00
|
|
|
|
2025-10-21 19:50:07 +02:00
|
|
|
{/* 2. Pending PO Approvals - What purchase orders need approval? */}
|
|
|
|
|
<div data-tour="pending-po-approvals">
|
|
|
|
|
<PendingPOApprovals
|
|
|
|
|
onApprovePO={handleApprovePO}
|
|
|
|
|
onRejectPO={handleRejectPO}
|
|
|
|
|
onViewDetails={handleViewPODetails}
|
|
|
|
|
onViewAllPOs={handleViewAllPOs}
|
|
|
|
|
maxPOs={5}
|
2025-10-12 18:47:33 +02:00
|
|
|
/>
|
|
|
|
|
</div>
|
2025-09-19 16:17:04 +02:00
|
|
|
|
2025-10-21 19:50:07 +02:00
|
|
|
{/* 3. Today's Production - What needs to be produced today? */}
|
|
|
|
|
<div data-tour="today-production">
|
|
|
|
|
<TodayProduction
|
|
|
|
|
onStartBatch={handleStartBatch}
|
|
|
|
|
onPauseBatch={handlePauseBatch}
|
2025-10-12 18:47:33 +02:00
|
|
|
onViewDetails={handleViewDetails}
|
2025-10-21 19:50:07 +02:00
|
|
|
onViewAllPlans={handleViewAllProduction}
|
|
|
|
|
maxBatches={5}
|
2025-10-12 18:47:33 +02:00
|
|
|
/>
|
|
|
|
|
</div>
|
2025-08-28 10:41:04 +02:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export default DashboardPage;
|