Add statuscard changes

This commit is contained in:
Urtzi Alfaro
2025-09-11 13:01:35 +02:00
parent 44b789f523
commit 523b926854
3 changed files with 320 additions and 67 deletions

View File

@@ -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 = (

View File

@@ -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>

View File

@@ -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>
);
})
)}