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 = { const sizeClasses = {
xs: 'px-2 py-1 text-xs gap-1 min-h-6', 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', 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', 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', 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' xl: 'px-8 py-3 text-lg gap-3 min-h-14 sm:min-h-16'
}; };
const loadingSpinner = ( const loadingSpinner = (

View File

@@ -34,9 +34,12 @@ export interface StatusCardProps {
icon?: LucideIcon; icon?: LucideIcon;
variant?: 'primary' | 'outline'; variant?: 'primary' | 'outline';
onClick: () => void; onClick: () => void;
priority?: 'primary' | 'secondary' | 'tertiary';
destructive?: boolean;
}>; }>;
onClick?: () => void; onClick?: () => void;
className?: string; className?: string;
compact?: boolean;
} }
/** /**
@@ -74,7 +77,7 @@ export const getStatusColor = (status: string): StatusIndicatorConfig['color'] =
* StatusCard - Reusable card component with consistent status styling * StatusCard - Reusable card component with consistent status styling
*/ */
export const StatusCard: React.FC<StatusCardProps> = ({ export const StatusCard: React.FC<StatusCardProps> = ({
id, id: _id,
statusIndicator, statusIndicator,
title, title,
subtitle, subtitle,
@@ -86,10 +89,23 @@ export const StatusCard: React.FC<StatusCardProps> = ({
actions = [], actions = [],
onClick, onClick,
className = '', className = '',
compact: _compact = false,
}) => { }) => {
const StatusIcon = statusIndicator.icon; const StatusIcon = statusIndicator.icon;
const hasInteraction = onClick || actions.length > 0; 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 ( return (
<Card <Card
className={` className={`
@@ -201,21 +217,65 @@ export const StatusCard: React.FC<StatusCardProps> = ({
</div> </div>
)} )}
{/* Actions */} {/* Enhanced Mobile-Responsive Actions */}
{actions.length > 0 && ( {actions.length > 0 && (
<div className="flex gap-2 pt-3 border-t border-[var(--border-primary)]"> <div className="pt-3 border-t border-[var(--border-primary)]">
{actions.map((action, index) => ( {/* Primary Actions - Full width on mobile, inline on desktop */}
<Button {primaryActions.length > 0 && (
key={index} <div className={`
variant={action.variant || 'outline'} flex gap-2 mb-2
size="sm" ${primaryActions.length > 1 ? 'flex-col sm:flex-row' : ''}
className="flex-1" `}>
onClick={action.onClick} {primaryActions.map((action, index) => (
> <Button
{action.icon && <action.icon className="w-4 h-4 mr-2" />} key={`primary-${index}`}
{action.label} variant={action.destructive ? 'outline' : (action.variant || 'primary')}
</Button> 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>
)} )}
</div> </div>

View File

@@ -1,5 +1,5 @@
import React, { useState } from 'react'; 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 { Button, Input, Card, StatsGrid, StatusCard, getStatusColor, StatusModal } from '../../../../components/ui';
import { formatters } from '../../../../components/ui/Stats/StatsPresets'; import { formatters } from '../../../../components/ui/Stats/StatsPresets';
import { PageHeader } from '../../../../components/layout'; import { PageHeader } from '../../../../components/layout';
@@ -20,6 +20,8 @@ const ProcurementPage: React.FC = () => {
const [showForm, setShowForm] = useState(false); const [showForm, setShowForm] = useState(false);
const [modalMode, setModalMode] = useState<'view' | 'edit'>('view'); const [modalMode, setModalMode] = useState<'view' | 'edit'>('view');
const [selectedPlan, setSelectedPlan] = useState<any>(null); const [selectedPlan, setSelectedPlan] = useState<any>(null);
const [editingPlan, setEditingPlan] = useState<any>(null);
const [editFormData, setEditFormData] = useState<any>({});
const { currentTenant } = useTenantStore(); const { currentTenant } = useTenantStore();
const tenantId = currentTenant?.id || ''; const tenantId = currentTenant?.id || '';
@@ -38,6 +40,73 @@ const ProcurementPage: React.FC = () => {
const updatePlanStatusMutation = useUpdateProcurementPlanStatus(); const updatePlanStatusMutation = useUpdateProcurementPlanStatus();
const triggerSchedulerMutation = useTriggerDailyScheduler(); 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) { if (!tenantId) {
return ( return (
<div className="flex justify-center items-center h-64"> <div className="flex justify-center items-center h-64">
@@ -162,10 +231,24 @@ const ProcurementPage: React.FC = () => {
}, },
{ {
id: "trigger", id: "trigger",
label: "Ejecutar Programador", label: triggerSchedulerMutation.isPending ? "Ejecutando..." : "Ejecutar Programador",
variant: "outline" as const, variant: "outline" as const,
icon: Calendar, icon: triggerSchedulerMutation.isPending ? Loader : Calendar,
onClick: () => triggerSchedulerMutation.mutate(tenantId) 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> </Card>
)} )}
{/* Procurement Plans Grid */} {/* Procurement Plans Grid - Mobile-Optimized */}
{activeTab === 'plans' && ( {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 ? ( {isPlansLoading ? (
<div className="col-span-full flex justify-center items-center h-32"> <div className="col-span-full flex justify-center items-center h-32">
<Loader className="w-8 h-8 animate-spin text-[var(--color-primary)]" /> <Loader className="w-8 h-8 animate-spin text-[var(--color-primary)]" />
@@ -247,50 +330,160 @@ const ProcurementPage: React.FC = () => {
) : ( ) : (
filteredPlans.map((plan) => { filteredPlans.map((plan) => {
const statusConfig = getPlanStatusConfig(plan.status); 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 ( return (
<StatusCard <div key={plan.id} className="space-y-2">
key={plan.id} <StatusCard
id={plan.plan_number} id={plan.plan_number}
statusIndicator={statusConfig} statusIndicator={statusConfig}
title={`Plan ${plan.plan_number}`} title={`Plan ${plan.plan_number}`}
subtitle={new Date(plan.plan_date).toLocaleDateString('es-ES')} subtitle={new Date(plan.plan_date).toLocaleDateString('es-ES')}
primaryValue={formatters.currency(plan.total_estimated_cost)} primaryValue={formatters.currency(plan.total_estimated_cost)}
primaryValueLabel={`${plan.total_requirements} requerimientos`} primaryValueLabel={`${plan.total_requirements} requerimientos`}
secondaryInfo={{ secondaryInfo={{
label: 'Período', label: 'Período',
value: `${new Date(plan.plan_period_start).toLocaleDateString('es-ES')} - ${new Date(plan.plan_period_end).toLocaleDateString('es-ES')}` value: `${new Date(plan.plan_period_start).toLocaleDateString('es-ES')} - ${new Date(plan.plan_period_end).toLocaleDateString('es-ES')}`
}} }}
metadata={[ metadata={[
`${plan.planning_horizon_days} días de horizonte`, `${plan.planning_horizon_days} días de horizonte`,
`Estrategia: ${plan.procurement_strategy}`, `Estrategia: ${plan.procurement_strategy}`,
...(plan.special_requirements ? [`"${plan.special_requirements}"`] : []) ...(plan.special_requirements ? [`"${plan.special_requirements}"`] : [])
]} ]}
actions={[ actions={actions}
{ />
label: 'Ver',
icon: Eye, {/* Inline Edit Form for Draft Plans */}
variant: 'outline', {isEditing && (
onClick: () => { <Card className="p-4 border-l-4 border-l-[var(--color-primary)] bg-[var(--color-primary)]/5">
setSelectedPlan(plan); <h4 className="text-sm font-semibold text-[var(--text-primary)] mb-3">
setModalMode('view'); Editando Plan {plan.plan_number}
setShowForm(true); </h4>
} <div className="space-y-3">
}, <div>
...(plan.status === 'pending_approval' ? [{ <label className="block text-xs font-medium text-[var(--text-secondary)] mb-1">
label: 'Aprobar', Requerimientos Especiales
icon: CheckCircle, </label>
variant: 'outline' as const, <textarea
onClick: () => { value={editFormData.special_requirements}
updatePlanStatusMutation.mutate({ onChange={(e) => setEditFormData((prev: any) => ({ ...prev, special_requirements: e.target.value }))}
tenant_id: tenantId, placeholder="Ingrese requerimientos especiales para este plan..."
plan_id: plan.id, className="w-full px-3 py-2 text-sm border border-[var(--border-primary)] rounded-lg
status: 'approved' 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>
); );
}) })
)} )}