Add fixes to procurement logic and fix rel-time connections

This commit is contained in:
Urtzi Alfaro
2025-10-02 13:20:30 +02:00
parent c9d8d1d071
commit 1243c2ca6d
24 changed files with 4984 additions and 348 deletions

View File

@@ -0,0 +1,748 @@
# 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!