Improve the frontend
This commit is contained in:
@@ -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>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import { Plus, Clock, AlertCircle, CheckCircle, Timer, ChefHat, Eye, Edit, Package, Zap, User, PlusCircle } from 'lucide-react';
|
||||
import { Plus, Clock, AlertCircle, CheckCircle, Timer, ChefHat, Eye, Edit, Package, PlusCircle } from 'lucide-react';
|
||||
import { Button, StatsGrid, EditViewModal, Toggle, SearchAndFilter, type FilterConfig } from '../../../../components/ui';
|
||||
import { statusColors } from '../../../../styles/colors';
|
||||
import { formatters } from '../../../../components/ui/Stats/StatsPresets';
|
||||
@@ -35,7 +35,6 @@ const ProductionPage: React.FC = () => {
|
||||
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||
const [showQualityModal, setShowQualityModal] = useState(false);
|
||||
const [modalMode, setModalMode] = useState<'view' | 'edit'>('view');
|
||||
const [isAIMode, setIsAIMode] = useState(true);
|
||||
|
||||
const currentTenant = useCurrentTenant();
|
||||
const tenantId = currentTenant?.id || '';
|
||||
@@ -289,49 +288,15 @@ const ProductionPage: React.FC = () => {
|
||||
title="Gestión de Producción"
|
||||
description="Planifica y controla la producción diaria de tu panadería"
|
||||
/>
|
||||
<div className="flex items-center gap-4">
|
||||
{/* AI/Manual Mode Segmented Control */}
|
||||
<div className="inline-flex p-1 bg-[var(--surface-secondary)] rounded-xl border border-[var(--border-primary)] shadow-sm">
|
||||
<button
|
||||
onClick={() => setIsAIMode(true)}
|
||||
className={`
|
||||
flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all duration-200 ease-in-out
|
||||
${isAIMode
|
||||
? 'bg-[var(--color-primary)] text-white shadow-sm'
|
||||
: 'text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--surface-tertiary)]'
|
||||
}
|
||||
`}
|
||||
>
|
||||
<Zap className="w-4 h-4" />
|
||||
Automático IA
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setIsAIMode(false)}
|
||||
className={`
|
||||
flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all duration-200 ease-in-out
|
||||
${!isAIMode
|
||||
? 'bg-[var(--color-primary)] text-white shadow-sm'
|
||||
: 'text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--surface-tertiary)]'
|
||||
}
|
||||
`}
|
||||
>
|
||||
<User className="w-4 h-4" />
|
||||
Manual
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{!isAIMode && (
|
||||
<Button
|
||||
variant="primary"
|
||||
size="md"
|
||||
onClick={() => setShowCreateModal(true)}
|
||||
className="flex items-center gap-2.5 px-6 py-3 text-sm font-semibold tracking-wide shadow-lg hover:shadow-xl transition-all duration-200"
|
||||
>
|
||||
<PlusCircle className="w-5 h-5" />
|
||||
Nueva Orden de Producción
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="md"
|
||||
onClick={() => setShowCreateModal(true)}
|
||||
className="flex items-center gap-2.5 px-6 py-3 text-sm font-semibold tracking-wide shadow-lg hover:shadow-xl transition-all duration-200"
|
||||
>
|
||||
<PlusCircle className="w-5 h-5" />
|
||||
Nueva Orden de Producción
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Production Stats */}
|
||||
|
||||
@@ -283,7 +283,7 @@ const TeamPage: React.FC = () => {
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
<div className="space-y-6">
|
||||
<PageHeader
|
||||
title="Gestión de Equipo"
|
||||
description="Administra los miembros del equipo, roles y permisos"
|
||||
@@ -300,7 +300,7 @@ const TeamPage: React.FC = () => {
|
||||
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
<div className="space-y-6">
|
||||
<PageHeader
|
||||
title={t('settings:team.title', 'Gestión de Equipo')}
|
||||
description={t('settings:team.description', 'Administra los miembros del equipo, roles y permisos')}
|
||||
@@ -368,48 +368,46 @@ const TeamPage: React.FC = () => {
|
||||
] as FilterConfig[]}
|
||||
/>
|
||||
|
||||
{/* Add Member Button */}
|
||||
{canManageTeam && filteredMembers.length > 0 && (
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
onClick={() => setShowAddForm(true)}
|
||||
variant="primary"
|
||||
size="md"
|
||||
className="font-medium px-4 py-2 shadow-sm hover:shadow-md transition-all duration-200"
|
||||
>
|
||||
<Plus className="w-4 h-4 mr-2 flex-shrink-0" />
|
||||
<span>Agregar Miembro</span>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Team Members List - Responsive grid */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-4 lg:gap-6">
|
||||
{filteredMembers.map((member) => (
|
||||
<StatusCard
|
||||
key={member.id}
|
||||
id={`team-member-${member.id}`}
|
||||
statusIndicator={getMemberStatusConfig(member)}
|
||||
title={member.user?.full_name || member.user_full_name}
|
||||
subtitle={member.user?.email || member.user_email}
|
||||
primaryValue={Math.floor((Date.now() - new Date(member.joined_at).getTime()) / (1000 * 60 * 60 * 24))}
|
||||
primaryValueLabel="días"
|
||||
secondaryInfo={{
|
||||
label: 'Estado',
|
||||
value: member.is_active ? 'Activo' : 'Inactivo'
|
||||
}}
|
||||
metadata={[
|
||||
`Email: ${member.user?.email || member.user_email}`,
|
||||
`Teléfono: ${member.user?.phone || 'No disponible'}`,
|
||||
...(member.role === TENANT_ROLES.OWNER ? ['🏢 Propietario de la organización'] : [])
|
||||
]}
|
||||
actions={getMemberActions(member)}
|
||||
className={`
|
||||
${!member.is_active ? 'opacity-75' : ''}
|
||||
transition-all duration-200 hover:scale-[1.02]
|
||||
`}
|
||||
/>
|
||||
))}
|
||||
{filteredMembers.map((member: any) => {
|
||||
const user = member.user;
|
||||
const daysInTeam = Math.floor((Date.now() - new Date(member.joined_at).getTime()) / (1000 * 60 * 60 * 24));
|
||||
const lastLogin = user?.last_login ? new Date(user.last_login).toLocaleDateString('es-ES', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
}) : 'Nunca';
|
||||
|
||||
return (
|
||||
<StatusCard
|
||||
key={member.id}
|
||||
id={`team-member-${member.id}`}
|
||||
statusIndicator={getMemberStatusConfig(member)}
|
||||
title={user?.full_name || member.user_full_name || 'Usuario'}
|
||||
subtitle={user?.email || member.user_email || ''}
|
||||
primaryValue={daysInTeam}
|
||||
primaryValueLabel="días en el equipo"
|
||||
secondaryInfo={{
|
||||
label: 'Último acceso',
|
||||
value: lastLogin
|
||||
}}
|
||||
metadata={[
|
||||
`Email: ${user?.email || member.user_email || 'No disponible'}`,
|
||||
`Teléfono: ${user?.phone || 'No disponible'}`,
|
||||
`Idioma: ${user?.language?.toUpperCase() || 'No especificado'}`,
|
||||
`Unido: ${new Date(member.joined_at).toLocaleDateString('es-ES', { year: 'numeric', month: 'short', day: 'numeric' })}`,
|
||||
...(member.role === TENANT_ROLES.OWNER ? ['🏢 Propietario de la organización'] : []),
|
||||
...(user?.timezone ? [`Zona horaria: ${user.timezone}`] : [])
|
||||
]}
|
||||
actions={getMemberActions(member)}
|
||||
className={`
|
||||
${!member.is_active ? 'opacity-75' : ''}
|
||||
transition-all duration-200 hover:scale-[1.02]
|
||||
`}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{filteredMembers.length === 0 && (
|
||||
|
||||
@@ -325,7 +325,7 @@ const LandingPage: React.FC = () => {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-20 grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
<div className="mt-20 grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
{/* AI Technology */}
|
||||
<div className="group relative bg-[var(--bg-primary)] rounded-2xl p-8 shadow-lg hover:shadow-2xl transition-all duration-300 border-2 border-[var(--border-primary)] hover:border-blue-500/50">
|
||||
<div className="absolute -top-4 left-8">
|
||||
@@ -432,97 +432,144 @@ const LandingPage: React.FC = () => {
|
||||
</div>
|
||||
|
||||
{/* Smart Inventory */}
|
||||
<div className="group relative bg-[var(--bg-primary)] rounded-2xl p-8 shadow-lg hover:shadow-2xl transition-all duration-300 border border-[var(--border-primary)] hover:border-[var(--color-primary)]/30">
|
||||
<div className="group relative bg-[var(--bg-primary)] rounded-2xl p-8 shadow-lg hover:shadow-2xl transition-all duration-300 border-2 border-[var(--border-primary)] hover:border-indigo-500/50">
|
||||
<div className="absolute -top-4 left-8">
|
||||
<div className="w-12 h-12 bg-gradient-to-r from-[var(--color-secondary)] to-[var(--color-secondary-dark)] rounded-xl flex items-center justify-center shadow-lg">
|
||||
<div className="w-12 h-12 bg-gradient-to-r from-indigo-600 to-purple-600 rounded-xl flex items-center justify-center shadow-lg">
|
||||
<Package className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<h3 className="text-xl font-bold text-[var(--text-primary)]">{t('landing:features.smart_inventory.title', 'Inventario Inteligente')}</h3>
|
||||
<p className="mt-4 text-[var(--text-secondary)]">
|
||||
{t('landing:features.smart_inventory.description', 'Control automático de stock con alertas predictivas, órdenes de compra automatizadas y optimización de costos.')}
|
||||
{t('landing:features.smart_inventory.description', 'Control automático de stock con alertas predictivas, órdenes de compra automatizadas y optimización de costos de materias primas en tiempo real.')}
|
||||
</p>
|
||||
<div className="mt-6">
|
||||
<div className="flex items-center text-sm text-[var(--color-secondary)]">
|
||||
<Check className="w-4 h-4 mr-2" />
|
||||
{t('landing:features.smart_inventory.features.alerts', 'Alertas automáticas de stock bajo')}
|
||||
<div className="mt-6 space-y-3">
|
||||
<div className="flex items-center text-sm">
|
||||
<div className="flex-shrink-0 w-6 h-6 bg-indigo-500/10 rounded-full flex items-center justify-center mr-3">
|
||||
<Package className="w-3 h-3 text-indigo-600" />
|
||||
</div>
|
||||
<span className="text-[var(--text-secondary)]">{t('landing:features.smart_inventory.features.alerts', 'Alertas automáticas de stock bajo')}</span>
|
||||
</div>
|
||||
<div className="flex items-center text-sm text-[var(--color-secondary)] mt-2">
|
||||
<Check className="w-4 h-4 mr-2" />
|
||||
{t('landing:features.smart_inventory.features.orders', 'Órdenes de compra automatizadas')}
|
||||
<div className="flex items-center text-sm">
|
||||
<div className="flex-shrink-0 w-6 h-6 bg-indigo-500/10 rounded-full flex items-center justify-center mr-3">
|
||||
<TrendingUp className="w-3 h-3 text-indigo-600" />
|
||||
</div>
|
||||
<span className="text-[var(--text-secondary)]">{t('landing:features.smart_inventory.features.orders', 'Órdenes de compra automatizadas')}</span>
|
||||
</div>
|
||||
<div className="flex items-center text-sm text-[var(--color-secondary)] mt-2">
|
||||
<Check className="w-4 h-4 mr-2" />
|
||||
{t('landing:features.smart_inventory.features.optimization', 'Optimización de costos de materias primas')}
|
||||
<div className="flex items-center text-sm">
|
||||
<div className="flex-shrink-0 w-6 h-6 bg-indigo-500/10 rounded-full flex items-center justify-center mr-3">
|
||||
<Euro className="w-3 h-3 text-indigo-600" />
|
||||
</div>
|
||||
<span className="text-[var(--text-secondary)]">{t('landing:features.smart_inventory.features.optimization', 'Optimización de costos de materias primas')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Production Planning */}
|
||||
<div className="group relative bg-[var(--bg-primary)] rounded-2xl p-8 shadow-lg hover:shadow-2xl transition-all duration-300 border border-[var(--border-primary)] hover:border-[var(--color-primary)]/30">
|
||||
<div className="group relative bg-[var(--bg-primary)] rounded-2xl p-8 shadow-lg hover:shadow-2xl transition-all duration-300 border-2 border-[var(--border-primary)] hover:border-rose-500/50">
|
||||
<div className="absolute -top-4 left-8">
|
||||
<div className="w-12 h-12 bg-gradient-to-r from-[var(--color-accent)] to-[var(--color-accent-dark)] rounded-xl flex items-center justify-center shadow-lg">
|
||||
<div className="w-12 h-12 bg-gradient-to-r from-rose-600 to-pink-600 rounded-xl flex items-center justify-center shadow-lg">
|
||||
<Calendar className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<h3 className="text-xl font-bold text-[var(--text-primary)]">{t('landing:features.production_planning.title', 'Planificación de Producción')}</h3>
|
||||
<p className="mt-4 text-[var(--text-secondary)]">
|
||||
{t('landing:features.production_planning.description', 'Programa automáticamente la producción diaria basada en predicciones, optimiza horarios y recursos disponibles.')}
|
||||
{t('landing:features.production_planning.description', 'Programa automáticamente la producción diaria basada en predicciones de IA, optimiza horarios, recursos y maximiza la eficiencia de tus hornos.')}
|
||||
</p>
|
||||
<div className="mt-6">
|
||||
<div className="flex items-center text-sm text-[var(--color-accent)]">
|
||||
<Check className="w-4 h-4 mr-2" />
|
||||
{t('landing:features.production_planning.features.scheduling', 'Programación automática de horneado')}
|
||||
<div className="mt-6 space-y-3">
|
||||
<div className="flex items-center text-sm">
|
||||
<div className="flex-shrink-0 w-6 h-6 bg-rose-500/10 rounded-full flex items-center justify-center mr-3">
|
||||
<Calendar className="w-3 h-3 text-rose-600" />
|
||||
</div>
|
||||
<span className="text-[var(--text-secondary)]">{t('landing:features.production_planning.features.scheduling', 'Programación automática de horneado')}</span>
|
||||
</div>
|
||||
<div className="flex items-center text-sm text-[var(--color-accent)] mt-2">
|
||||
<Check className="w-4 h-4 mr-2" />
|
||||
{t('landing:features.production_planning.features.oven', 'Optimización de uso de hornos')}
|
||||
<div className="flex items-center text-sm">
|
||||
<div className="flex-shrink-0 w-6 h-6 bg-rose-500/10 rounded-full flex items-center justify-center mr-3">
|
||||
<Zap className="w-3 h-3 text-rose-600" />
|
||||
</div>
|
||||
<span className="text-[var(--text-secondary)]">{t('landing:features.production_planning.features.oven', 'Optimización de uso de hornos')}</span>
|
||||
</div>
|
||||
<div className="flex items-center text-sm text-[var(--color-accent)] mt-2">
|
||||
<Check className="w-4 h-4 mr-2" />
|
||||
{t('landing:features.production_planning.features.staff', 'Gestión de personal y turnos')}
|
||||
<div className="flex items-center text-sm">
|
||||
<div className="flex-shrink-0 w-6 h-6 bg-rose-500/10 rounded-full flex items-center justify-center mr-3">
|
||||
<Users className="w-3 h-3 text-rose-600" />
|
||||
</div>
|
||||
<span className="text-[var(--text-secondary)]">{t('landing:features.production_planning.features.staff', 'Gestión de personal y turnos')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Advanced Analytics */}
|
||||
<div className="group relative bg-[var(--bg-primary)] rounded-2xl p-8 shadow-lg hover:shadow-2xl transition-all duration-300 border-2 border-[var(--border-primary)] hover:border-cyan-500/50">
|
||||
<div className="absolute -top-4 left-8">
|
||||
<div className="w-12 h-12 bg-gradient-to-r from-cyan-600 to-teal-600 rounded-xl flex items-center justify-center shadow-lg">
|
||||
<PieChart className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<h3 className="text-xl font-bold text-[var(--text-primary)]">{t('landing:features.advanced_analytics.title', 'Analytics Avanzado')}</h3>
|
||||
<p className="mt-4 text-[var(--text-secondary)]">
|
||||
{t('landing:features.advanced_analytics.description', 'Dashboards en tiempo real con métricas clave de negocio, análisis de rentabilidad por producto y reportes personalizables para tomar decisiones basadas en datos.')}
|
||||
</p>
|
||||
<div className="mt-6 space-y-3">
|
||||
<div className="flex items-center text-sm">
|
||||
<div className="flex-shrink-0 w-6 h-6 bg-cyan-500/10 rounded-full flex items-center justify-center mr-3">
|
||||
<BarChart3 className="w-3 h-3 text-cyan-600" />
|
||||
</div>
|
||||
<span className="text-[var(--text-secondary)]">{t('landing:features.advanced_analytics.features.realtime', 'Dashboards en tiempo real')}</span>
|
||||
</div>
|
||||
<div className="flex items-center text-sm">
|
||||
<div className="flex-shrink-0 w-6 h-6 bg-cyan-500/10 rounded-full flex items-center justify-center mr-3">
|
||||
<TrendingUp className="w-3 h-3 text-cyan-600" />
|
||||
</div>
|
||||
<span className="text-[var(--text-secondary)]">{t('landing:features.advanced_analytics.features.profitability', 'Análisis de rentabilidad por producto')}</span>
|
||||
</div>
|
||||
<div className="flex items-center text-sm">
|
||||
<div className="flex-shrink-0 w-6 h-6 bg-cyan-500/10 rounded-full flex items-center justify-center mr-3">
|
||||
<PieChart className="w-3 h-3 text-cyan-600" />
|
||||
</div>
|
||||
<span className="text-[var(--text-secondary)]">{t('landing:features.advanced_analytics.features.reports', 'Reportes personalizables')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Additional Features Grid */}
|
||||
<div className="mt-16 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
|
||||
<div className="text-center p-6 bg-[var(--bg-primary)] rounded-xl border border-[var(--border-primary)]">
|
||||
{/* Secondary Features - Compact Grid */}
|
||||
<div className="mt-16 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<div className="text-center p-6 bg-[var(--bg-primary)] rounded-xl border border-[var(--border-primary)] hover:border-[var(--color-primary)]/30 transition-all duration-300">
|
||||
<div className="w-12 h-12 bg-[var(--color-primary)]/10 rounded-lg flex items-center justify-center mx-auto mb-4">
|
||||
<BarChart3 className="w-6 h-6 text-[var(--color-primary)]" />
|
||||
</div>
|
||||
<h4 className="font-semibold text-[var(--text-primary)]">{t('landing:features.advanced_analytics.title', 'Analytics Avanzado')}</h4>
|
||||
<p className="text-sm text-[var(--text-secondary)] mt-2">{t('landing:features.advanced_analytics.description', 'Dashboards en tiempo real con métricas clave')}</p>
|
||||
</div>
|
||||
|
||||
<div className="text-center p-6 bg-[var(--bg-primary)] rounded-xl border border-[var(--border-primary)]">
|
||||
<div className="w-12 h-12 bg-[var(--color-secondary)]/10 rounded-lg flex items-center justify-center mx-auto mb-4">
|
||||
<Euro className="w-6 h-6 text-[var(--color-secondary)]" />
|
||||
<Euro className="w-6 h-6 text-[var(--color-primary)]" />
|
||||
</div>
|
||||
<h4 className="font-semibold text-[var(--text-primary)]">{t('landing:features.pos_integration.title', 'POS Integrado')}</h4>
|
||||
<p className="text-sm text-[var(--text-secondary)] mt-2">{t('landing:features.pos_integration.description', 'Sistema de ventas completo y fácil de usar')}</p>
|
||||
</div>
|
||||
|
||||
<div className="text-center p-6 bg-[var(--bg-primary)] rounded-xl border border-[var(--border-primary)]">
|
||||
<div className="w-12 h-12 bg-[var(--color-accent)]/10 rounded-lg flex items-center justify-center mx-auto mb-4">
|
||||
<Shield className="w-6 h-6 text-[var(--color-accent)]" />
|
||||
<div className="text-center p-6 bg-[var(--bg-primary)] rounded-xl border border-[var(--border-primary)] hover:border-[var(--color-primary)]/30 transition-all duration-300">
|
||||
<div className="w-12 h-12 bg-green-500/10 rounded-lg flex items-center justify-center mx-auto mb-4">
|
||||
<Shield className="w-6 h-6 text-green-600" />
|
||||
</div>
|
||||
<h4 className="font-semibold text-[var(--text-primary)]">{t('landing:features.quality_control.title', 'Control de Calidad')}</h4>
|
||||
<p className="text-sm text-[var(--text-secondary)] mt-2">{t('landing:features.quality_control.description', 'Trazabilidad completa y gestión HACCP')}</p>
|
||||
</div>
|
||||
|
||||
<div className="text-center p-6 bg-[var(--bg-primary)] rounded-xl border border-[var(--border-primary)]">
|
||||
<div className="w-12 h-12 bg-[var(--color-info)]/10 rounded-lg flex items-center justify-center mx-auto mb-4">
|
||||
<Settings className="w-6 h-6 text-[var(--color-info)]" />
|
||||
<div className="text-center p-6 bg-[var(--bg-primary)] rounded-xl border border-[var(--border-primary)] hover:border-[var(--color-primary)]/30 transition-all duration-300">
|
||||
<div className="w-12 h-12 bg-purple-500/10 rounded-lg flex items-center justify-center mx-auto mb-4">
|
||||
<Settings className="w-6 h-6 text-purple-600" />
|
||||
</div>
|
||||
<h4 className="font-semibold text-[var(--text-primary)]">{t('landing:features.automation.title', 'Automatización')}</h4>
|
||||
<p className="text-sm text-[var(--text-secondary)] mt-2">{t('landing:features.automation.description', 'Procesos automáticos que ahorran tiempo')}</p>
|
||||
</div>
|
||||
|
||||
<div className="text-center p-6 bg-[var(--bg-primary)] rounded-xl border border-[var(--border-primary)] hover:border-[var(--color-primary)]/30 transition-all duration-300">
|
||||
<div className="w-12 h-12 bg-blue-500/10 rounded-lg flex items-center justify-center mx-auto mb-4">
|
||||
<Zap className="w-6 h-6 text-blue-600" />
|
||||
</div>
|
||||
<h4 className="font-semibold text-[var(--text-primary)]">{t('landing:features.cloud_based.title', 'En la Nube')}</h4>
|
||||
<p className="text-sm text-[var(--text-secondary)] mt-2">{t('landing:features.cloud_based.description', 'Accede desde cualquier lugar, siempre actualizado')}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user