Improve the frontend

This commit is contained in:
Urtzi Alfaro
2025-10-21 19:50:07 +02:00
parent 05da20357d
commit 8d30172483
105 changed files with 14699 additions and 4630 deletions

View File

@@ -2,31 +2,35 @@ import React, { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { PageHeader } from '../../components/layout';
import { Button } from '../../components/ui/Button';
import { Card, CardHeader, CardBody } from '../../components/ui/Card';
import StatsGrid from '../../components/ui/Stats/StatsGrid';
import RealTimeAlerts from '../../components/domain/dashboard/RealTimeAlerts';
import ProcurementPlansToday from '../../components/domain/dashboard/ProcurementPlansToday';
import ProductionPlansToday from '../../components/domain/dashboard/ProductionPlansToday';
import PurchaseOrdersTracking from '../../components/domain/dashboard/PurchaseOrdersTracking';
import PendingPOApprovals from '../../components/domain/dashboard/PendingPOApprovals';
import TodayProduction from '../../components/domain/dashboard/TodayProduction';
import { useTenant } from '../../stores/tenant.store';
import { useDemoTour, shouldStartTour, clearTourStartPending } from '../../features/demo-onboarding';
import { useDashboardStats } from '../../api/hooks/dashboard';
import {
AlertTriangle,
Clock,
Euro,
Package,
Plus,
Building2
Package
} from 'lucide-react';
const DashboardPage: React.FC = () => {
const { t } = useTranslation();
const navigate = useNavigate();
const { availableTenants } = useTenant();
const { availableTenants, currentTenant } = useTenant();
const { startTour } = useDemoTour();
const isDemoMode = localStorage.getItem('demo_mode') === 'true';
// Fetch real dashboard statistics
const { data: dashboardStats, isLoading: isLoadingStats, error: statsError } = useDashboardStats(
currentTenant?.id || '',
{
enabled: !!currentTenant?.id,
}
);
useEffect(() => {
console.log('[Dashboard] Demo mode:', isDemoMode);
console.log('[Dashboard] Should start tour:', shouldStartTour());
@@ -44,81 +48,137 @@ const DashboardPage: React.FC = () => {
}
}, [isDemoMode, startTour]);
const handleAddNewBakery = () => {
navigate('/app/onboarding?new=true');
const handleViewAllProcurement = () => {
navigate('/app/operations/procurement');
};
const criticalStats = [
{
title: t('dashboard:stats.sales_today', 'Sales Today'),
value: '€1,247',
icon: Euro,
variant: 'success' as const,
trend: {
value: 12,
direction: 'up' as const,
label: t('dashboard:trends.vs_yesterday', '% vs yesterday')
},
subtitle: '+€135 ' + t('dashboard:messages.more_than_yesterday', 'more than yesterday')
},
{
title: t('dashboard:stats.pending_orders', 'Pending Orders'),
value: '23',
icon: Clock,
variant: 'warning' as const,
trend: {
value: 4,
direction: 'down' as const,
label: t('dashboard:trends.vs_yesterday', '% vs yesterday')
},
subtitle: t('dashboard:messages.require_attention', 'Require attention')
},
{
title: t('dashboard:stats.products_sold', 'Products Sold'),
value: '156',
icon: Package,
variant: 'info' as const,
trend: {
value: 8,
direction: 'up' as const,
label: t('dashboard:trends.vs_yesterday', '% vs yesterday')
},
subtitle: '+12 ' + t('dashboard:messages.more_units', 'more units')
},
{
title: t('dashboard:stats.stock_alerts', 'Critical Stock'),
value: '4',
icon: AlertTriangle,
variant: 'error' as const,
trend: {
value: 100,
direction: 'up' as const,
label: t('dashboard:trends.vs_yesterday', '% vs yesterday')
},
subtitle: t('dashboard:messages.action_required', 'Action required')
}
];
const handleViewAllProduction = () => {
navigate('/app/operations/production');
};
const handleOrderItem = (itemId: string) => {
console.log('Ordering item:', itemId);
navigate('/app/operations/procurement');
};
const handleStartOrder = (orderId: string) => {
console.log('Starting production order:', orderId);
const handleStartBatch = (batchId: string) => {
console.log('Starting production batch:', batchId);
};
const handlePauseOrder = (orderId: string) => {
console.log('Pausing production order:', orderId);
const handlePauseBatch = (batchId: string) => {
console.log('Pausing production batch:', batchId);
};
const handleViewDetails = (id: string) => {
console.log('Viewing details for:', id);
};
const handleViewAllPlans = () => {
console.log('Viewing all plans');
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');
};
// 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]);
return (
<div className="space-y-6 p-4 sm:p-6">
<PageHeader
@@ -128,76 +188,57 @@ const DashboardPage: React.FC = () => {
{/* Critical Metrics using StatsGrid */}
<div data-tour="dashboard-stats">
<StatsGrid
stats={criticalStats}
columns={4}
gap="lg"
className="mb-6"
/>
{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"
/>
)}
</div>
{/* Quick Actions - Add New Bakery */}
{availableTenants && availableTenants.length > 0 && (
<Card>
<CardHeader>
<h3 className="text-lg font-semibold text-[var(--text-primary)]">{t('dashboard:sections.quick_actions', 'Quick Actions')}</h3>
<p className="text-sm text-[var(--text-secondary)]">{t('dashboard:messages.manage_organizations', 'Manage your organizations')}</p>
</CardHeader>
<CardBody>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<Button
onClick={handleAddNewBakery}
variant="outline"
size="lg"
className="h-auto p-6 flex flex-col items-center gap-3 bg-gradient-to-br from-[var(--color-primary)]/5 to-[var(--color-primary)]/10 border-[var(--color-primary)]/20 hover:border-[var(--color-primary)]/40 hover:bg-[var(--color-primary)]/20 transition-all duration-200"
>
<div className="w-12 h-12 bg-[var(--color-primary)]/10 rounded-lg flex items-center justify-center">
<Plus className="w-6 h-6 text-[var(--color-primary)]" />
</div>
<div className="text-center">
<div className="font-semibold text-[var(--text-primary)]">{t('dashboard:quick_actions.add_new_bakery', 'Add New Bakery')}</div>
<div className="text-sm text-[var(--text-secondary)] mt-1">{t('dashboard:messages.setup_new_business', 'Set up a new business from scratch')}</div>
</div>
</Button>
<div className="flex flex-col items-center justify-center p-6 bg-[var(--bg-secondary)] rounded-lg border border-[var(--border-primary)]">
<Building2 className="w-8 h-8 text-[var(--text-tertiary)] mb-2" />
<div className="text-center">
<div className="text-sm font-medium text-[var(--text-secondary)]">{t('dashboard:messages.active_organizations', 'Active Organizations')}</div>
<div className="text-2xl font-bold text-[var(--color-primary)]">{availableTenants.length}</div>
</div>
</div>
</div>
</CardBody>
</Card>
)}
{/* Full width blocks - one after another */}
{/* Dashboard Content - Four Main Sections */}
<div className="space-y-6">
{/* 1. Real-time alerts block */}
{/* 1. Real-time Alerts */}
<div data-tour="real-time-alerts">
<RealTimeAlerts />
</div>
{/* 2. Purchase Orders Tracking block */}
<PurchaseOrdersTracking />
{/* 3. Procurement plans block */}
<div data-tour="procurement-plans">
<ProcurementPlansToday
onOrderItem={handleOrderItem}
onViewDetails={handleViewDetails}
onViewAllPlans={handleViewAllPlans}
{/* 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}
/>
</div>
{/* 4. Production plans block */}
<div data-tour="production-plans">
<ProductionPlansToday
onStartOrder={handleStartOrder}
onPauseOrder={handlePauseOrder}
{/* 3. Today's Production - What needs to be produced today? */}
<div data-tour="today-production">
<TodayProduction
onStartBatch={handleStartBatch}
onPauseBatch={handlePauseBatch}
onViewDetails={handleViewDetails}
onViewAllPlans={handleViewAllPlans}
onViewAllPlans={handleViewAllProduction}
maxBatches={5}
/>
</div>
</div>