diff --git a/frontend/src/components/ui/Button/Button.tsx b/frontend/src/components/ui/Button/Button.tsx index a5cb3800..eb4b4991 100644 --- a/frontend/src/components/ui/Button/Button.tsx +++ b/frontend/src/components/ui/Button/Button.tsx @@ -79,10 +79,10 @@ const Button = forwardRef(({ const sizeClasses = { xs: 'px-2 py-1 text-xs gap-1 min-h-6', - sm: 'px-3 py-1.5 text-sm gap-1.5 min-h-8', - md: 'px-4 py-2 text-sm gap-2 min-h-10', - lg: 'px-6 py-2.5 text-base gap-2 min-h-12', - xl: 'px-8 py-3 text-lg gap-3 min-h-14' + sm: 'px-3 py-1.5 text-sm gap-1.5 min-h-8 sm:min-h-10', // Better touch target on mobile + md: 'px-4 py-2 text-sm gap-2 min-h-10 sm:min-h-12', + lg: 'px-6 py-2.5 text-base gap-2 min-h-12 sm:min-h-14', + xl: 'px-8 py-3 text-lg gap-3 min-h-14 sm:min-h-16' }; const loadingSpinner = ( diff --git a/frontend/src/components/ui/StatusCard/StatusCard.tsx b/frontend/src/components/ui/StatusCard/StatusCard.tsx index b79acd7c..92a2260f 100644 --- a/frontend/src/components/ui/StatusCard/StatusCard.tsx +++ b/frontend/src/components/ui/StatusCard/StatusCard.tsx @@ -34,9 +34,12 @@ export interface StatusCardProps { icon?: LucideIcon; variant?: 'primary' | 'outline'; onClick: () => void; + priority?: 'primary' | 'secondary' | 'tertiary'; + destructive?: boolean; }>; onClick?: () => void; className?: string; + compact?: boolean; } /** @@ -74,7 +77,7 @@ export const getStatusColor = (status: string): StatusIndicatorConfig['color'] = * StatusCard - Reusable card component with consistent status styling */ export const StatusCard: React.FC = ({ - id, + id: _id, statusIndicator, title, subtitle, @@ -86,10 +89,23 @@ export const StatusCard: React.FC = ({ actions = [], onClick, className = '', + compact: _compact = false, }) => { const StatusIcon = statusIndicator.icon; const hasInteraction = onClick || actions.length > 0; + // Sort actions by priority + const sortedActions = [...actions].sort((a, b) => { + const priorityOrder = { primary: 0, secondary: 1, tertiary: 2 }; + const aPriority = priorityOrder[a.priority || 'secondary']; + const bPriority = priorityOrder[b.priority || 'secondary']; + return aPriority - bPriority; + }); + + // Split actions into primary and secondary groups + const primaryActions = sortedActions.filter(action => action.priority === 'primary'); + const secondaryActions = sortedActions.filter(action => action.priority !== 'primary'); + return ( = ({ )} - {/* Actions */} + {/* Enhanced Mobile-Responsive Actions */} {actions.length > 0 && ( -
- {actions.map((action, index) => ( - - ))} +
+ {/* Primary Actions - Full width on mobile, inline on desktop */} + {primaryActions.length > 0 && ( +
1 ? 'flex-col sm:flex-row' : ''} + `}> + {primaryActions.map((action, index) => ( + + ))} +
+ )} + + {/* Secondary Actions - Compact horizontal layout */} + {secondaryActions.length > 0 && ( +
+ {secondaryActions.map((action, index) => ( + + ))} +
+ )}
)}
diff --git a/frontend/src/pages/app/operations/procurement/ProcurementPage.tsx b/frontend/src/pages/app/operations/procurement/ProcurementPage.tsx index 9f078a33..49d55e3e 100644 --- a/frontend/src/pages/app/operations/procurement/ProcurementPage.tsx +++ b/frontend/src/pages/app/operations/procurement/ProcurementPage.tsx @@ -1,5 +1,5 @@ import React, { useState } from 'react'; -import { Plus, Search, Download, ShoppingCart, Truck, DollarSign, Calendar, Clock, CheckCircle, AlertCircle, Package, Eye, Loader } from 'lucide-react'; +import { Plus, Search, Download, ShoppingCart, Truck, DollarSign, Calendar, Clock, CheckCircle, AlertCircle, Package, Eye, Loader, Edit, ArrowRight, Play, Pause, X, Save } 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'; @@ -20,6 +20,8 @@ const ProcurementPage: React.FC = () => { const [showForm, setShowForm] = useState(false); const [modalMode, setModalMode] = useState<'view' | 'edit'>('view'); const [selectedPlan, setSelectedPlan] = useState(null); + const [editingPlan, setEditingPlan] = useState(null); + const [editFormData, setEditFormData] = useState({}); const { currentTenant } = useTenantStore(); const tenantId = currentTenant?.id || ''; @@ -38,6 +40,73 @@ const ProcurementPage: React.FC = () => { 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 + console.log('Saving plan edits:', editFormData); + setEditingPlan(null); + setEditFormData({}); + // Here you would typically call an update API + }; + + const handleCancelEdit = () => { + setEditingPlan(null); + setEditFormData({}); + }; + if (!tenantId) { return (
@@ -162,10 +231,24 @@ const ProcurementPage: React.FC = () => { }, { id: "trigger", - label: "Ejecutar Programador", + label: triggerSchedulerMutation.isPending ? "Ejecutando..." : "Ejecutar Programador", variant: "outline" as const, - icon: Calendar, - onClick: () => triggerSchedulerMutation.mutate(tenantId) + icon: triggerSchedulerMutation.isPending ? Loader : Calendar, + onClick: () => { + triggerSchedulerMutation.mutate(tenantId, { + onSuccess: (data) => { + console.log('✅ Scheduler ejecutado exitosamente:', data.message); + // 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 } ]} /> @@ -237,9 +320,9 @@ const ProcurementPage: React.FC = () => { )} - {/* Procurement Plans Grid */} + {/* Procurement Plans Grid - Mobile-Optimized */} {activeTab === 'plans' && ( -
+
{isPlansLoading ? (
@@ -247,50 +330,160 @@ const ProcurementPage: React.FC = () => { ) : ( 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); + } + }); + + // 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 ( - { - setSelectedPlan(plan); - setModalMode('view'); - setShowForm(true); - } - }, - ...(plan.status === 'pending_approval' ? [{ - label: 'Aprobar', - icon: CheckCircle, - variant: 'outline' as const, - onClick: () => { - updatePlanStatusMutation.mutate({ - tenant_id: tenantId, - plan_id: plan.id, - status: 'approved' - }); - } - }] : []) - ]} - /> +
+ + + {/* Inline Edit Form for Draft Plans */} + {isEditing && ( + +

+ Editando Plan {plan.plan_number} +

+
+
+ +