Improve the frontend and repository layer
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PageHeader } from '../../components/layout';
|
||||
@@ -6,15 +6,29 @@ import StatsGrid from '../../components/ui/Stats/StatsGrid';
|
||||
import RealTimeAlerts from '../../components/domain/dashboard/RealTimeAlerts';
|
||||
import PendingPOApprovals from '../../components/domain/dashboard/PendingPOApprovals';
|
||||
import TodayProduction from '../../components/domain/dashboard/TodayProduction';
|
||||
import SustainabilityWidget from '../../components/domain/sustainability/SustainabilityWidget';
|
||||
import { EditViewModal } from '../../components/ui';
|
||||
import { useTenant } from '../../stores/tenant.store';
|
||||
import { useDemoTour, shouldStartTour, clearTourStartPending } from '../../features/demo-onboarding';
|
||||
import { useDashboardStats } from '../../api/hooks/dashboard';
|
||||
import { usePurchaseOrder, useApprovePurchaseOrder, useRejectPurchaseOrder } from '../../api/hooks/purchase-orders';
|
||||
import { useBatchDetails, useUpdateBatchStatus } from '../../api/hooks/production';
|
||||
import { ProductionStatusEnum } from '../../api';
|
||||
import {
|
||||
AlertTriangle,
|
||||
Clock,
|
||||
Euro,
|
||||
Package
|
||||
Package,
|
||||
FileText,
|
||||
Building2,
|
||||
Calendar,
|
||||
CheckCircle,
|
||||
X,
|
||||
ShoppingCart,
|
||||
Factory,
|
||||
Timer
|
||||
} from 'lucide-react';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
const DashboardPage: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -23,6 +37,13 @@ const DashboardPage: React.FC = () => {
|
||||
const { startTour } = useDemoTour();
|
||||
const isDemoMode = localStorage.getItem('demo_mode') === 'true';
|
||||
|
||||
// Modal state management
|
||||
const [selectedPOId, setSelectedPOId] = useState<string | null>(null);
|
||||
const [selectedBatchId, setSelectedBatchId] = useState<string | null>(null);
|
||||
const [showPOModal, setShowPOModal] = useState(false);
|
||||
const [showBatchModal, setShowBatchModal] = useState(false);
|
||||
const [approvalNotes, setApprovalNotes] = useState('');
|
||||
|
||||
// Fetch real dashboard statistics
|
||||
const { data: dashboardStats, isLoading: isLoadingStats, error: statsError } = useDashboardStats(
|
||||
currentTenant?.id || '',
|
||||
@@ -31,6 +52,29 @@ const DashboardPage: React.FC = () => {
|
||||
}
|
||||
);
|
||||
|
||||
// Fetch PO details when modal is open
|
||||
const { data: poDetails, isLoading: isLoadingPO } = usePurchaseOrder(
|
||||
currentTenant?.id || '',
|
||||
selectedPOId || '',
|
||||
{
|
||||
enabled: !!currentTenant?.id && !!selectedPOId && showPOModal
|
||||
}
|
||||
);
|
||||
|
||||
// Fetch Production batch details when modal is open
|
||||
const { data: batchDetails, isLoading: isLoadingBatch } = useBatchDetails(
|
||||
currentTenant?.id || '',
|
||||
selectedBatchId || '',
|
||||
{
|
||||
enabled: !!currentTenant?.id && !!selectedBatchId && showBatchModal
|
||||
}
|
||||
);
|
||||
|
||||
// Mutations
|
||||
const approvePOMutation = useApprovePurchaseOrder();
|
||||
const rejectPOMutation = useRejectPurchaseOrder();
|
||||
const updateBatchStatusMutation = useUpdateBatchStatus();
|
||||
|
||||
useEffect(() => {
|
||||
console.log('[Dashboard] Demo mode:', isDemoMode);
|
||||
console.log('[Dashboard] Should start tour:', shouldStartTour());
|
||||
@@ -61,29 +105,70 @@ const DashboardPage: React.FC = () => {
|
||||
navigate('/app/operations/procurement');
|
||||
};
|
||||
|
||||
const handleStartBatch = (batchId: string) => {
|
||||
console.log('Starting production batch:', batchId);
|
||||
const handleStartBatch = async (batchId: string) => {
|
||||
try {
|
||||
await updateBatchStatusMutation.mutateAsync({
|
||||
tenantId: currentTenant?.id || '',
|
||||
batchId,
|
||||
statusUpdate: { status: ProductionStatusEnum.IN_PROGRESS }
|
||||
});
|
||||
toast.success('Lote iniciado');
|
||||
} catch (error) {
|
||||
console.error('Error starting batch:', error);
|
||||
toast.error('Error al iniciar lote');
|
||||
}
|
||||
};
|
||||
|
||||
const handlePauseBatch = (batchId: string) => {
|
||||
console.log('Pausing production batch:', batchId);
|
||||
const handlePauseBatch = async (batchId: string) => {
|
||||
try {
|
||||
await updateBatchStatusMutation.mutateAsync({
|
||||
tenantId: currentTenant?.id || '',
|
||||
batchId,
|
||||
statusUpdate: { status: ProductionStatusEnum.ON_HOLD }
|
||||
});
|
||||
toast.success('Lote pausado');
|
||||
} catch (error) {
|
||||
console.error('Error pausing batch:', error);
|
||||
toast.error('Error al pausar lote');
|
||||
}
|
||||
};
|
||||
|
||||
const handleViewDetails = (id: string) => {
|
||||
console.log('Viewing details for:', id);
|
||||
const handleViewDetails = (batchId: string) => {
|
||||
setSelectedBatchId(batchId);
|
||||
setShowBatchModal(true);
|
||||
};
|
||||
|
||||
const handleApprovePO = (poId: string) => {
|
||||
console.log('Approved PO:', poId);
|
||||
const handleApprovePO = async (poId: string) => {
|
||||
try {
|
||||
await approvePOMutation.mutateAsync({
|
||||
tenantId: currentTenant?.id || '',
|
||||
poId,
|
||||
notes: 'Aprobado desde el dashboard'
|
||||
});
|
||||
toast.success('Orden aprobada');
|
||||
} catch (error) {
|
||||
console.error('Error approving PO:', error);
|
||||
toast.error('Error al aprobar orden');
|
||||
}
|
||||
};
|
||||
|
||||
const handleRejectPO = (poId: string) => {
|
||||
console.log('Rejected PO:', poId);
|
||||
const handleRejectPO = async (poId: string) => {
|
||||
try {
|
||||
await rejectPOMutation.mutateAsync({
|
||||
tenantId: currentTenant?.id || '',
|
||||
poId,
|
||||
reason: 'Rechazado desde el dashboard'
|
||||
});
|
||||
toast.success('Orden rechazada');
|
||||
} catch (error) {
|
||||
console.error('Error rejecting PO:', error);
|
||||
toast.error('Error al rechazar orden');
|
||||
}
|
||||
};
|
||||
|
||||
const handleViewPODetails = (poId: string) => {
|
||||
console.log('Viewing PO details:', poId);
|
||||
navigate(`/app/suppliers/purchase-orders/${poId}`);
|
||||
setSelectedPOId(poId);
|
||||
setShowPOModal(true);
|
||||
};
|
||||
|
||||
const handleViewAllPOs = () => {
|
||||
@@ -178,6 +263,114 @@ const DashboardPage: React.FC = () => {
|
||||
];
|
||||
}, [dashboardStats, t]);
|
||||
|
||||
// Helper function to build PO detail sections (reused from ProcurementPage)
|
||||
const buildPODetailsSections = (po: any) => {
|
||||
if (!po) return [];
|
||||
|
||||
const getPOStatusConfig = (status: string) => {
|
||||
const normalizedStatus = status?.toUpperCase().replace(/_/g, '_');
|
||||
const configs: Record<string, any> = {
|
||||
PENDING_APPROVAL: { text: 'Pendiente de Aprobación', color: 'var(--color-warning)' },
|
||||
APPROVED: { text: 'Aprobado', color: 'var(--color-success)' },
|
||||
SENT_TO_SUPPLIER: { text: 'Enviado al Proveedor', color: 'var(--color-info)' },
|
||||
CONFIRMED: { text: 'Confirmado', color: 'var(--color-success)' },
|
||||
RECEIVED: { text: 'Recibido', color: 'var(--color-success)' },
|
||||
COMPLETED: { text: 'Completado', color: 'var(--color-success)' },
|
||||
CANCELLED: { text: 'Cancelado', color: 'var(--color-error)' },
|
||||
};
|
||||
return configs[normalizedStatus] || { text: status, color: 'var(--color-info)' };
|
||||
};
|
||||
|
||||
const statusConfig = getPOStatusConfig(po.status);
|
||||
|
||||
return [
|
||||
{
|
||||
title: 'Información General',
|
||||
icon: FileText,
|
||||
fields: [
|
||||
{ label: 'Número de Orden', value: po.po_number, type: 'text' as const },
|
||||
{ label: 'Estado', value: statusConfig.text, type: 'status' as const },
|
||||
{ label: 'Prioridad', value: po.priority === 'urgent' ? 'Urgente' : po.priority === 'high' ? 'Alta' : po.priority === 'low' ? 'Baja' : 'Normal', type: 'text' as const },
|
||||
{ label: 'Fecha de Creación', value: new Date(po.created_at).toLocaleDateString('es-ES', { year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit' }), type: 'text' as const }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Información del Proveedor',
|
||||
icon: Building2,
|
||||
fields: [
|
||||
{ label: 'Proveedor', value: po.supplier?.name || po.supplier_name || 'N/A', type: 'text' as const },
|
||||
{ label: 'Email', value: po.supplier?.contact_email || 'N/A', type: 'text' as const },
|
||||
{ label: 'Teléfono', value: po.supplier?.contact_phone || 'N/A', type: 'text' as const }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Resumen Financiero',
|
||||
icon: Euro,
|
||||
fields: [
|
||||
{ label: 'Subtotal', value: `€${(typeof po.subtotal === 'string' ? parseFloat(po.subtotal) : po.subtotal || 0).toFixed(2)}`, type: 'text' as const },
|
||||
{ label: 'Impuestos', value: `€${(typeof po.tax_amount === 'string' ? parseFloat(po.tax_amount) : po.tax_amount || 0).toFixed(2)}`, type: 'text' as const },
|
||||
{ label: 'TOTAL', value: `€${(typeof po.total_amount === 'string' ? parseFloat(po.total_amount) : po.total_amount || 0).toFixed(2)}`, type: 'text' as const, highlight: true }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Entrega',
|
||||
icon: Calendar,
|
||||
fields: [
|
||||
{ label: 'Fecha Requerida', value: po.required_delivery_date ? new Date(po.required_delivery_date).toLocaleDateString('es-ES', { year: 'numeric', month: 'long', day: 'numeric' }) : 'No especificada', type: 'text' as const },
|
||||
{ label: 'Fecha Esperada', value: po.expected_delivery_date ? new Date(po.expected_delivery_date).toLocaleDateString('es-ES', { year: 'numeric', month: 'long', day: 'numeric' }) : 'No especificada', type: 'text' as const }
|
||||
]
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
// Helper function to build Production batch detail sections
|
||||
const buildBatchDetailsSections = (batch: any) => {
|
||||
if (!batch) return [];
|
||||
|
||||
return [
|
||||
{
|
||||
title: 'Información General',
|
||||
icon: Package,
|
||||
fields: [
|
||||
{ label: 'Producto', value: batch.product_name, type: 'text' as const, highlight: true },
|
||||
{ label: 'Número de Lote', value: batch.batch_number, type: 'text' as const },
|
||||
{ label: 'Cantidad Planificada', value: `${batch.planned_quantity} unidades`, type: 'text' as const },
|
||||
{ label: 'Cantidad Real', value: batch.actual_quantity ? `${batch.actual_quantity} unidades` : 'Pendiente', type: 'text' as const },
|
||||
{ label: 'Estado', value: batch.status, type: 'text' as const },
|
||||
{ label: 'Prioridad', value: batch.priority, type: 'text' as const }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Cronograma',
|
||||
icon: Clock,
|
||||
fields: [
|
||||
{ label: 'Inicio Planificado', value: batch.planned_start_time ? new Date(batch.planned_start_time).toLocaleString('es-ES') : 'No especificado', type: 'text' as const },
|
||||
{ label: 'Fin Planificado', value: batch.planned_end_time ? new Date(batch.planned_end_time).toLocaleString('es-ES') : 'No especificado', type: 'text' as const },
|
||||
{ label: 'Inicio Real', value: batch.actual_start_time ? new Date(batch.actual_start_time).toLocaleString('es-ES') : 'Pendiente', type: 'text' as const },
|
||||
{ label: 'Fin Real', value: batch.actual_end_time ? new Date(batch.actual_end_time).toLocaleString('es-ES') : 'Pendiente', type: 'text' as const }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Producción',
|
||||
icon: Factory,
|
||||
fields: [
|
||||
{ label: 'Personal Asignado', value: batch.staff_assigned?.join(', ') || 'No asignado', type: 'text' as const },
|
||||
{ label: 'Estación', value: batch.station_id || 'No asignada', type: 'text' as const },
|
||||
{ label: 'Duración Planificada', value: batch.planned_duration_minutes ? `${batch.planned_duration_minutes} minutos` : 'No especificada', type: 'text' as const }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Calidad y Costos',
|
||||
icon: CheckCircle,
|
||||
fields: [
|
||||
{ label: 'Puntuación de Calidad', value: batch.quality_score ? `${batch.quality_score}/10` : 'Pendiente', type: 'text' as const },
|
||||
{ label: 'Rendimiento', value: batch.yield_percentage ? `${batch.yield_percentage}%` : 'Calculando...', type: 'text' as const },
|
||||
{ label: 'Costo Estimado', value: batch.estimated_cost ? `€${batch.estimated_cost}` : '€0.00', type: 'text' as const },
|
||||
{ label: 'Costo Real', value: batch.actual_cost ? `€${batch.actual_cost}` : '€0.00', type: 'text' as const }
|
||||
]
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6 p-4 sm:p-6">
|
||||
@@ -213,14 +406,26 @@ const DashboardPage: React.FC = () => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Dashboard Content - Four Main Sections */}
|
||||
{/* Dashboard Content - Main Sections */}
|
||||
<div className="space-y-6">
|
||||
{/* 1. Real-time Alerts */}
|
||||
<div data-tour="real-time-alerts">
|
||||
<RealTimeAlerts />
|
||||
</div>
|
||||
|
||||
{/* 2. Pending PO Approvals - What purchase orders need approval? */}
|
||||
{/* 2. Sustainability Impact - NEW! */}
|
||||
<div data-tour="sustainability-widget">
|
||||
<SustainabilityWidget
|
||||
days={30}
|
||||
onViewDetails={() => navigate('/app/analytics/sustainability')}
|
||||
onExportReport={() => {
|
||||
// TODO: Implement export modal
|
||||
console.log('Export sustainability report');
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 3. Pending PO Approvals - What purchase orders need approval? */}
|
||||
<div data-tour="pending-po-approvals">
|
||||
<PendingPOApprovals
|
||||
onApprovePO={handleApprovePO}
|
||||
@@ -231,7 +436,7 @@ const DashboardPage: React.FC = () => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 3. Today's Production - What needs to be produced today? */}
|
||||
{/* 4. Today's Production - What needs to be produced today? */}
|
||||
<div data-tour="today-production">
|
||||
<TodayProduction
|
||||
onStartBatch={handleStartBatch}
|
||||
@@ -242,6 +447,150 @@ const DashboardPage: React.FC = () => {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Purchase Order Details Modal */}
|
||||
{showPOModal && poDetails && (
|
||||
<EditViewModal
|
||||
isOpen={showPOModal}
|
||||
onClose={() => {
|
||||
setShowPOModal(false);
|
||||
setSelectedPOId(null);
|
||||
}}
|
||||
title={`Orden de Compra: ${poDetails.po_number}`}
|
||||
subtitle={`Proveedor: ${poDetails.supplier?.name || poDetails.supplier_name || 'N/A'}`}
|
||||
mode="view"
|
||||
sections={buildPODetailsSections(poDetails)}
|
||||
loading={isLoadingPO}
|
||||
statusIndicator={{
|
||||
color: poDetails.status === 'PENDING_APPROVAL' ? 'var(--color-warning)' :
|
||||
poDetails.status === 'APPROVED' ? 'var(--color-success)' :
|
||||
'var(--color-info)',
|
||||
text: poDetails.status === 'PENDING_APPROVAL' ? 'Pendiente de Aprobación' :
|
||||
poDetails.status === 'APPROVED' ? 'Aprobado' :
|
||||
poDetails.status || 'N/A',
|
||||
icon: ShoppingCart
|
||||
}}
|
||||
actions={
|
||||
poDetails.status === 'PENDING_APPROVAL' ? [
|
||||
{
|
||||
label: 'Aprobar',
|
||||
onClick: async () => {
|
||||
try {
|
||||
await approvePOMutation.mutateAsync({
|
||||
tenantId: currentTenant?.id || '',
|
||||
poId: poDetails.id,
|
||||
notes: 'Aprobado desde el dashboard'
|
||||
});
|
||||
toast.success('Orden aprobada');
|
||||
setShowPOModal(false);
|
||||
setSelectedPOId(null);
|
||||
} catch (error) {
|
||||
console.error('Error approving PO:', error);
|
||||
toast.error('Error al aprobar orden');
|
||||
}
|
||||
},
|
||||
variant: 'primary' as const,
|
||||
icon: CheckCircle
|
||||
},
|
||||
{
|
||||
label: 'Rechazar',
|
||||
onClick: async () => {
|
||||
try {
|
||||
await rejectPOMutation.mutateAsync({
|
||||
tenantId: currentTenant?.id || '',
|
||||
poId: poDetails.id,
|
||||
reason: 'Rechazado desde el dashboard'
|
||||
});
|
||||
toast.success('Orden rechazada');
|
||||
setShowPOModal(false);
|
||||
setSelectedPOId(null);
|
||||
} catch (error) {
|
||||
console.error('Error rejecting PO:', error);
|
||||
toast.error('Error al rechazar orden');
|
||||
}
|
||||
},
|
||||
variant: 'outline' as const,
|
||||
icon: X
|
||||
}
|
||||
] : undefined
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Production Batch Details Modal */}
|
||||
{showBatchModal && batchDetails && (
|
||||
<EditViewModal
|
||||
isOpen={showBatchModal}
|
||||
onClose={() => {
|
||||
setShowBatchModal(false);
|
||||
setSelectedBatchId(null);
|
||||
}}
|
||||
title={batchDetails.product_name}
|
||||
subtitle={`Lote #${batchDetails.batch_number}`}
|
||||
mode="view"
|
||||
sections={buildBatchDetailsSections(batchDetails)}
|
||||
loading={isLoadingBatch}
|
||||
statusIndicator={{
|
||||
color: batchDetails.status === 'PENDING' ? 'var(--color-warning)' :
|
||||
batchDetails.status === 'IN_PROGRESS' ? 'var(--color-info)' :
|
||||
batchDetails.status === 'COMPLETED' ? 'var(--color-success)' :
|
||||
batchDetails.status === 'FAILED' ? 'var(--color-error)' :
|
||||
'var(--color-info)',
|
||||
text: batchDetails.status === 'PENDING' ? 'Pendiente' :
|
||||
batchDetails.status === 'IN_PROGRESS' ? 'En Progreso' :
|
||||
batchDetails.status === 'COMPLETED' ? 'Completado' :
|
||||
batchDetails.status === 'FAILED' ? 'Fallido' :
|
||||
batchDetails.status === 'ON_HOLD' ? 'Pausado' :
|
||||
batchDetails.status || 'N/A',
|
||||
icon: Factory
|
||||
}}
|
||||
actions={
|
||||
batchDetails.status === 'PENDING' ? [
|
||||
{
|
||||
label: 'Iniciar Lote',
|
||||
onClick: async () => {
|
||||
try {
|
||||
await updateBatchStatusMutation.mutateAsync({
|
||||
tenantId: currentTenant?.id || '',
|
||||
batchId: batchDetails.id,
|
||||
statusUpdate: { status: ProductionStatusEnum.IN_PROGRESS }
|
||||
});
|
||||
toast.success('Lote iniciado');
|
||||
setShowBatchModal(false);
|
||||
setSelectedBatchId(null);
|
||||
} catch (error) {
|
||||
console.error('Error starting batch:', error);
|
||||
toast.error('Error al iniciar lote');
|
||||
}
|
||||
},
|
||||
variant: 'primary' as const,
|
||||
icon: CheckCircle
|
||||
}
|
||||
] : batchDetails.status === 'IN_PROGRESS' ? [
|
||||
{
|
||||
label: 'Pausar Lote',
|
||||
onClick: async () => {
|
||||
try {
|
||||
await updateBatchStatusMutation.mutateAsync({
|
||||
tenantId: currentTenant?.id || '',
|
||||
batchId: batchDetails.id,
|
||||
statusUpdate: { status: ProductionStatusEnum.ON_HOLD }
|
||||
});
|
||||
toast.success('Lote pausado');
|
||||
setShowBatchModal(false);
|
||||
setSelectedBatchId(null);
|
||||
} catch (error) {
|
||||
console.error('Error pausing batch:', error);
|
||||
toast.error('Error al pausar lote');
|
||||
}
|
||||
},
|
||||
variant: 'outline' as const,
|
||||
icon: X
|
||||
}
|
||||
] : undefined
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user