749 lines
20 KiB
Markdown
749 lines
20 KiB
Markdown
|
|
# 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`)
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// 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`)
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
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`)
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// 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
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
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
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
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
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
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
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
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
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
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
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
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:
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
{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
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
<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**
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
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
|
||
|
|
|
||
|
|
- [x] ✅ Types updated (`frontend/src/api/types/orders.ts`)
|
||
|
|
- [x] ✅ API methods added (`frontend/src/api/services/orders.ts`)
|
||
|
|
- [x] ✅ 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
|
||
|
|
|
||
|
|
1. **Import the hooks you need:**
|
||
|
|
```typescript
|
||
|
|
import {
|
||
|
|
useRecalculateProcurementPlan,
|
||
|
|
useApproveProcurementPlan,
|
||
|
|
useRejectProcurementPlan,
|
||
|
|
useCreatePurchaseOrdersFromPlan,
|
||
|
|
useLinkRequirementToPurchaseOrder,
|
||
|
|
useUpdateRequirementDeliveryStatus,
|
||
|
|
} from '@/api/hooks/orders';
|
||
|
|
```
|
||
|
|
|
||
|
|
2. **Use in your component:**
|
||
|
|
```typescript
|
||
|
|
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!' });
|
||
|
|
```
|
||
|
|
|
||
|
|
3. **Check loading state:**
|
||
|
|
```typescript
|
||
|
|
{approveMutation.isPending && <Loader />}
|
||
|
|
```
|
||
|
|
|
||
|
|
4. **Access data:**
|
||
|
|
```typescript
|
||
|
|
{approveMutation.data?.approval_workflow.map(...)}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 💡 TIPS
|
||
|
|
|
||
|
|
- **Cache Invalidation**: All hooks automatically invalidate related queries, so your UI updates automatically
|
||
|
|
- **Error Handling**: Use `onError` callback to show user-friendly error messages
|
||
|
|
- **Loading States**: Use `isPending` to show loading spinners
|
||
|
|
- **Optimistic Updates**: Consider using `onMutate` for 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!
|