Files
bakery-ia/FRONTEND_INTEGRATION_GUIDE.md

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

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

  1. Import the hooks you need:
import {
  useRecalculateProcurementPlan,
  useApproveProcurementPlan,
  useRejectProcurementPlan,
  useCreatePurchaseOrdersFromPlan,
  useLinkRequirementToPurchaseOrder,
  useUpdateRequirementDeliveryStatus,
} from '@/api/hooks/orders';
  1. 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!' });
  1. Check loading state:
{approveMutation.isPending && <Loader />}
  1. Access data:
{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!