Add fixes to procurement logic and fix rel-time connections
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Plus, ShoppingCart, Truck, Euro, Calendar, Clock, CheckCircle, AlertCircle, Package, Eye, Loader, Edit, ArrowRight, X, Save, Building2, Play, Zap, User } from 'lucide-react';
|
||||
import { Button, StatsGrid, StatusCard, getStatusColor, EditViewModal, SearchAndFilter, type FilterConfig } from '../../../../components/ui';
|
||||
import { Button, Card, StatsGrid, StatusCard, getStatusColor, EditViewModal, SearchAndFilter, Input, type FilterConfig } from '../../../../components/ui';
|
||||
import { formatters } from '../../../../components/ui/Stats/StatsPresets';
|
||||
import { PageHeader } from '../../../../components/layout';
|
||||
import { CreatePurchaseOrderModal } from '../../../../components/domain/procurement/CreatePurchaseOrderModal';
|
||||
@@ -10,7 +10,13 @@ import {
|
||||
usePlanRequirements,
|
||||
useGenerateProcurementPlan,
|
||||
useUpdateProcurementPlanStatus,
|
||||
useTriggerDailyScheduler
|
||||
useTriggerDailyScheduler,
|
||||
useRecalculateProcurementPlan,
|
||||
useApproveProcurementPlan,
|
||||
useRejectProcurementPlan,
|
||||
useCreatePurchaseOrdersFromPlan,
|
||||
useLinkRequirementToPurchaseOrder,
|
||||
useUpdateRequirementDeliveryStatus
|
||||
} from '../../../../api';
|
||||
import { useTenantStore } from '../../../../stores/tenant.store';
|
||||
|
||||
@@ -38,6 +44,20 @@ const ProcurementPage: React.FC = () => {
|
||||
force_regenerate: false
|
||||
});
|
||||
|
||||
// New feature state
|
||||
const [showApprovalModal, setShowApprovalModal] = useState(false);
|
||||
const [approvalAction, setApprovalAction] = useState<'approve' | 'reject'>('approve');
|
||||
const [approvalNotes, setApprovalNotes] = useState('');
|
||||
const [planForApproval, setPlanForApproval] = useState<any>(null);
|
||||
const [showDeliveryUpdateModal, setShowDeliveryUpdateModal] = useState(false);
|
||||
const [requirementForDelivery, setRequirementForDelivery] = useState<any>(null);
|
||||
const [deliveryUpdateForm, setDeliveryUpdateForm] = useState({
|
||||
delivery_status: 'pending',
|
||||
received_quantity: 0,
|
||||
actual_delivery_date: '',
|
||||
quality_rating: 5
|
||||
});
|
||||
|
||||
|
||||
// Requirement details functionality
|
||||
const handleViewRequirementDetails = (requirement: any) => {
|
||||
@@ -82,6 +102,13 @@ const ProcurementPage: React.FC = () => {
|
||||
const updatePlanStatusMutation = useUpdateProcurementPlanStatus();
|
||||
const triggerSchedulerMutation = useTriggerDailyScheduler();
|
||||
|
||||
// New feature mutations
|
||||
const recalculatePlanMutation = useRecalculateProcurementPlan();
|
||||
const approvePlanMutation = useApproveProcurementPlan();
|
||||
const rejectPlanMutation = useRejectProcurementPlan();
|
||||
const createPOsMutation = useCreatePurchaseOrdersFromPlan();
|
||||
const updateDeliveryMutation = useUpdateRequirementDeliveryStatus();
|
||||
|
||||
// Helper functions for stage transitions and edit functionality
|
||||
const getNextStage = (currentStatus: string): string | null => {
|
||||
const stageFlow: { [key: string]: string } = {
|
||||
@@ -158,6 +185,96 @@ const ProcurementPage: React.FC = () => {
|
||||
setSelectedPlanForRequirements(null);
|
||||
};
|
||||
|
||||
// NEW FEATURE HANDLERS
|
||||
const handleRecalculatePlan = (plan: any) => {
|
||||
if (window.confirm('¿Recalcular el plan con el inventario actual? Esto puede cambiar las cantidades requeridas.')) {
|
||||
recalculatePlanMutation.mutate({ tenantId, planId: plan.id });
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenApprovalModal = (plan: any, action: 'approve' | 'reject') => {
|
||||
setPlanForApproval(plan);
|
||||
setApprovalAction(action);
|
||||
setApprovalNotes('');
|
||||
setShowApprovalModal(true);
|
||||
};
|
||||
|
||||
const handleConfirmApproval = () => {
|
||||
if (!planForApproval) return;
|
||||
|
||||
if (approvalAction === 'approve') {
|
||||
approvePlanMutation.mutate({
|
||||
tenantId,
|
||||
planId: planForApproval.id,
|
||||
approval_notes: approvalNotes || undefined
|
||||
}, {
|
||||
onSuccess: () => {
|
||||
setShowApprovalModal(false);
|
||||
setPlanForApproval(null);
|
||||
setApprovalNotes('');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
rejectPlanMutation.mutate({
|
||||
tenantId,
|
||||
planId: planForApproval.id,
|
||||
rejection_notes: approvalNotes || undefined
|
||||
}, {
|
||||
onSuccess: () => {
|
||||
setShowApprovalModal(false);
|
||||
setPlanForApproval(null);
|
||||
setApprovalNotes('');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreatePurchaseOrders = (plan: any) => {
|
||||
if (plan.status !== 'approved') {
|
||||
alert('El plan debe estar aprobado antes de crear órdenes de compra');
|
||||
return;
|
||||
}
|
||||
|
||||
if (window.confirm(`¿Crear órdenes de compra automáticamente para ${plan.total_requirements} requerimientos?`)) {
|
||||
createPOsMutation.mutate({
|
||||
tenantId,
|
||||
planId: plan.id,
|
||||
autoApprove: false
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenDeliveryUpdate = (requirement: any) => {
|
||||
setRequirementForDelivery(requirement);
|
||||
setDeliveryUpdateForm({
|
||||
delivery_status: requirement.delivery_status || 'pending',
|
||||
received_quantity: requirement.received_quantity || 0,
|
||||
actual_delivery_date: requirement.actual_delivery_date || '',
|
||||
quality_rating: requirement.quality_rating || 5
|
||||
});
|
||||
setShowDeliveryUpdateModal(true);
|
||||
};
|
||||
|
||||
const handleConfirmDeliveryUpdate = () => {
|
||||
if (!requirementForDelivery) return;
|
||||
|
||||
updateDeliveryMutation.mutate({
|
||||
tenantId,
|
||||
requirementId: requirementForDelivery.id,
|
||||
request: {
|
||||
delivery_status: deliveryUpdateForm.delivery_status,
|
||||
received_quantity: deliveryUpdateForm.received_quantity || undefined,
|
||||
actual_delivery_date: deliveryUpdateForm.actual_delivery_date || undefined,
|
||||
quality_rating: deliveryUpdateForm.quality_rating || undefined
|
||||
}
|
||||
}, {
|
||||
onSuccess: () => {
|
||||
setShowDeliveryUpdateModal(false);
|
||||
setRequirementForDelivery(null);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if (!tenantId) {
|
||||
return (
|
||||
<div className="flex justify-center items-center h-64">
|
||||
@@ -413,8 +530,74 @@ const ProcurementPage: React.FC = () => {
|
||||
}
|
||||
);
|
||||
} else {
|
||||
// Primary action: Stage transition (most important)
|
||||
if (nextStageConfig) {
|
||||
// NEW FEATURES: Recalculate and Approval actions for draft/pending
|
||||
if (plan.status === 'draft') {
|
||||
const planAgeHours = (new Date().getTime() - new Date(plan.created_at).getTime()) / (1000 * 60 * 60);
|
||||
|
||||
actions.push({
|
||||
label: planAgeHours > 24 ? '⚠️ Recalcular' : 'Recalcular',
|
||||
icon: ArrowRight,
|
||||
variant: 'outline' as const,
|
||||
priority: 'primary' as const,
|
||||
onClick: () => handleRecalculatePlan(plan)
|
||||
});
|
||||
|
||||
actions.push({
|
||||
label: 'Aprobar',
|
||||
icon: CheckCircle,
|
||||
variant: 'primary' as const,
|
||||
priority: 'primary' as const,
|
||||
onClick: () => handleOpenApprovalModal(plan, 'approve')
|
||||
});
|
||||
|
||||
actions.push({
|
||||
label: 'Rechazar',
|
||||
icon: X,
|
||||
variant: 'outline' as const,
|
||||
priority: 'secondary' as const,
|
||||
destructive: true,
|
||||
onClick: () => handleOpenApprovalModal(plan, 'reject')
|
||||
});
|
||||
} else if (plan.status === 'pending_approval') {
|
||||
actions.push({
|
||||
label: 'Aprobar',
|
||||
icon: CheckCircle,
|
||||
variant: 'primary' as const,
|
||||
priority: 'primary' as const,
|
||||
onClick: () => handleOpenApprovalModal(plan, 'approve')
|
||||
});
|
||||
|
||||
actions.push({
|
||||
label: 'Rechazar',
|
||||
icon: X,
|
||||
variant: 'outline' as const,
|
||||
priority: 'secondary' as const,
|
||||
destructive: true,
|
||||
onClick: () => handleOpenApprovalModal(plan, 'reject')
|
||||
});
|
||||
}
|
||||
|
||||
// NEW FEATURE: Auto-create POs for approved plans
|
||||
if (plan.status === 'approved') {
|
||||
actions.push({
|
||||
label: 'Crear Órdenes de Compra',
|
||||
icon: ShoppingCart,
|
||||
variant: 'primary' as const,
|
||||
priority: 'primary' as const,
|
||||
onClick: () => handleCreatePurchaseOrders(plan)
|
||||
});
|
||||
|
||||
actions.push({
|
||||
label: 'Iniciar Ejecución',
|
||||
icon: Play,
|
||||
variant: 'outline' as const,
|
||||
priority: 'secondary' as const,
|
||||
onClick: () => handleStageTransition(plan.id, plan.status)
|
||||
});
|
||||
}
|
||||
|
||||
// Original stage transition for other statuses
|
||||
if (nextStageConfig && !['draft', 'pending_approval', 'approved'].includes(plan.status)) {
|
||||
actions.push({
|
||||
label: nextStageConfig.label,
|
||||
icon: nextStageConfig.icon,
|
||||
@@ -459,7 +642,7 @@ const ProcurementPage: React.FC = () => {
|
||||
// Tertiary action: Cancel (least prominent, destructive)
|
||||
if (!['completed', 'cancelled'].includes(plan.status)) {
|
||||
actions.push({
|
||||
label: 'Cancelar',
|
||||
label: 'Cancelar Plan',
|
||||
icon: X,
|
||||
variant: 'outline' as const,
|
||||
priority: 'tertiary' as const,
|
||||
@@ -1281,6 +1464,229 @@ const ProcurementPage: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* NEW FEATURE MODALS */}
|
||||
|
||||
{/* Approval/Rejection Modal */}
|
||||
{showApprovalModal && planForApproval && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
|
||||
<div className="bg-[var(--bg-primary)] rounded-lg shadow-xl w-full max-w-md mx-4">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-6 border-b border-[var(--border-primary)]">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className={`p-2 rounded-lg ${approvalAction === 'approve' ? 'bg-green-100' : 'bg-red-100'}`}>
|
||||
{approvalAction === 'approve' ? (
|
||||
<CheckCircle className={`w-5 h-5 text-green-600`} />
|
||||
) : (
|
||||
<X className={`w-5 h-5 text-red-600`} />
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-[var(--text-primary)]">
|
||||
{approvalAction === 'approve' ? 'Aprobar Plan' : 'Rechazar Plan'}
|
||||
</h2>
|
||||
<p className="text-sm text-[var(--text-secondary)]">
|
||||
Plan {planForApproval.plan_number}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setShowApprovalModal(false)}
|
||||
className="p-2"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-6 space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
Notas {approvalAction === 'approve' ? '(Opcional)' : '(Requerido)'}
|
||||
</label>
|
||||
<textarea
|
||||
value={approvalNotes}
|
||||
onChange={(e) => setApprovalNotes(e.target.value)}
|
||||
placeholder={approvalAction === 'approve' ? 'Razón de aprobación...' : 'Razón de rechazo...'}
|
||||
className="w-full px-3 py-2 text-sm border border-[var(--border-primary)] rounded-lg
|
||||
bg-[var(--bg-primary)] text-[var(--text-primary)] placeholder-[var(--text-tertiary)]
|
||||
focus:ring-2 focus:ring-[var(--color-primary)]/20 focus:border-[var(--color-primary)]
|
||||
transition-colors duration-200 resize-vertical min-h-[100px]"
|
||||
rows={4}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="bg-[var(--bg-secondary)] p-4 rounded-lg">
|
||||
<h4 className="text-sm font-medium text-[var(--text-primary)] mb-2">Detalles del Plan</h4>
|
||||
<div className="space-y-1 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-[var(--text-secondary)]">Requerimientos:</span>
|
||||
<span className="text-[var(--text-primary)] font-medium">{planForApproval.total_requirements}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-[var(--text-secondary)]">Costo Estimado:</span>
|
||||
<span className="text-[var(--text-primary)] font-medium">€{planForApproval.total_estimated_cost?.toFixed(2)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-[var(--text-secondary)]">Proveedores:</span>
|
||||
<span className="text-[var(--text-primary)] font-medium">{planForApproval.primary_suppliers_count || 0}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex justify-end space-x-3 p-6 border-t border-[var(--border-primary)]">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setShowApprovalModal(false)}
|
||||
>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button
|
||||
variant={approvalAction === 'approve' ? 'primary' : 'outline'}
|
||||
onClick={handleConfirmApproval}
|
||||
disabled={approvePlanMutation.isPending || rejectPlanMutation.isPending}
|
||||
className={approvalAction === 'reject' ? 'bg-red-600 hover:bg-red-700 text-white' : ''}
|
||||
>
|
||||
{approvePlanMutation.isPending || rejectPlanMutation.isPending ? (
|
||||
<>
|
||||
<Loader className="w-4 h-4 mr-2 animate-spin" />
|
||||
Procesando...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{approvalAction === 'approve' ? <CheckCircle className="w-4 h-4 mr-2" /> : <X className="w-4 h-4 mr-2" />}
|
||||
{approvalAction === 'approve' ? 'Aprobar Plan' : 'Rechazar Plan'}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Delivery Status Update Modal */}
|
||||
{showDeliveryUpdateModal && requirementForDelivery && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
|
||||
<div className="bg-[var(--bg-primary)] rounded-lg shadow-xl w-full max-w-lg mx-4">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-6 border-b border-[var(--border-primary)]">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="p-2 rounded-lg bg-blue-100">
|
||||
<Truck className="w-5 h-5 text-blue-600" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-[var(--text-primary)]">
|
||||
Actualizar Estado de Entrega
|
||||
</h2>
|
||||
<p className="text-sm text-[var(--text-secondary)]">
|
||||
{requirementForDelivery.product_name}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setShowDeliveryUpdateModal(false)}
|
||||
className="p-2"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-6 space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
Estado de Entrega
|
||||
</label>
|
||||
<select
|
||||
value={deliveryUpdateForm.delivery_status}
|
||||
onChange={(e) => setDeliveryUpdateForm({ ...deliveryUpdateForm, delivery_status: e.target.value })}
|
||||
className="w-full px-3 py-2 text-sm border border-[var(--border-primary)] rounded-lg
|
||||
bg-[var(--bg-primary)] text-[var(--text-primary)]
|
||||
focus:ring-2 focus:ring-[var(--color-primary)]/20 focus:border-[var(--color-primary)]"
|
||||
>
|
||||
<option value="pending">Pendiente</option>
|
||||
<option value="in_transit">En Tránsito</option>
|
||||
<option value="delivered">Entregado</option>
|
||||
<option value="delayed">Retrasado</option>
|
||||
<option value="cancelled">Cancelado</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
Cantidad Recibida
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
value={deliveryUpdateForm.received_quantity}
|
||||
onChange={(e) => setDeliveryUpdateForm({ ...deliveryUpdateForm, received_quantity: Number(e.target.value) })}
|
||||
min="0"
|
||||
step="0.01"
|
||||
/>
|
||||
<p className="text-xs text-[var(--text-tertiary)] mt-1">
|
||||
Ordenado: {requirementForDelivery.ordered_quantity || requirementForDelivery.net_requirement} {requirementForDelivery.unit_of_measure}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
Fecha de Entrega Real
|
||||
</label>
|
||||
<Input
|
||||
type="date"
|
||||
value={deliveryUpdateForm.actual_delivery_date}
|
||||
onChange={(e) => setDeliveryUpdateForm({ ...deliveryUpdateForm, actual_delivery_date: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
Calificación de Calidad (1-10)
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
value={deliveryUpdateForm.quality_rating}
|
||||
onChange={(e) => setDeliveryUpdateForm({ ...deliveryUpdateForm, quality_rating: Number(e.target.value) })}
|
||||
min="1"
|
||||
max="10"
|
||||
step="0.1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex justify-end space-x-3 p-6 border-t border-[var(--border-primary)]">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setShowDeliveryUpdateModal(false)}
|
||||
>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={handleConfirmDeliveryUpdate}
|
||||
disabled={updateDeliveryMutation.isPending}
|
||||
>
|
||||
{updateDeliveryMutation.isPending ? (
|
||||
<>
|
||||
<Loader className="w-4 h-4 mr-2 animate-spin" />
|
||||
Actualizando...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Save className="w-4 h-4 mr-2" />
|
||||
Actualizar Estado
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user