20 KiB
20 KiB
FRONTEND INTEGRATION GUIDE - Procurement Features
✅ COMPLETED FRONTEND CHANGES
All TypeScript types, API service methods, and React hooks have been implemented. This guide shows how to use them in your components.
📦 WHAT'S BEEN ADDED
1. New Types (frontend/src/api/types/orders.ts)
// Approval workflow tracking
export interface ApprovalWorkflowEntry {
timestamp: string;
from_status: string;
to_status: string;
user_id?: string;
notes?: string;
}
// Purchase order creation result
export interface CreatePOsResult {
success: boolean;
created_pos: Array<{
po_id: string;
po_number: string;
supplier_id: string;
items_count: number;
total_amount: number;
}>;
failed_pos: Array<{
supplier_id: string;
error: string;
}>;
total_created: number;
total_failed: number;
}
// Request types
export interface LinkRequirementToPORequest {
purchase_order_id: string;
purchase_order_number: string;
ordered_quantity: number;
expected_delivery_date?: string;
}
export interface UpdateDeliveryStatusRequest {
delivery_status: string;
received_quantity?: number;
actual_delivery_date?: string;
quality_rating?: number;
}
export interface ApprovalRequest {
approval_notes?: string;
}
export interface RejectionRequest {
rejection_notes?: string;
}
Updated ProcurementPlanResponse:
- Added
approval_workflow?: ApprovalWorkflowEntry[]- tracks all approval actions
2. New API Methods (frontend/src/api/services/orders.ts)
class OrdersService {
// Recalculate plan with current inventory
static async recalculateProcurementPlan(tenantId: string, planId: string): Promise<GeneratePlanResponse>
// Approve plan with notes
static async approveProcurementPlan(tenantId: string, planId: string, request?: ApprovalRequest): Promise<ProcurementPlanResponse>
// Reject plan with notes
static async rejectProcurementPlan(tenantId: string, planId: string, request?: RejectionRequest): Promise<ProcurementPlanResponse>
// Auto-create POs from plan
static async createPurchaseOrdersFromPlan(tenantId: string, planId: string, autoApprove?: boolean): Promise<CreatePOsResult>
// Link requirement to PO
static async linkRequirementToPurchaseOrder(tenantId: string, requirementId: string, request: LinkRequirementToPORequest): Promise<{...}>
// Update delivery status
static async updateRequirementDeliveryStatus(tenantId: string, requirementId: string, request: UpdateDeliveryStatusRequest): Promise<{...}>
}
3. New React Hooks (frontend/src/api/hooks/orders.ts)
// Recalculate plan
useRecalculateProcurementPlan(options?)
// Approve plan
useApproveProcurementPlan(options?)
// Reject plan
useRejectProcurementPlan(options?)
// Create POs from plan
useCreatePurchaseOrdersFromPlan(options?)
// Link requirement to PO
useLinkRequirementToPurchaseOrder(options?)
// Update delivery status
useUpdateRequirementDeliveryStatus(options?)
🎨 HOW TO USE IN COMPONENTS
Example 1: Recalculate Plan Button
import { useRecalculateProcurementPlan } from '@/api/hooks/orders';
import { useToast } from '@/hooks/useToast';
function ProcurementPlanActions({ plan, tenantId }) {
const { toast } = useToast();
const recalculateMutation = useRecalculateProcurementPlan({
onSuccess: (data) => {
if (data.success && data.plan) {
toast({
title: 'Plan recalculado',
description: `Plan actualizado con ${data.plan.total_requirements} requerimientos`,
variant: 'success',
});
}
},
onError: (error) => {
toast({
title: 'Error al recalcular',
description: error.message,
variant: 'destructive',
});
},
});
const handleRecalculate = () => {
if (confirm('¿Recalcular el plan con el inventario actual?')) {
recalculateMutation.mutate({ tenantId, planId: plan.id });
}
};
// Show warning if plan is old
const planAgeHours = (new Date().getTime() - new Date(plan.created_at).getTime()) / (1000 * 60 * 60);
const isStale = planAgeHours > 24;
return (
<div>
{isStale && (
<Alert variant="warning">
<AlertCircle className="h-4 w-4" />
<AlertTitle>Plan desactualizado</AlertTitle>
<AlertDescription>
Este plan tiene más de 24 horas. El inventario puede haber cambiado.
</AlertDescription>
</Alert>
)}
<Button
onClick={handleRecalculate}
disabled={recalculateMutation.isPending}
variant="outline"
>
{recalculateMutation.isPending ? 'Recalculando...' : 'Recalcular Plan'}
</Button>
</div>
);
}
Example 2: Approve/Reject Plan with Notes
import { useApproveProcurementPlan, useRejectProcurementPlan } from '@/api/hooks/orders';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Textarea } from '@/components/ui/textarea';
function ApprovalDialog({ plan, tenantId, open, onClose }) {
const [notes, setNotes] = useState('');
const [action, setAction] = useState<'approve' | 'reject'>('approve');
const approveMutation = useApproveProcurementPlan({
onSuccess: () => {
toast({ title: 'Plan aprobado', variant: 'success' });
onClose();
},
});
const rejectMutation = useRejectProcurementPlan({
onSuccess: () => {
toast({ title: 'Plan rechazado', variant: 'success' });
onClose();
},
});
const handleSubmit = () => {
if (action === 'approve') {
approveMutation.mutate({
tenantId,
planId: plan.id,
approval_notes: notes || undefined,
});
} else {
rejectMutation.mutate({
tenantId,
planId: plan.id,
rejection_notes: notes || undefined,
});
}
};
return (
<Dialog open={open} onOpenChange={onClose}>
<DialogContent>
<DialogHeader>
<DialogTitle>
{action === 'approve' ? 'Aprobar' : 'Rechazar'} Plan de Compras
</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div>
<label className="text-sm font-medium">Notas (opcional)</label>
<Textarea
value={notes}
onChange={(e) => setNotes(e.target.value)}
placeholder={
action === 'approve'
? 'Razón de aprobación...'
: 'Razón de rechazo...'
}
rows={4}
/>
</div>
<div className="flex gap-2">
<Button
variant="outline"
onClick={() => setAction('reject')}
className={action === 'reject' ? 'bg-red-50' : ''}
>
Rechazar
</Button>
<Button
onClick={() => setAction('approve')}
className={action === 'approve' ? 'bg-green-50' : ''}
>
Aprobar
</Button>
</div>
<Button
onClick={handleSubmit}
disabled={approveMutation.isPending || rejectMutation.isPending}
className="w-full"
>
Confirmar
</Button>
</div>
</DialogContent>
</Dialog>
);
}
Example 3: Auto-Create Purchase Orders
import { useCreatePurchaseOrdersFromPlan } from '@/api/hooks/orders';
function CreatePOsButton({ plan, tenantId }) {
const createPOsMutation = useCreatePurchaseOrdersFromPlan({
onSuccess: (result) => {
if (result.success) {
toast({
title: `${result.total_created} órdenes de compra creadas`,
description: result.created_pos.map(po =>
`${po.po_number}: ${po.items_count} items - $${po.total_amount.toFixed(2)}`
).join('\n'),
variant: 'success',
});
if (result.failed_pos.length > 0) {
toast({
title: `${result.total_failed} órdenes fallaron`,
description: result.failed_pos.map(f => f.error).join('\n'),
variant: 'destructive',
});
}
}
},
});
const handleCreatePOs = () => {
if (plan.status !== 'approved') {
toast({
title: 'Plan no aprobado',
description: 'Debes aprobar el plan antes de crear órdenes de compra',
variant: 'warning',
});
return;
}
if (confirm(`Crear órdenes de compra para ${plan.total_requirements} requerimientos?`)) {
createPOsMutation.mutate({
tenantId,
planId: plan.id,
autoApprove: false, // Set to true for auto-approval
});
}
};
return (
<Button
onClick={handleCreatePOs}
disabled={createPOsMutation.isPending || plan.status !== 'approved'}
>
{createPOsMutation.isPending ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Creando órdenes...
</>
) : (
<>
<FileText className="mr-2 h-4 w-4" />
Crear Órdenes de Compra
</>
)}
</Button>
);
}
Example 4: Display Approval Workflow History
function ApprovalHistory({ plan }: { plan: ProcurementPlanResponse }) {
if (!plan.approval_workflow || plan.approval_workflow.length === 0) {
return null;
}
return (
<Card>
<CardHeader>
<CardTitle>Historial de Aprobaciones</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-3">
{plan.approval_workflow.map((entry, index) => (
<div key={index} className="flex items-start gap-3 border-l-2 border-gray-200 pl-4">
<div className="flex-1">
<div className="flex items-center gap-2">
<Badge variant={entry.to_status === 'approved' ? 'success' : 'destructive'}>
{entry.from_status} → {entry.to_status}
</Badge>
<span className="text-xs text-gray-500">
{new Date(entry.timestamp).toLocaleString()}
</span>
</div>
{entry.notes && (
<p className="text-sm text-gray-600 mt-1">{entry.notes}</p>
)}
{entry.user_id && (
<p className="text-xs text-gray-400 mt-1">Usuario: {entry.user_id}</p>
)}
</div>
</div>
))}
</div>
</CardContent>
</Card>
);
}
Example 5: Requirements Table with Supplier Info
function RequirementsTable({ requirements }: { requirements: ProcurementRequirementResponse[] }) {
return (
<Table>
<TableHeader>
<TableRow>
<TableHead>Producto</TableHead>
<TableHead>Cantidad</TableHead>
<TableHead>Proveedor</TableHead>
<TableHead>Lead Time</TableHead>
<TableHead>Orden de Compra</TableHead>
<TableHead>Estado de Entrega</TableHead>
<TableHead>Acciones</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{requirements.map((req) => (
<TableRow key={req.id}>
<TableCell>
<div>
<div className="font-medium">{req.product_name}</div>
<div className="text-xs text-gray-500">{req.product_sku}</div>
</div>
</TableCell>
<TableCell>
{req.net_requirement} {req.unit_of_measure}
</TableCell>
<TableCell>
{req.supplier_name ? (
<div>
<div className="font-medium">{req.supplier_name}</div>
{req.minimum_order_quantity && (
<div className="text-xs text-gray-500">
Mín: {req.minimum_order_quantity}
</div>
)}
</div>
) : (
<Badge variant="warning">Sin proveedor</Badge>
)}
</TableCell>
<TableCell>
{req.supplier_lead_time_days ? (
<Badge variant="outline">{req.supplier_lead_time_days} días</Badge>
) : (
'-'
)}
</TableCell>
<TableCell>
{req.purchase_order_number ? (
<a href={`/purchase-orders/${req.purchase_order_id}`} className="text-blue-600 hover:underline">
{req.purchase_order_number}
</a>
) : (
<Badge variant="secondary">Pendiente</Badge>
)}
</TableCell>
<TableCell>
<DeliveryStatusBadge status={req.delivery_status} onTime={req.on_time_delivery} />
</TableCell>
<TableCell>
<RequirementActions requirement={req} />
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
);
}
Example 6: Update Delivery Status
function UpdateDeliveryDialog({ requirement, tenantId, open, onClose }) {
const [formData, setFormData] = useState({
delivery_status: requirement.delivery_status,
received_quantity: requirement.received_quantity || 0,
actual_delivery_date: requirement.actual_delivery_date || '',
quality_rating: requirement.quality_rating || 5,
});
const updateMutation = useUpdateRequirementDeliveryStatus({
onSuccess: () => {
toast({ title: 'Estado actualizado', variant: 'success' });
onClose();
},
});
const handleSubmit = () => {
updateMutation.mutate({
tenantId,
requirementId: requirement.id,
request: {
delivery_status: formData.delivery_status,
received_quantity: formData.received_quantity,
actual_delivery_date: formData.actual_delivery_date || undefined,
quality_rating: formData.quality_rating,
},
});
};
return (
<Dialog open={open} onOpenChange={onClose}>
<DialogContent>
<DialogHeader>
<DialogTitle>Actualizar Estado de Entrega</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div>
<label>Estado</label>
<Select
value={formData.delivery_status}
onValueChange={(value) =>
setFormData({ ...formData, delivery_status: value })
}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="pending">Pendiente</SelectItem>
<SelectItem value="in_transit">En Tránsito</SelectItem>
<SelectItem value="delivered">Entregado</SelectItem>
<SelectItem value="delayed">Retrasado</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<label>Cantidad Recibida</label>
<Input
type="number"
value={formData.received_quantity}
onChange={(e) =>
setFormData({ ...formData, received_quantity: Number(e.target.value) })
}
/>
<p className="text-xs text-gray-500 mt-1">
Ordenado: {requirement.ordered_quantity} {requirement.unit_of_measure}
</p>
</div>
<div>
<label>Fecha de Entrega Real</label>
<Input
type="date"
value={formData.actual_delivery_date}
onChange={(e) =>
setFormData({ ...formData, actual_delivery_date: e.target.value })
}
/>
</div>
<div>
<label>Calificación de Calidad (1-10)</label>
<Input
type="number"
min="1"
max="10"
value={formData.quality_rating}
onChange={(e) =>
setFormData({ ...formData, quality_rating: Number(e.target.value) })
}
/>
</div>
<Button onClick={handleSubmit} disabled={updateMutation.isPending} className="w-full">
{updateMutation.isPending ? 'Actualizando...' : 'Actualizar Estado'}
</Button>
</div>
</DialogContent>
</Dialog>
);
}
🎯 RECOMMENDED UI UPDATES
1. ProcurementPage.tsx - Add Action Buttons
Add these buttons to the plan header:
{plan.status === 'draft' && (
<>
<Button onClick={() => setShowRecalculateDialog(true)}>
Recalcular
</Button>
<Button onClick={() => setShowApprovalDialog(true)}>
Aprobar / Rechazar
</Button>
</>
)}
{plan.status === 'approved' && (
<Button onClick={() => handleCreatePOs()}>
Crear Órdenes de Compra Automáticamente
</Button>
)}
2. Requirements Table - Add Columns
Update your requirements table to show:
- Supplier Name (with link)
- Lead Time badge
- PO Number (with link if exists)
- Delivery Status badge with on-time indicator
- Action dropdown with "Update Delivery" option
3. Plan Details Card - Show New Metrics
<div className="grid grid-cols-3 gap-4">
<MetricCard
title="Ajuste Estacional"
value={`${((plan.seasonality_adjustment - 1) * 100).toFixed(0)}%`}
icon={<TrendingUp />}
/>
<MetricCard
title="Diversificación de Proveedores"
value={`${plan.supplier_diversification_score}/10`}
icon={<Users />}
/>
<MetricCard
title="Proveedores Únicos"
value={plan.primary_suppliers_count}
icon={<Building />}
/>
</div>
4. Dashboard Performance Metrics
function ProcurementMetrics({ metrics }) {
return (
<Card>
<CardHeader>
<CardTitle>Métricas de Desempeño</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
<ProgressMetric
label="Tasa de Cumplimiento"
value={metrics.average_fulfillment_rate}
target={95}
/>
<ProgressMetric
label="Entregas a Tiempo"
value={metrics.average_on_time_delivery}
target={90}
/>
<ProgressMetric
label="Precisión de Costos"
value={metrics.cost_accuracy}
target={95}
/>
<ProgressMetric
label="Calidad de Proveedores"
value={metrics.supplier_performance * 10}
target={80}
/>
</div>
</CardContent>
</Card>
);
}
📋 INTEGRATION CHECKLIST
- ✅ Types updated (
frontend/src/api/types/orders.ts) - ✅ API methods added (
frontend/src/api/services/orders.ts) - ✅ React hooks created (
frontend/src/api/hooks/orders.ts) - 🔲 Add Recalculate button to ProcurementPage
- 🔲 Add Approve/Reject modal to ProcurementPage
- 🔲 Add Auto-Create POs button to ProcurementPage
- 🔲 Update Requirements table with supplier columns
- 🔲 Add delivery status update functionality
- 🔲 Display approval workflow history
- 🔲 Show performance metrics on dashboard
- 🔲 Add supplier info to requirement cards
- 🔲 Show seasonality and diversity scores
🚀 QUICK START
- Import the hooks you need:
import {
useRecalculateProcurementPlan,
useApproveProcurementPlan,
useRejectProcurementPlan,
useCreatePurchaseOrdersFromPlan,
useLinkRequirementToPurchaseOrder,
useUpdateRequirementDeliveryStatus,
} from '@/api/hooks/orders';
- Use in your component:
const approveMutation = useApproveProcurementPlan({
onSuccess: () => toast({ title: 'Success!' }),
onError: (error) => toast({ title: 'Error', description: error.message }),
});
// Call it
approveMutation.mutate({ tenantId, planId, approval_notes: 'Looks good!' });
- Check loading state:
{approveMutation.isPending && <Loader />}
- Access data:
{approveMutation.data?.approval_workflow.map(...)}
💡 TIPS
- Cache Invalidation: All hooks automatically invalidate related queries, so your UI updates automatically
- Error Handling: Use
onErrorcallback to show user-friendly error messages - Loading States: Use
isPendingto show loading spinners - Optimistic Updates: Consider using
onMutatefor instant UI feedback - TypeScript: All types are fully typed for autocomplete and type safety
🎉 YOU'RE READY!
All backend functionality is implemented and all frontend infrastructure (types, services, hooks) is ready. Just add the UI components following the examples above and your procurement system will be fully functional!