Add statuscard changes
This commit is contained in:
@@ -79,10 +79,10 @@ const Button = forwardRef<HTMLButtonElement, ButtonProps>(({
|
||||
|
||||
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 = (
|
||||
|
||||
@@ -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<StatusCardProps> = ({
|
||||
id,
|
||||
id: _id,
|
||||
statusIndicator,
|
||||
title,
|
||||
subtitle,
|
||||
@@ -86,10 +89,23 @@ export const StatusCard: React.FC<StatusCardProps> = ({
|
||||
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 (
|
||||
<Card
|
||||
className={`
|
||||
@@ -201,21 +217,65 @@ export const StatusCard: React.FC<StatusCardProps> = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Actions */}
|
||||
{/* Enhanced Mobile-Responsive Actions */}
|
||||
{actions.length > 0 && (
|
||||
<div className="flex gap-2 pt-3 border-t border-[var(--border-primary)]">
|
||||
{actions.map((action, index) => (
|
||||
<Button
|
||||
key={index}
|
||||
variant={action.variant || 'outline'}
|
||||
size="sm"
|
||||
className="flex-1"
|
||||
onClick={action.onClick}
|
||||
>
|
||||
{action.icon && <action.icon className="w-4 h-4 mr-2" />}
|
||||
{action.label}
|
||||
</Button>
|
||||
))}
|
||||
<div className="pt-3 border-t border-[var(--border-primary)]">
|
||||
{/* Primary Actions - Full width on mobile, inline on desktop */}
|
||||
{primaryActions.length > 0 && (
|
||||
<div className={`
|
||||
flex gap-2 mb-2
|
||||
${primaryActions.length > 1 ? 'flex-col sm:flex-row' : ''}
|
||||
`}>
|
||||
{primaryActions.map((action, index) => (
|
||||
<Button
|
||||
key={`primary-${index}`}
|
||||
variant={action.destructive ? 'outline' : (action.variant || 'primary')}
|
||||
size="sm"
|
||||
className={`
|
||||
${primaryActions.length === 1 ? 'w-full sm:w-auto sm:min-w-[120px]' : 'flex-1'}
|
||||
${action.destructive ? 'text-red-600 border-red-300 hover:bg-red-50 hover:border-red-400' : ''}
|
||||
font-medium transition-all duration-200
|
||||
`}
|
||||
onClick={action.onClick}
|
||||
>
|
||||
{action.icon && <action.icon className="w-4 h-4 mr-2 flex-shrink-0" />}
|
||||
<span className="truncate">{action.label}</span>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Secondary Actions - Compact horizontal layout */}
|
||||
{secondaryActions.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{secondaryActions.map((action, index) => (
|
||||
<Button
|
||||
key={`secondary-${index}`}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className={`
|
||||
flex-shrink-0 min-w-0
|
||||
${action.destructive ? 'text-red-600 border-red-300 hover:bg-red-50 hover:border-red-400' : ''}
|
||||
${secondaryActions.length > 3 ? 'text-xs px-2' : 'text-sm px-3'}
|
||||
transition-all duration-200
|
||||
`}
|
||||
onClick={action.onClick}
|
||||
>
|
||||
{action.icon && <action.icon className={`
|
||||
${secondaryActions.length > 3 ? 'w-3 h-3' : 'w-4 h-4'}
|
||||
${action.label ? 'mr-1 sm:mr-2' : ''}
|
||||
flex-shrink-0
|
||||
`} />}
|
||||
<span className={`
|
||||
${secondaryActions.length > 3 ? 'hidden sm:inline' : ''}
|
||||
truncate
|
||||
`}>
|
||||
{action.label}
|
||||
</span>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -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<any>(null);
|
||||
const [editingPlan, setEditingPlan] = useState<any>(null);
|
||||
const [editFormData, setEditFormData] = useState<any>({});
|
||||
|
||||
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 (
|
||||
<div className="flex justify-center items-center h-64">
|
||||
@@ -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 = () => {
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Procurement Plans Grid */}
|
||||
{/* Procurement Plans Grid - Mobile-Optimized */}
|
||||
{activeTab === 'plans' && (
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
<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)]" />
|
||||
@@ -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 (
|
||||
<StatusCard
|
||||
key={plan.id}
|
||||
id={plan.plan_number}
|
||||
statusIndicator={statusConfig}
|
||||
title={`Plan ${plan.plan_number}`}
|
||||
subtitle={new Date(plan.plan_date).toLocaleDateString('es-ES')}
|
||||
primaryValue={formatters.currency(plan.total_estimated_cost)}
|
||||
primaryValueLabel={`${plan.total_requirements} requerimientos`}
|
||||
secondaryInfo={{
|
||||
label: 'Período',
|
||||
value: `${new Date(plan.plan_period_start).toLocaleDateString('es-ES')} - ${new Date(plan.plan_period_end).toLocaleDateString('es-ES')}`
|
||||
}}
|
||||
metadata={[
|
||||
`${plan.planning_horizon_days} días de horizonte`,
|
||||
`Estrategia: ${plan.procurement_strategy}`,
|
||||
...(plan.special_requirements ? [`"${plan.special_requirements}"`] : [])
|
||||
]}
|
||||
actions={[
|
||||
{
|
||||
label: 'Ver',
|
||||
icon: Eye,
|
||||
variant: 'outline',
|
||||
onClick: () => {
|
||||
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'
|
||||
});
|
||||
}
|
||||
}] : [])
|
||||
]}
|
||||
/>
|
||||
<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')}
|
||||
primaryValue={formatters.currency(plan.total_estimated_cost)}
|
||||
primaryValueLabel={`${plan.total_requirements} requerimientos`}
|
||||
secondaryInfo={{
|
||||
label: 'Período',
|
||||
value: `${new Date(plan.plan_period_start).toLocaleDateString('es-ES')} - ${new Date(plan.plan_period_end).toLocaleDateString('es-ES')}`
|
||||
}}
|
||||
metadata={[
|
||||
`${plan.planning_horizon_days} días de horizonte`,
|
||||
`Estrategia: ${plan.procurement_strategy}`,
|
||||
...(plan.special_requirements ? [`"${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>
|
||||
);
|
||||
})
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user