1274 lines
55 KiB
TypeScript
1274 lines
55 KiB
TypeScript
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, Input, Card, StatsGrid, StatusCard, getStatusColor, StatusModal } from '../../../../components/ui';
|
|
import { formatters } from '../../../../components/ui/Stats/StatsPresets';
|
|
import { PageHeader } from '../../../../components/layout';
|
|
import { CreatePurchaseOrderModal } from '../../../../components/domain/procurement/CreatePurchaseOrderModal';
|
|
import {
|
|
useProcurementDashboard,
|
|
useProcurementPlans,
|
|
usePlanRequirements,
|
|
useGenerateProcurementPlan,
|
|
useUpdateProcurementPlanStatus,
|
|
useTriggerDailyScheduler
|
|
} from '../../../../api';
|
|
import { useTenantStore } from '../../../../stores/tenant.store';
|
|
|
|
const ProcurementPage: React.FC = () => {
|
|
const [searchTerm, setSearchTerm] = useState('');
|
|
const [showForm, setShowForm] = useState(false);
|
|
const [modalMode, setModalMode] = useState<'view' | 'edit'>('view');
|
|
const [selectedPlan, setSelectedPlan] = useState<any>(null);
|
|
const [editingPlan, setEditingPlan] = useState<any>(null);
|
|
const [editFormData, setEditFormData] = useState<any>({});
|
|
const [selectedPlanForRequirements, setSelectedPlanForRequirements] = useState<string | null>(null);
|
|
const [showCriticalRequirements, setShowCriticalRequirements] = useState(false);
|
|
const [showGeneratePlanModal, setShowGeneratePlanModal] = useState(false);
|
|
const [showRequirementDetailsModal, setShowRequirementDetailsModal] = useState(false);
|
|
const [selectedRequirement, setSelectedRequirement] = useState<any>(null);
|
|
const [showCreatePurchaseOrderModal, setShowCreatePurchaseOrderModal] = useState(false);
|
|
const [selectedRequirementsForPO, setSelectedRequirementsForPO] = useState<any[]>([]);
|
|
const [isAIMode, setIsAIMode] = useState(true);
|
|
const [generatePlanForm, setGeneratePlanForm] = useState({
|
|
plan_date: new Date().toISOString().split('T')[0],
|
|
planning_horizon_days: 14,
|
|
include_safety_stock: true,
|
|
safety_stock_percentage: 20,
|
|
force_regenerate: false
|
|
});
|
|
|
|
|
|
// Requirement details functionality
|
|
const handleViewRequirementDetails = (requirement: any) => {
|
|
setSelectedRequirement(requirement);
|
|
setShowRequirementDetailsModal(true);
|
|
};
|
|
|
|
const { currentTenant } = useTenantStore();
|
|
const tenantId = currentTenant?.id || '';
|
|
|
|
// Real API data hooks
|
|
const { data: dashboardData, isLoading: isDashboardLoading } = useProcurementDashboard(tenantId);
|
|
const { data: procurementPlans, isLoading: isPlansLoading } = useProcurementPlans({
|
|
tenant_id: tenantId,
|
|
limit: 50,
|
|
offset: 0
|
|
});
|
|
|
|
// Get plan requirements for selected plan
|
|
const { data: allPlanRequirements, isLoading: isPlanRequirementsLoading } = usePlanRequirements({
|
|
tenant_id: tenantId,
|
|
plan_id: selectedPlanForRequirements || ''
|
|
// Remove status filter to get all requirements
|
|
}, {
|
|
enabled: !!selectedPlanForRequirements && !!tenantId
|
|
});
|
|
|
|
// Filter critical requirements client-side
|
|
const planRequirements = allPlanRequirements?.filter(req => {
|
|
// Check various conditions that might make a requirement critical
|
|
const isLowStock = req.current_stock_level && req.required_quantity &&
|
|
(req.current_stock_level / req.required_quantity) < 0.5;
|
|
const isNearDeadline = req.required_by_date &&
|
|
(new Date(req.required_by_date).getTime() - Date.now()) / (1000 * 60 * 60 * 24) < 7;
|
|
const hasHighPriority = req.priority === 'high';
|
|
|
|
return isLowStock || isNearDeadline || hasHighPriority;
|
|
});
|
|
|
|
|
|
const generatePlanMutation = useGenerateProcurementPlan();
|
|
const updatePlanStatusMutation = useUpdateProcurementPlanStatus();
|
|
const triggerSchedulerMutation = useTriggerDailyScheduler();
|
|
|
|
// Helper functions for stage transitions and edit functionality
|
|
const getNextStage = (currentStatus: string): string | null => {
|
|
const stageFlow: { [key: string]: string } = {
|
|
'draft': 'pending_approval',
|
|
'pending_approval': 'approved',
|
|
'approved': 'in_execution',
|
|
'in_execution': 'completed'
|
|
};
|
|
return stageFlow[currentStatus] || null;
|
|
};
|
|
|
|
const getStageActionConfig = (status: string) => {
|
|
const configs: { [key: string]: { label: string; icon: any; variant: 'primary' | 'outline'; color?: string } } = {
|
|
'draft': { label: 'Enviar a Aprobación', icon: ArrowRight, variant: 'primary' },
|
|
'pending_approval': { label: 'Aprobar', icon: CheckCircle, variant: 'primary' },
|
|
'approved': { label: 'Iniciar Ejecución', icon: Play, variant: 'primary' },
|
|
'in_execution': { label: 'Completar', icon: CheckCircle, variant: 'primary' }
|
|
};
|
|
return configs[status];
|
|
};
|
|
|
|
const canEdit = (status: string): boolean => {
|
|
return status === 'draft';
|
|
};
|
|
|
|
const handleStageTransition = (planId: string, currentStatus: string) => {
|
|
const nextStage = getNextStage(currentStatus);
|
|
if (nextStage) {
|
|
updatePlanStatusMutation.mutate({
|
|
tenant_id: tenantId,
|
|
plan_id: planId,
|
|
status: nextStage as any
|
|
});
|
|
}
|
|
};
|
|
|
|
const handleCancelPlan = (planId: string) => {
|
|
updatePlanStatusMutation.mutate({
|
|
tenant_id: tenantId,
|
|
plan_id: planId,
|
|
status: 'cancelled'
|
|
});
|
|
};
|
|
|
|
const handleEditPlan = (plan: any) => {
|
|
setEditingPlan(plan);
|
|
setEditFormData({
|
|
special_requirements: plan.special_requirements || '',
|
|
planning_horizon_days: plan.planning_horizon_days || 14,
|
|
priority: plan.priority || 'medium'
|
|
});
|
|
};
|
|
|
|
const handleSaveEdit = () => {
|
|
// For now, we'll just update the special requirements since that's the main editable field
|
|
// In a real implementation, you might have a separate API endpoint for updating plan details
|
|
setEditingPlan(null);
|
|
setEditFormData({});
|
|
// Here you would typically call an update API
|
|
};
|
|
|
|
const handleCancelEdit = () => {
|
|
setEditingPlan(null);
|
|
setEditFormData({});
|
|
};
|
|
|
|
const handleShowCriticalRequirements = (planId: string) => {
|
|
setSelectedPlanForRequirements(planId);
|
|
setShowCriticalRequirements(true);
|
|
};
|
|
|
|
const handleCloseCriticalRequirements = () => {
|
|
setShowCriticalRequirements(false);
|
|
setSelectedPlanForRequirements(null);
|
|
};
|
|
|
|
if (!tenantId) {
|
|
return (
|
|
<div className="flex justify-center items-center h-64">
|
|
<div className="text-center">
|
|
<h3 className="text-lg font-medium text-[var(--text-primary)] mb-2">
|
|
No hay tenant seleccionado
|
|
</h3>
|
|
<p className="text-[var(--text-secondary)]">
|
|
Selecciona un tenant para ver los datos de procurement
|
|
</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
|
|
const getPlanStatusConfig = (status: string) => {
|
|
const statusConfig = {
|
|
draft: { text: 'Borrador', icon: Clock },
|
|
pending_approval: { text: 'Pendiente Aprobación', icon: Clock },
|
|
approved: { text: 'Aprobado', icon: CheckCircle },
|
|
in_execution: { text: 'En Ejecución', icon: Truck },
|
|
completed: { text: 'Completado', icon: CheckCircle },
|
|
cancelled: { text: 'Cancelado', icon: AlertCircle },
|
|
};
|
|
|
|
const config = statusConfig[status as keyof typeof statusConfig];
|
|
const Icon = config?.icon;
|
|
|
|
return {
|
|
color: getStatusColor(status === 'in_execution' ? 'inTransit' : status === 'pending_approval' ? 'pending' : status),
|
|
text: config?.text || status,
|
|
icon: Icon,
|
|
isCritical: status === 'cancelled',
|
|
isHighlight: status === 'pending_approval'
|
|
};
|
|
};
|
|
|
|
const filteredPlans = procurementPlans?.plans?.filter(plan => {
|
|
const matchesSearch = plan.plan_number.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
plan.status.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
(plan.special_requirements && plan.special_requirements.toLowerCase().includes(searchTerm.toLowerCase()));
|
|
|
|
return matchesSearch;
|
|
}) || [];
|
|
|
|
|
|
const stats = {
|
|
totalPlans: dashboardData?.summary?.total_plans || 0,
|
|
activePlans: dashboardData?.summary?.active_plans || 0,
|
|
pendingRequirements: dashboardData?.summary?.pending_requirements || 0,
|
|
criticalRequirements: dashboardData?.summary?.critical_requirements || 0,
|
|
totalEstimatedCost: dashboardData?.summary?.total_estimated_cost || 0,
|
|
totalApprovedCost: dashboardData?.summary?.total_approved_cost || 0,
|
|
};
|
|
|
|
const procurementStats = [
|
|
{
|
|
title: 'Planes Totales',
|
|
value: stats.totalPlans,
|
|
variant: 'default' as const,
|
|
icon: Package,
|
|
},
|
|
{
|
|
title: 'Planes Activos',
|
|
value: stats.activePlans,
|
|
variant: 'success' as const,
|
|
icon: CheckCircle,
|
|
},
|
|
{
|
|
title: 'Requerimientos Pendientes',
|
|
value: stats.pendingRequirements,
|
|
variant: 'warning' as const,
|
|
icon: Clock,
|
|
},
|
|
{
|
|
title: 'Críticos',
|
|
value: stats.criticalRequirements,
|
|
variant: 'warning' as const,
|
|
icon: AlertCircle,
|
|
},
|
|
{
|
|
title: 'Costo Estimado',
|
|
value: formatters.currency(stats.totalEstimatedCost),
|
|
variant: 'info' as const,
|
|
icon: Euro,
|
|
},
|
|
{
|
|
title: 'Costo Aprobado',
|
|
value: formatters.currency(stats.totalApprovedCost),
|
|
variant: 'success' as const,
|
|
icon: Euro,
|
|
},
|
|
];
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div className="flex items-center justify-between mb-6">
|
|
<PageHeader
|
|
title="Planificación de Compras"
|
|
description="Administra planes de compras, requerimientos y análisis de procurement"
|
|
/>
|
|
<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>
|
|
|
|
{/* Action Buttons */}
|
|
{!isAIMode && (
|
|
<Button
|
|
variant="primary"
|
|
onClick={() => {
|
|
// Open the purchase order modal with empty requirements
|
|
// This allows manual creation of purchase orders without procurement plans
|
|
setSelectedRequirementsForPO([]);
|
|
setShowCreatePurchaseOrderModal(true);
|
|
}}
|
|
className="flex items-center gap-2"
|
|
>
|
|
<Plus className="w-4 h-4" />
|
|
Crear Orden de Compra
|
|
</Button>
|
|
)}
|
|
|
|
{/* Testing button - keep for development */}
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => {
|
|
triggerSchedulerMutation.mutate(tenantId, {
|
|
onSuccess: (data) => {
|
|
// Scheduler executed successfully
|
|
// Show success notification (if you have a notification system)
|
|
// toast.success(data.message);
|
|
},
|
|
onError: (error) => {
|
|
console.error('❌ Error ejecutando scheduler:', error);
|
|
// Show error notification
|
|
// toast.error('Error ejecutando el programador de compras');
|
|
}
|
|
})
|
|
}}
|
|
disabled={triggerSchedulerMutation.isPending}
|
|
className="flex items-center gap-2"
|
|
>
|
|
{triggerSchedulerMutation.isPending ? (
|
|
<Loader className="w-4 h-4 animate-spin" />
|
|
) : (
|
|
<Calendar className="w-4 h-4" />
|
|
)}
|
|
{triggerSchedulerMutation.isPending ? "Ejecutando..." : "Ejecutar Programador"}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Stats Grid */}
|
|
{isDashboardLoading ? (
|
|
<div className="flex justify-center items-center h-32">
|
|
<Loader className="w-8 h-8 animate-spin text-[var(--color-primary)]" />
|
|
</div>
|
|
) : (
|
|
<StatsGrid
|
|
stats={procurementStats}
|
|
columns={3}
|
|
/>
|
|
)}
|
|
|
|
|
|
<Card className="p-4">
|
|
<div className="flex flex-col sm:flex-row gap-4">
|
|
<div className="flex-1">
|
|
<Input
|
|
placeholder="Buscar planes por número, estado o notas..."
|
|
value={searchTerm}
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
className="w-full"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
|
|
{/* Procurement Plans Grid - Mobile-Optimized */}
|
|
<div className="grid gap-4 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-3">
|
|
{isPlansLoading ? (
|
|
<div className="col-span-full flex justify-center items-center h-32">
|
|
<Loader className="w-8 h-8 animate-spin text-[var(--color-primary)]" />
|
|
</div>
|
|
) : (
|
|
filteredPlans.map((plan) => {
|
|
const statusConfig = getPlanStatusConfig(plan.status);
|
|
const nextStageConfig = getStageActionConfig(plan.status);
|
|
const isEditing = editingPlan?.id === plan.id;
|
|
const isEditable = canEdit(plan.status);
|
|
|
|
// Build actions array with proper priority hierarchy for better UX
|
|
const actions = [];
|
|
|
|
// Edit mode actions (highest priority when editing)
|
|
if (isEditing) {
|
|
actions.push(
|
|
{
|
|
label: 'Guardar',
|
|
icon: Save,
|
|
variant: 'primary' as const,
|
|
priority: 'primary' as const,
|
|
onClick: handleSaveEdit
|
|
},
|
|
{
|
|
label: 'Cancelar',
|
|
icon: X,
|
|
variant: 'outline' as const,
|
|
priority: 'primary' as const,
|
|
destructive: true,
|
|
onClick: handleCancelEdit
|
|
}
|
|
);
|
|
} else {
|
|
// Primary action: Stage transition (most important)
|
|
if (nextStageConfig) {
|
|
actions.push({
|
|
label: nextStageConfig.label,
|
|
icon: nextStageConfig.icon,
|
|
variant: nextStageConfig.variant,
|
|
priority: 'primary' as const,
|
|
onClick: () => handleStageTransition(plan.id, plan.status)
|
|
});
|
|
}
|
|
|
|
// Secondary actions: Edit and View
|
|
if (isEditable) {
|
|
actions.push({
|
|
label: 'Editar',
|
|
icon: Edit,
|
|
variant: 'outline' as const,
|
|
priority: 'secondary' as const,
|
|
onClick: () => handleEditPlan(plan)
|
|
});
|
|
}
|
|
|
|
actions.push({
|
|
label: 'Ver',
|
|
icon: Eye,
|
|
variant: 'outline' as const,
|
|
priority: 'secondary' as const,
|
|
onClick: () => {
|
|
setSelectedPlan(plan);
|
|
setModalMode('view');
|
|
setShowForm(true);
|
|
}
|
|
});
|
|
|
|
// Show Critical Requirements button
|
|
actions.push({
|
|
label: 'Req. Críticos',
|
|
icon: AlertCircle,
|
|
variant: 'outline' as const,
|
|
priority: 'secondary' as const,
|
|
onClick: () => handleShowCriticalRequirements(plan.id)
|
|
});
|
|
|
|
// Tertiary action: Cancel (least prominent, destructive)
|
|
if (!['completed', 'cancelled'].includes(plan.status)) {
|
|
actions.push({
|
|
label: 'Cancelar',
|
|
icon: X,
|
|
variant: 'outline' as const,
|
|
priority: 'tertiary' as const,
|
|
destructive: true,
|
|
onClick: () => handleCancelPlan(plan.id)
|
|
});
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div key={plan.id} className="space-y-2">
|
|
<StatusCard
|
|
id={plan.plan_number}
|
|
statusIndicator={statusConfig}
|
|
title={`Plan ${plan.plan_number}`}
|
|
subtitle={`${new Date(plan.plan_date).toLocaleDateString('es-ES', { day: '2-digit', month: '2-digit', year: '2-digit' })} • ${plan.procurement_strategy}`}
|
|
primaryValue={plan.total_requirements}
|
|
primaryValueLabel="requerimientos"
|
|
secondaryInfo={{
|
|
label: 'Presupuesto',
|
|
value: `€${formatters.compact(plan.total_estimated_cost)}`
|
|
}}
|
|
progress={plan.planning_horizon_days ? {
|
|
label: `${plan.planning_horizon_days} días de horizonte`,
|
|
percentage: Math.min((plan.planning_horizon_days / 30) * 100, 100),
|
|
color: plan.planning_horizon_days > 14 ? '#10b981' : plan.planning_horizon_days > 7 ? '#f59e0b' : '#ef4444'
|
|
} : undefined}
|
|
metadata={[
|
|
`Período: ${new Date(plan.plan_period_start).toLocaleDateString('es-ES', { day: '2-digit', month: '2-digit' })} - ${new Date(plan.plan_period_end).toLocaleDateString('es-ES', { day: '2-digit', month: '2-digit' })}`,
|
|
`Creado: ${new Date(plan.created_at).toLocaleDateString('es-ES', { day: '2-digit', month: '2-digit' })}`,
|
|
...(plan.special_requirements ? [`Req. especiales: ${plan.special_requirements}`] : [])
|
|
]}
|
|
actions={actions}
|
|
/>
|
|
|
|
{/* Inline Edit Form for Draft Plans */}
|
|
{isEditing && (
|
|
<Card className="p-4 border-l-4 border-l-[var(--color-primary)] bg-[var(--color-primary)]/5">
|
|
<h4 className="text-sm font-semibold text-[var(--text-primary)] mb-3">
|
|
Editando Plan {plan.plan_number}
|
|
</h4>
|
|
<div className="space-y-3">
|
|
<div>
|
|
<label className="block text-xs font-medium text-[var(--text-secondary)] mb-1">
|
|
Requerimientos Especiales
|
|
</label>
|
|
<textarea
|
|
value={editFormData.special_requirements}
|
|
onChange={(e) => setEditFormData((prev: any) => ({ ...prev, special_requirements: e.target.value }))}
|
|
placeholder="Ingrese requerimientos especiales para este plan..."
|
|
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-[60px]"
|
|
rows={2}
|
|
/>
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<div>
|
|
<label className="block text-xs font-medium text-[var(--text-secondary)] mb-1">
|
|
Días de Horizonte
|
|
</label>
|
|
<Input
|
|
type="number"
|
|
value={editFormData.planning_horizon_days}
|
|
onChange={(e) => setEditFormData((prev: any) => ({ ...prev, planning_horizon_days: parseInt(e.target.value) }))}
|
|
min="1"
|
|
max="365"
|
|
className="w-full"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-xs font-medium text-[var(--text-secondary)] mb-1">
|
|
Prioridad
|
|
</label>
|
|
<select
|
|
value={editFormData.priority}
|
|
onChange={(e) => setEditFormData((prev: any) => ({ ...prev, priority: 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)]
|
|
transition-colors duration-200"
|
|
>
|
|
<option value="low">Baja</option>
|
|
<option value="medium">Media</option>
|
|
<option value="high">Alta</option>
|
|
<option value="urgent">Urgente</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
)}
|
|
</div>
|
|
);
|
|
})
|
|
)
|
|
}
|
|
</div>
|
|
|
|
{/* Empty State for Procurement Plans */}
|
|
{!isPlansLoading && filteredPlans.length === 0 && (
|
|
<div className="text-center py-12">
|
|
<Package className="mx-auto h-12 w-12 text-[var(--text-tertiary)] mb-4" />
|
|
<h3 className="text-lg font-medium text-[var(--text-primary)] mb-2">
|
|
No se encontraron planes de compra
|
|
</h3>
|
|
<p className="text-[var(--text-secondary)] mb-4">
|
|
Intenta ajustar la búsqueda o generar un nuevo plan de compra
|
|
</p>
|
|
<Button
|
|
onClick={() => generatePlanMutation.mutate({
|
|
tenantId,
|
|
request: {
|
|
force_regenerate: false,
|
|
planning_horizon_days: 14,
|
|
include_safety_stock: true,
|
|
safety_stock_percentage: 20
|
|
}
|
|
})}
|
|
disabled={generatePlanMutation.isPending}
|
|
>
|
|
{generatePlanMutation.isPending ? (
|
|
<Loader className="w-4 h-4 mr-2 animate-spin" />
|
|
) : (
|
|
<Plus className="w-4 h-4 mr-2" />
|
|
)}
|
|
Generar Plan de Compra
|
|
</Button>
|
|
</div>
|
|
)}
|
|
|
|
{/* Critical Requirements Modal */}
|
|
{showCriticalRequirements && selectedPlanForRequirements && (
|
|
<div className="fixed top-[var(--header-height)] left-0 right-0 bottom-0 bg-black bg-opacity-50 flex items-center justify-center z-40">
|
|
<div className="bg-[var(--bg-primary)] rounded-lg shadow-xl max-w-4xl w-full max-h-[90vh] overflow-hidden 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="flex items-center justify-center w-8 h-8 rounded-full bg-red-100">
|
|
<AlertCircle className="w-5 h-5 text-red-600" />
|
|
</div>
|
|
<div>
|
|
<h2 className="text-lg font-semibold text-[var(--text-primary)]">
|
|
Requerimientos Críticos
|
|
</h2>
|
|
<p className="text-sm text-[var(--text-secondary)]">
|
|
Plan {filteredPlans.find(p => p.id === selectedPlanForRequirements)?.plan_number || selectedPlanForRequirements}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<Button
|
|
variant="outline"
|
|
onClick={handleCloseCriticalRequirements}
|
|
className="p-2"
|
|
>
|
|
<X className="w-4 h-4" />
|
|
</Button>
|
|
</div>
|
|
|
|
{/* Content */}
|
|
<div className="p-6 overflow-y-auto max-h-[calc(90vh-120px)]">
|
|
{isPlanRequirementsLoading ? (
|
|
<div className="flex justify-center items-center h-32">
|
|
<Loader className="w-8 h-8 animate-spin text-[var(--color-primary)]" />
|
|
</div>
|
|
) : planRequirements && planRequirements.length > 0 ? (
|
|
<div className="space-y-4">
|
|
<div className="flex justify-end mb-4">
|
|
<Button
|
|
variant="primary"
|
|
onClick={() => {
|
|
// Set all requirements for PO creation
|
|
setSelectedRequirementsForPO(planRequirements);
|
|
setShowCreatePurchaseOrderModal(true);
|
|
}}
|
|
>
|
|
<Plus className="w-4 h-4 mr-2" />
|
|
Crear Orden de Compra para Todos
|
|
</Button>
|
|
</div>
|
|
|
|
{planRequirements.map((requirement) => (
|
|
<StatusCard
|
|
key={requirement.id}
|
|
id={requirement.requirement_number}
|
|
statusIndicator={{
|
|
color: getStatusColor('danger'),
|
|
text: 'Crítico',
|
|
icon: AlertCircle,
|
|
isCritical: true
|
|
}}
|
|
title={requirement.product_name}
|
|
subtitle={`${requirement.requirement_number} • ${requirement.supplier_name || 'Sin proveedor'} • Plan: ${filteredPlans.find(p => p.id === selectedPlanForRequirements)?.plan_number || 'N/A'}`}
|
|
primaryValue={requirement.required_quantity}
|
|
primaryValueLabel={requirement.unit_of_measure}
|
|
secondaryInfo={{
|
|
label: 'Límite',
|
|
value: new Date(requirement.required_by_date).toLocaleDateString('es-ES', { day: '2-digit', month: '2-digit' })
|
|
}}
|
|
progress={requirement.current_stock_level && requirement.required_quantity ? {
|
|
label: `${Math.round((requirement.current_stock_level / requirement.required_quantity) * 100)}% cubierto`,
|
|
percentage: Math.min((requirement.current_stock_level / requirement.required_quantity) * 100, 100),
|
|
color: requirement.current_stock_level >= requirement.required_quantity ? '#10b981' : requirement.current_stock_level >= requirement.required_quantity * 0.5 ? '#f59e0b' : '#ef4444'
|
|
} : undefined}
|
|
metadata={[
|
|
`Stock: ${requirement.current_stock_level || 0} ${requirement.unit_of_measure}`,
|
|
`Necesario: ${requirement.required_quantity - (requirement.current_stock_level || 0)} ${requirement.unit_of_measure}`,
|
|
`Costo: €${formatters.compact(requirement.estimated_total_cost || 0)}`,
|
|
`Días restantes: ${Math.ceil((new Date(requirement.required_by_date).getTime() - Date.now()) / (1000 * 60 * 60 * 24))}`,
|
|
`Estado: ${requirement.status || 'N/A'}`,
|
|
`PO: ${requirement.purchase_order_number || 'Sin PO asignado'}`,
|
|
`Prioridad: ${requirement.priority || 'N/A'}`
|
|
]}
|
|
actions={[
|
|
{
|
|
label: 'Ver Detalles',
|
|
icon: Eye,
|
|
variant: 'primary',
|
|
priority: 'primary',
|
|
onClick: () => handleViewRequirementDetails(requirement)
|
|
},
|
|
...(requirement.purchase_order_number ? [
|
|
{
|
|
label: 'Ver PO',
|
|
icon: Eye,
|
|
variant: 'outline' as const,
|
|
priority: 'secondary' as const,
|
|
onClick: () => {
|
|
// TODO: Open purchase order details
|
|
}
|
|
}
|
|
] : [
|
|
{
|
|
label: 'Crear PO',
|
|
icon: Plus,
|
|
variant: 'outline' as const,
|
|
priority: 'secondary' as const,
|
|
onClick: () => {
|
|
// Set the single requirement for PO creation
|
|
setSelectedRequirementsForPO([requirement]);
|
|
setShowCreatePurchaseOrderModal(true);
|
|
}
|
|
}
|
|
]),
|
|
{
|
|
label: requirement.supplier_name ? 'Cambiar Proveedor' : 'Asignar Proveedor',
|
|
icon: Building2,
|
|
variant: 'outline' as const,
|
|
priority: 'secondary' as const,
|
|
onClick: () => {
|
|
// TODO: Open supplier assignment modal
|
|
}
|
|
}
|
|
]}
|
|
/>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<div className="text-center py-12">
|
|
<CheckCircle className="mx-auto h-12 w-12 text-green-500 mb-4" />
|
|
<h3 className="text-lg font-medium text-[var(--text-primary)] mb-2">
|
|
No hay requerimientos críticos
|
|
</h3>
|
|
<p className="text-[var(--text-secondary)]">
|
|
Este plan no tiene requerimientos críticos en este momento
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Procurement Plan Modal */}
|
|
{showForm && selectedPlan && (
|
|
<StatusModal
|
|
isOpen={showForm}
|
|
onClose={() => {
|
|
setShowForm(false);
|
|
setSelectedPlan(null);
|
|
setModalMode('view');
|
|
}}
|
|
mode={modalMode}
|
|
onModeChange={setModalMode}
|
|
title={`Plan de Compra ${selectedPlan.plan_number}`}
|
|
subtitle={new Date(selectedPlan.plan_date).toLocaleDateString('es-ES')}
|
|
statusIndicator={getPlanStatusConfig(selectedPlan.status)}
|
|
size="lg"
|
|
sections={[
|
|
{
|
|
title: 'Información del Plan',
|
|
icon: Package,
|
|
fields: [
|
|
{
|
|
label: 'Número de Plan',
|
|
value: selectedPlan.plan_number,
|
|
highlight: true
|
|
},
|
|
{
|
|
label: 'Tipo de Plan',
|
|
value: selectedPlan.plan_type
|
|
},
|
|
{
|
|
label: 'Estrategia',
|
|
value: selectedPlan.procurement_strategy
|
|
},
|
|
{
|
|
label: 'Prioridad',
|
|
value: selectedPlan.priority,
|
|
type: 'status'
|
|
}
|
|
]
|
|
},
|
|
{
|
|
title: 'Fechas y Períodos',
|
|
icon: Calendar,
|
|
fields: [
|
|
{
|
|
label: 'Fecha del Plan',
|
|
value: selectedPlan.plan_date,
|
|
type: 'date'
|
|
},
|
|
{
|
|
label: 'Período de Inicio',
|
|
value: selectedPlan.plan_period_start,
|
|
type: 'date'
|
|
},
|
|
{
|
|
label: 'Período de Fin',
|
|
value: selectedPlan.plan_period_end,
|
|
type: 'date',
|
|
highlight: true
|
|
},
|
|
{
|
|
label: 'Horizonte de Planificación',
|
|
value: `${selectedPlan.planning_horizon_days} días`
|
|
}
|
|
]
|
|
},
|
|
{
|
|
title: 'Información Financiera',
|
|
icon: Euro,
|
|
fields: [
|
|
{
|
|
label: 'Costo Estimado Total',
|
|
value: selectedPlan.total_estimated_cost,
|
|
type: 'currency',
|
|
highlight: true
|
|
},
|
|
{
|
|
label: 'Costo Aprobado Total',
|
|
value: selectedPlan.total_approved_cost,
|
|
type: 'currency'
|
|
},
|
|
{
|
|
label: 'Varianza de Costo',
|
|
value: selectedPlan.cost_variance,
|
|
type: 'currency'
|
|
}
|
|
]
|
|
},
|
|
{
|
|
title: 'Estadísticas',
|
|
icon: ShoppingCart,
|
|
fields: [
|
|
{
|
|
label: 'Total de Requerimientos',
|
|
value: `${selectedPlan.total_requirements} requerimientos`
|
|
},
|
|
{
|
|
label: 'Demanda Total (Cantidad)',
|
|
value: `${selectedPlan.total_demand_quantity} unidades`
|
|
},
|
|
{
|
|
label: 'Proveedores Primarios',
|
|
value: `${selectedPlan.primary_suppliers_count} proveedores`
|
|
},
|
|
{
|
|
label: 'Proveedores de Respaldo',
|
|
value: `${selectedPlan.backup_suppliers_count} proveedores`
|
|
}
|
|
]
|
|
},
|
|
...(selectedPlan.special_requirements ? [{
|
|
title: 'Requerimientos Especiales',
|
|
fields: [
|
|
{
|
|
label: 'Observaciones',
|
|
value: selectedPlan.special_requirements,
|
|
span: 2 as const,
|
|
editable: true,
|
|
placeholder: 'Añadir requerimientos especiales para el plan...'
|
|
}
|
|
]
|
|
}] : [])
|
|
]}
|
|
onEdit={() => {
|
|
// TODO: Implement plan editing functionality
|
|
}}
|
|
/>
|
|
)}
|
|
|
|
{/* Generate Plan Modal */}
|
|
{showGeneratePlanModal && (
|
|
<div className="fixed top-[var(--header-height)] left-0 right-0 bottom-0 bg-black bg-opacity-50 flex items-center justify-center z-40">
|
|
<div className="bg-[var(--bg-primary)] rounded-lg shadow-xl max-w-2xl w-full max-h-[90vh] overflow-hidden 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="flex items-center justify-center w-8 h-8 rounded-full bg-blue-100">
|
|
<Plus className="w-5 h-5 text-blue-600" />
|
|
</div>
|
|
<div>
|
|
<h2 className="text-lg font-semibold text-[var(--text-primary)]">
|
|
Generar Plan de Compras
|
|
</h2>
|
|
<p className="text-sm text-[var(--text-secondary)]">
|
|
Configura los parámetros para generar un nuevo plan
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => setShowGeneratePlanModal(false)}
|
|
className="p-2"
|
|
>
|
|
<X className="w-4 h-4" />
|
|
</Button>
|
|
</div>
|
|
|
|
{/* Content */}
|
|
<div className="p-6 overflow-y-auto max-h-[calc(90vh-200px)]">
|
|
<div className="space-y-6">
|
|
{/* Plan Date */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
|
Fecha del Plan
|
|
</label>
|
|
<Input
|
|
type="date"
|
|
value={generatePlanForm.plan_date}
|
|
onChange={(e) => setGeneratePlanForm(prev => ({ ...prev, plan_date: e.target.value }))}
|
|
className="w-full"
|
|
/>
|
|
<p className="text-xs text-[var(--text-tertiary)] mt-1">
|
|
Fecha para la cual se generará el plan de compras
|
|
</p>
|
|
</div>
|
|
|
|
{/* Planning Horizon */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
|
Horizonte de Planificación (días)
|
|
</label>
|
|
<Input
|
|
type="number"
|
|
min="1"
|
|
max="365"
|
|
value={generatePlanForm.planning_horizon_days}
|
|
onChange={(e) => setGeneratePlanForm(prev => ({ ...prev, planning_horizon_days: parseInt(e.target.value) }))}
|
|
className="w-full"
|
|
/>
|
|
<p className="text-xs text-[var(--text-tertiary)] mt-1">
|
|
Número de días a considerar en la planificación (1-365)
|
|
</p>
|
|
</div>
|
|
|
|
{/* Safety Stock */}
|
|
<div className="space-y-4">
|
|
<div className="flex items-center space-x-3">
|
|
<input
|
|
type="checkbox"
|
|
id="include_safety_stock"
|
|
checked={generatePlanForm.include_safety_stock}
|
|
onChange={(e) => setGeneratePlanForm(prev => ({ ...prev, include_safety_stock: e.target.checked }))}
|
|
className="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500"
|
|
/>
|
|
<label htmlFor="include_safety_stock" className="text-sm font-medium text-[var(--text-secondary)]">
|
|
Incluir Stock de Seguridad
|
|
</label>
|
|
</div>
|
|
|
|
{generatePlanForm.include_safety_stock && (
|
|
<div>
|
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
|
Porcentaje de Stock de Seguridad (%)
|
|
</label>
|
|
<Input
|
|
type="number"
|
|
min="0"
|
|
max="100"
|
|
value={generatePlanForm.safety_stock_percentage}
|
|
onChange={(e) => setGeneratePlanForm(prev => ({ ...prev, safety_stock_percentage: parseInt(e.target.value) }))}
|
|
className="w-full"
|
|
/>
|
|
<p className="text-xs text-[var(--text-tertiary)] mt-1">
|
|
Porcentaje adicional para stock de seguridad (0-100%)
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Force Regenerate */}
|
|
<div className="flex items-center space-x-3">
|
|
<input
|
|
type="checkbox"
|
|
id="force_regenerate"
|
|
checked={generatePlanForm.force_regenerate}
|
|
onChange={(e) => setGeneratePlanForm(prev => ({ ...prev, force_regenerate: e.target.checked }))}
|
|
className="w-4 h-4 text-red-600 bg-gray-100 border-gray-300 rounded focus:ring-red-500"
|
|
/>
|
|
<label htmlFor="force_regenerate" className="text-sm font-medium text-[var(--text-secondary)]">
|
|
Forzar Regeneración
|
|
</label>
|
|
</div>
|
|
<p className="text-xs text-[var(--text-tertiary)] ml-7">
|
|
Si ya existe un plan para esta fecha, regenerarlo (esto eliminará el plan existente)
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Footer */}
|
|
<div className="flex justify-end space-x-3 p-6 border-t border-[var(--border-primary)]">
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => setShowGeneratePlanModal(false)}
|
|
disabled={generatePlanMutation.isPending}
|
|
>
|
|
Cancelar
|
|
</Button>
|
|
<Button
|
|
variant="primary"
|
|
onClick={() => {
|
|
generatePlanMutation.mutate({
|
|
tenantId,
|
|
request: {
|
|
plan_date: generatePlanForm.plan_date,
|
|
planning_horizon_days: generatePlanForm.planning_horizon_days,
|
|
include_safety_stock: generatePlanForm.include_safety_stock,
|
|
safety_stock_percentage: generatePlanForm.safety_stock_percentage,
|
|
force_regenerate: generatePlanForm.force_regenerate
|
|
}
|
|
}, {
|
|
onSuccess: () => {
|
|
setShowGeneratePlanModal(false);
|
|
// Reset form to defaults
|
|
setGeneratePlanForm({
|
|
plan_date: new Date().toISOString().split('T')[0],
|
|
planning_horizon_days: 14,
|
|
include_safety_stock: true,
|
|
safety_stock_percentage: 20,
|
|
force_regenerate: false
|
|
});
|
|
}
|
|
});
|
|
}}
|
|
disabled={generatePlanMutation.isPending}
|
|
>
|
|
{generatePlanMutation.isPending ? (
|
|
<>
|
|
<Loader className="w-4 h-4 mr-2 animate-spin" />
|
|
Generando...
|
|
</>
|
|
) : (
|
|
<>
|
|
<Plus className="w-4 h-4 mr-2" />
|
|
Generar Plan
|
|
</>
|
|
)}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Create Purchase Order Modal */}
|
|
{showCreatePurchaseOrderModal && (
|
|
<CreatePurchaseOrderModal
|
|
isOpen={showCreatePurchaseOrderModal}
|
|
onClose={() => {
|
|
setShowCreatePurchaseOrderModal(false);
|
|
setSelectedRequirementsForPO([]);
|
|
}}
|
|
requirements={selectedRequirementsForPO}
|
|
onSuccess={() => {
|
|
// Refresh the plan requirements data
|
|
setSelectedRequirementsForPO([]);
|
|
// You might want to invalidate queries here to refresh data
|
|
}}
|
|
/>
|
|
)}
|
|
|
|
{/* Requirement Details Modal */}
|
|
{showRequirementDetailsModal && selectedRequirement && (
|
|
<div className="fixed top-[var(--header-height)] left-0 right-0 bottom-0 bg-black bg-opacity-50 flex items-center justify-center z-40">
|
|
<div className="bg-[var(--bg-primary)] rounded-lg shadow-xl max-w-4xl w-full max-h-[90vh] overflow-hidden 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="flex items-center justify-center w-8 h-8 rounded-full bg-blue-100">
|
|
<Eye className="w-5 h-5 text-blue-600" />
|
|
</div>
|
|
<div>
|
|
<h2 className="text-lg font-semibold text-[var(--text-primary)]">
|
|
Detalles del Requerimiento
|
|
</h2>
|
|
<p className="text-sm text-[var(--text-secondary)]">
|
|
{selectedRequirement.product_name}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => setShowRequirementDetailsModal(false)}
|
|
className="p-2"
|
|
>
|
|
<X className="w-4 h-4" />
|
|
</Button>
|
|
</div>
|
|
|
|
{/* Content */}
|
|
<div className="p-6 overflow-y-auto max-h-[calc(90vh-200px)]">
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
{/* Product Information */}
|
|
<div className="space-y-4">
|
|
<h3 className="text-md font-semibold text-[var(--text-primary)] border-b pb-2">
|
|
Información del Producto
|
|
</h3>
|
|
<div className="space-y-2">
|
|
<div>
|
|
<label className="text-sm font-medium text-[var(--text-secondary)]">Nombre</label>
|
|
<p className="text-[var(--text-primary)]">{selectedRequirement.product_name}</p>
|
|
</div>
|
|
<div>
|
|
<label className="text-sm font-medium text-[var(--text-secondary)]">SKU</label>
|
|
<p className="text-[var(--text-primary)]">{selectedRequirement.product_sku || 'N/A'}</p>
|
|
</div>
|
|
<div>
|
|
<label className="text-sm font-medium text-[var(--text-secondary)]">Categoría</label>
|
|
<p className="text-[var(--text-primary)]">{selectedRequirement.product_category || 'N/A'}</p>
|
|
</div>
|
|
<div>
|
|
<label className="text-sm font-medium text-[var(--text-secondary)]">Tipo</label>
|
|
<p className="text-[var(--text-primary)]">{selectedRequirement.product_type}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Quantities */}
|
|
<div className="space-y-4">
|
|
<h3 className="text-md font-semibold text-[var(--text-primary)] border-b pb-2">
|
|
Cantidades
|
|
</h3>
|
|
<div className="space-y-2">
|
|
<div>
|
|
<label className="text-sm font-medium text-[var(--text-secondary)]">Cantidad Requerida</label>
|
|
<p className="text-[var(--text-primary)]">{selectedRequirement.required_quantity} {selectedRequirement.unit_of_measure}</p>
|
|
</div>
|
|
<div>
|
|
<label className="text-sm font-medium text-[var(--text-secondary)]">Stock de Seguridad</label>
|
|
<p className="text-[var(--text-primary)]">{selectedRequirement.safety_stock_quantity} {selectedRequirement.unit_of_measure}</p>
|
|
</div>
|
|
<div>
|
|
<label className="text-sm font-medium text-[var(--text-secondary)]">Stock Actual</label>
|
|
<p className="text-[var(--text-primary)]">{selectedRequirement.current_stock_level} {selectedRequirement.unit_of_measure}</p>
|
|
</div>
|
|
<div>
|
|
<label className="text-sm font-medium text-[var(--text-secondary)]">Requerimiento Neto</label>
|
|
<p className="text-[var(--text-primary)] font-semibold">{selectedRequirement.net_requirement} {selectedRequirement.unit_of_measure}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Costs */}
|
|
<div className="space-y-4">
|
|
<h3 className="text-md font-semibold text-[var(--text-primary)] border-b pb-2">
|
|
Costos
|
|
</h3>
|
|
<div className="space-y-2">
|
|
<div>
|
|
<label className="text-sm font-medium text-[var(--text-secondary)]">Costo Unitario Estimado</label>
|
|
<p className="text-[var(--text-primary)]">€{selectedRequirement.estimated_unit_cost || 'N/A'}</p>
|
|
</div>
|
|
<div>
|
|
<label className="text-sm font-medium text-[var(--text-secondary)]">Costo Total Estimado</label>
|
|
<p className="text-[var(--text-primary)] font-semibold">€{selectedRequirement.estimated_total_cost || 'N/A'}</p>
|
|
</div>
|
|
{selectedRequirement.last_purchase_cost && (
|
|
<div>
|
|
<label className="text-sm font-medium text-[var(--text-secondary)]">Último Precio de Compra</label>
|
|
<p className="text-[var(--text-primary)]">€{selectedRequirement.last_purchase_cost}</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Dates & Timeline */}
|
|
<div className="space-y-4">
|
|
<h3 className="text-md font-semibold text-[var(--text-primary)] border-b pb-2">
|
|
Fechas
|
|
</h3>
|
|
<div className="space-y-2">
|
|
<div>
|
|
<label className="text-sm font-medium text-[var(--text-secondary)]">Requerido Para</label>
|
|
<p className="text-[var(--text-primary)]">{selectedRequirement.required_by_date}</p>
|
|
</div>
|
|
{selectedRequirement.suggested_order_date && (
|
|
<div>
|
|
<label className="text-sm font-medium text-[var(--text-secondary)]">Fecha Sugerida de Pedido</label>
|
|
<p className="text-[var(--text-primary)]">{selectedRequirement.suggested_order_date}</p>
|
|
</div>
|
|
)}
|
|
{selectedRequirement.latest_order_date && (
|
|
<div>
|
|
<label className="text-sm font-medium text-[var(--text-secondary)]">Fecha Límite de Pedido</label>
|
|
<p className="text-[var(--text-primary)] text-red-600">{selectedRequirement.latest_order_date}</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Status & Priority */}
|
|
<div className="space-y-4">
|
|
<h3 className="text-md font-semibold text-[var(--text-primary)] border-b pb-2">
|
|
Estado y Prioridad
|
|
</h3>
|
|
<div className="space-y-2">
|
|
<div>
|
|
<label className="text-sm font-medium text-[var(--text-secondary)]">Estado</label>
|
|
<span className={`inline-flex px-2 py-1 text-xs font-medium rounded-full ${
|
|
selectedRequirement.status === 'pending' ? 'bg-yellow-100 text-yellow-800' :
|
|
selectedRequirement.status === 'approved' ? 'bg-green-100 text-green-800' :
|
|
selectedRequirement.status === 'ordered' ? 'bg-blue-100 text-blue-800' :
|
|
'bg-gray-100 text-gray-800'
|
|
}`}>
|
|
{selectedRequirement.status}
|
|
</span>
|
|
</div>
|
|
<div>
|
|
<label className="text-sm font-medium text-[var(--text-secondary)]">Prioridad</label>
|
|
<span className={`inline-flex px-2 py-1 text-xs font-medium rounded-full ${
|
|
selectedRequirement.priority === 'critical' ? 'bg-red-100 text-red-800' :
|
|
selectedRequirement.priority === 'high' ? 'bg-orange-100 text-orange-800' :
|
|
'bg-green-100 text-green-800'
|
|
}`}>
|
|
{selectedRequirement.priority}
|
|
</span>
|
|
</div>
|
|
<div>
|
|
<label className="text-sm font-medium text-[var(--text-secondary)]">Nivel de Riesgo</label>
|
|
<span className={`inline-flex px-2 py-1 text-xs font-medium rounded-full ${
|
|
selectedRequirement.risk_level === 'high' ? 'bg-red-100 text-red-800' :
|
|
selectedRequirement.risk_level === 'medium' ? 'bg-yellow-100 text-yellow-800' :
|
|
'bg-green-100 text-green-800'
|
|
}`}>
|
|
{selectedRequirement.risk_level}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Supplier Information */}
|
|
{selectedRequirement.supplier_name && (
|
|
<div className="space-y-4">
|
|
<h3 className="text-md font-semibold text-[var(--text-primary)] border-b pb-2">
|
|
Proveedor
|
|
</h3>
|
|
<div className="space-y-2">
|
|
<div>
|
|
<label className="text-sm font-medium text-[var(--text-secondary)]">Nombre</label>
|
|
<p className="text-[var(--text-primary)]">{selectedRequirement.supplier_name}</p>
|
|
</div>
|
|
{selectedRequirement.supplier_lead_time_days && (
|
|
<div>
|
|
<label className="text-sm font-medium text-[var(--text-secondary)]">Tiempo de Entrega</label>
|
|
<p className="text-[var(--text-primary)]">{selectedRequirement.supplier_lead_time_days} días</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Special Requirements */}
|
|
{selectedRequirement.special_requirements && (
|
|
<div className="mt-6 space-y-2">
|
|
<h3 className="text-md font-semibold text-[var(--text-primary)] border-b pb-2">
|
|
Requerimientos Especiales
|
|
</h3>
|
|
<p className="text-[var(--text-primary)] bg-gray-50 p-3 rounded-lg">
|
|
{selectedRequirement.special_requirements}
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Footer */}
|
|
<div className="flex justify-end space-x-3 p-6 border-t border-[var(--border-primary)]">
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => setShowRequirementDetailsModal(false)}
|
|
>
|
|
Cerrar
|
|
</Button>
|
|
{selectedRequirement.status === 'pending' && (
|
|
<Button
|
|
variant="primary"
|
|
onClick={() => {
|
|
// TODO: Implement approval functionality
|
|
setShowRequirementDetailsModal(false);
|
|
}}
|
|
>
|
|
<CheckCircle className="w-4 h-4 mr-2" />
|
|
Aprobar
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default ProcurementPage; |