Files
bakery-ia/frontend/src/components/suppliers/PurchaseOrderManagementPage.tsx
2025-08-14 13:26:59 +02:00

619 lines
21 KiB
TypeScript

import React, { useState, useEffect, useMemo } from 'react';
import {
Search,
Filter,
Plus,
Download,
RefreshCw,
ChevronDown,
FileText,
TrendingUp,
Clock,
AlertCircle,
Package,
DollarSign,
Grid,
List
} from 'lucide-react';
import {
usePurchaseOrders,
PurchaseOrder,
CreatePurchaseOrderRequest
} from '../../api/hooks/useSuppliers';
import { useAuth } from '../../api/hooks/useAuth';
import PurchaseOrderCard from './PurchaseOrderCard';
import PurchaseOrderForm from './PurchaseOrderForm';
import Card from '../ui/Card';
import Button from '../ui/Button';
import LoadingSpinner from '../ui/LoadingSpinner';
interface PurchaseOrderFilters {
search: string;
supplier_id: string;
status: string;
priority: string;
date_from: string;
date_to: string;
}
const PurchaseOrderManagementPage: React.FC = () => {
const { user } = useAuth();
const {
purchaseOrders,
purchaseOrder: selectedPurchaseOrder,
statistics,
ordersRequiringApproval,
overdueOrders,
isLoading,
isCreating,
error,
pagination,
loadPurchaseOrders,
loadPurchaseOrder,
loadStatistics,
loadOrdersRequiringApproval,
loadOverdueOrders,
createPurchaseOrder,
updateOrderStatus,
approveOrder,
sendToSupplier,
cancelOrder,
clearError,
refresh,
setPage
} = usePurchaseOrders();
const [filters, setFilters] = useState<PurchaseOrderFilters>({
search: '',
supplier_id: '',
status: '',
priority: '',
date_from: '',
date_to: ''
});
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
const [showFilters, setShowFilters] = useState(false);
const [showPurchaseOrderForm, setShowPurchaseOrderForm] = useState(false);
const [selectedOrder, setSelectedOrder] = useState<PurchaseOrder | null>(null);
// Load initial data
useEffect(() => {
if (user?.tenant_id) {
loadPurchaseOrders();
loadStatistics();
loadOrdersRequiringApproval();
loadOverdueOrders();
}
}, [user?.tenant_id]);
// Apply filters
useEffect(() => {
const searchParams: any = {};
if (filters.search) {
searchParams.search_term = filters.search;
}
if (filters.supplier_id) {
searchParams.supplier_id = filters.supplier_id;
}
if (filters.status) {
searchParams.status = filters.status;
}
if (filters.priority) {
searchParams.priority = filters.priority;
}
if (filters.date_from) {
searchParams.date_from = filters.date_from;
}
if (filters.date_to) {
searchParams.date_to = filters.date_to;
}
loadPurchaseOrders(searchParams);
}, [filters]);
// Status options
const statusOptions = [
{ value: '', label: 'Todos los estados' },
{ value: 'DRAFT', label: 'Borrador' },
{ value: 'PENDING_APPROVAL', label: 'Pendiente Aprobación' },
{ value: 'APPROVED', label: 'Aprobado' },
{ value: 'SENT_TO_SUPPLIER', label: 'Enviado a Proveedor' },
{ value: 'CONFIRMED', label: 'Confirmado' },
{ value: 'PARTIALLY_RECEIVED', label: 'Recibido Parcial' },
{ value: 'COMPLETED', label: 'Completado' },
{ value: 'CANCELLED', label: 'Cancelado' },
{ value: 'DISPUTED', label: 'En Disputa' }
];
// Priority options
const priorityOptions = [
{ value: '', label: 'Todas las prioridades' },
{ value: 'LOW', label: 'Baja' },
{ value: 'NORMAL', label: 'Normal' },
{ value: 'HIGH', label: 'Alta' },
{ value: 'URGENT', label: 'Urgente' }
];
// Handle purchase order creation
const handleCreatePurchaseOrder = async (orderData: CreatePurchaseOrderRequest) => {
const order = await createPurchaseOrder(orderData);
if (order) {
setShowPurchaseOrderForm(false);
// Refresh statistics and special lists
loadStatistics();
if (order.status === 'PENDING_APPROVAL') loadOrdersRequiringApproval();
}
};
// Handle order approval
const handleApproveOrder = async (
order: PurchaseOrder,
action: 'approve' | 'reject',
notes?: string
) => {
const updatedOrder = await approveOrder(order.id, action, notes);
if (updatedOrder) {
// Refresh relevant lists
loadOrdersRequiringApproval();
loadStatistics();
}
};
// Handle send to supplier
const handleSendToSupplier = async (order: PurchaseOrder, sendEmail: boolean = true) => {
const updatedOrder = await sendToSupplier(order.id, sendEmail);
if (updatedOrder) {
loadStatistics();
}
};
// Handle cancel order
const handleCancelOrder = async (order: PurchaseOrder, reason: string) => {
const updatedOrder = await cancelOrder(order.id, reason);
if (updatedOrder) {
loadStatistics();
}
};
// Handle clear filters
const handleClearFilters = () => {
setFilters({
search: '',
supplier_id: '',
status: '',
priority: '',
date_from: '',
date_to: ''
});
};
// Format currency
const formatCurrency = (amount: number) => {
return new Intl.NumberFormat('es-ES', {
style: 'currency',
currency: 'EUR',
minimumFractionDigits: 0,
maximumFractionDigits: 0
}).format(amount);
};
// Statistics cards data
const statsCards = useMemo(() => {
if (!statistics) return [];
return [
{
title: 'Total Pedidos',
value: statistics.total_orders.toString(),
icon: FileText,
color: 'blue'
},
{
title: 'Este Mes',
value: statistics.this_month_orders.toString(),
icon: TrendingUp,
color: 'green'
},
{
title: 'Pendientes Aprobación',
value: statistics.pending_approval.toString(),
icon: Clock,
color: 'yellow'
},
{
title: 'Gasto Este Mes',
value: formatCurrency(statistics.this_month_spend),
icon: DollarSign,
color: 'purple'
}
];
}, [statistics]);
if (isLoading && !purchaseOrders.length) {
return (
<div className="flex items-center justify-center h-64">
<LoadingSpinner size="lg" />
</div>
);
}
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-gray-900">Órdenes de Compra</h1>
<p className="text-gray-600">Gestiona tus pedidos y compras a proveedores</p>
</div>
<div className="flex items-center space-x-3">
<Button
variant="outline"
onClick={refresh}
disabled={isLoading}
>
<RefreshCw className={`w-4 h-4 mr-2 ${isLoading ? 'animate-spin' : ''}`} />
Actualizar
</Button>
<Button
onClick={() => setShowPurchaseOrderForm(true)}
disabled={isCreating}
>
<Plus className="w-4 h-4 mr-2" />
Nueva Orden
</Button>
</div>
</div>
{/* Error display */}
{error && (
<div className="bg-red-50 border border-red-200 rounded-lg p-4 flex items-center justify-between">
<div className="flex items-center space-x-2">
<AlertCircle className="w-5 h-5 text-red-500" />
<span className="text-red-700">{error}</span>
</div>
<button
onClick={clearError}
className="text-red-500 hover:text-red-700"
>
</button>
</div>
)}
{/* Statistics Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{statsCards.map((stat, index) => (
<Card key={index} className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">{stat.title}</p>
<p className="text-2xl font-bold text-gray-900">{stat.value}</p>
</div>
<div className={`w-12 h-12 rounded-lg flex items-center justify-center ${
stat.color === 'blue' ? 'bg-blue-100' :
stat.color === 'green' ? 'bg-green-100' :
stat.color === 'yellow' ? 'bg-yellow-100' :
'bg-purple-100'
}`}>
<stat.icon className={`w-6 h-6 ${
stat.color === 'blue' ? 'text-blue-600' :
stat.color === 'green' ? 'text-green-600' :
stat.color === 'yellow' ? 'text-yellow-600' :
'text-purple-600'
}`} />
</div>
</div>
</Card>
))}
</div>
{/* Quick Lists */}
{(ordersRequiringApproval.length > 0 || overdueOrders.length > 0) && (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Orders Requiring Approval */}
{ordersRequiringApproval.length > 0 && (
<Card>
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-gray-900 flex items-center">
<Clock className="w-5 h-5 text-yellow-500 mr-2" />
Requieren Aprobación
</h3>
<span className="bg-yellow-100 text-yellow-800 px-2 py-1 rounded-full text-sm">
{ordersRequiringApproval.length}
</span>
</div>
<div className="space-y-3">
{ordersRequiringApproval.slice(0, 3).map(order => (
<PurchaseOrderCard
key={order.id}
order={order}
compact
onApprove={handleApproveOrder}
onViewDetails={(order) => setSelectedOrder(order)}
/>
))}
{ordersRequiringApproval.length > 3 && (
<button
onClick={() => setFilters(prev => ({ ...prev, status: 'PENDING_APPROVAL' }))}
className="w-full py-2 text-center text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
>
Ver {ordersRequiringApproval.length - 3} más...
</button>
)}
</div>
</Card>
)}
{/* Overdue Orders */}
{overdueOrders.length > 0 && (
<Card>
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-gray-900 flex items-center">
<AlertCircle className="w-5 h-5 text-red-500 mr-2" />
Pedidos Vencidos
</h3>
<span className="bg-red-100 text-red-800 px-2 py-1 rounded-full text-sm">
{overdueOrders.length}
</span>
</div>
<div className="space-y-3">
{overdueOrders.slice(0, 3).map(order => (
<PurchaseOrderCard
key={order.id}
order={order}
compact
onViewDetails={(order) => setSelectedOrder(order)}
/>
))}
{overdueOrders.length > 3 && (
<button
onClick={() => {
const today = new Date().toISOString().split('T')[0];
setFilters(prev => ({ ...prev, date_to: today }));
}}
className="w-full py-2 text-center text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
>
Ver {overdueOrders.length - 3} más...
</button>
)}
</div>
</Card>
)}
</div>
)}
{/* Filters and Search */}
<Card>
<div className="flex flex-col lg:flex-row lg:items-center justify-between space-y-4 lg:space-y-0">
<div className="flex items-center space-x-4">
{/* Search */}
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400" />
<input
type="text"
placeholder="Buscar pedidos..."
value={filters.search}
onChange={(e) => setFilters(prev => ({ ...prev, search: e.target.value }))}
className="pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 w-64"
/>
</div>
{/* Filter Toggle */}
<button
onClick={() => setShowFilters(!showFilters)}
className={`flex items-center space-x-2 px-3 py-2 border rounded-lg transition-colors ${
showFilters ? 'bg-blue-50 border-blue-200 text-blue-700' : 'border-gray-300 text-gray-700 hover:bg-gray-50'
}`}
>
<Filter className="w-4 h-4" />
<span>Filtros</span>
<ChevronDown className={`w-4 h-4 transform transition-transform ${showFilters ? 'rotate-180' : ''}`} />
</button>
{/* Active filters indicator */}
{(filters.status || filters.priority || filters.supplier_id || filters.date_from || filters.date_to) && (
<div className="flex items-center space-x-2">
<span className="text-sm text-gray-500">Filtros activos:</span>
{filters.status && (
<span className="px-2 py-1 bg-blue-100 text-blue-800 text-xs rounded-full">
{statusOptions.find(opt => opt.value === filters.status)?.label}
</span>
)}
{filters.priority && (
<span className="px-2 py-1 bg-green-100 text-green-800 text-xs rounded-full">
{priorityOptions.find(opt => opt.value === filters.priority)?.label}
</span>
)}
<button
onClick={handleClearFilters}
className="text-xs text-red-600 hover:text-red-700"
>
Limpiar
</button>
</div>
)}
</div>
<div className="flex items-center space-x-3">
{/* View Mode Toggle */}
<div className="flex items-center space-x-1 border border-gray-300 rounded-lg p-1">
<button
onClick={() => setViewMode('grid')}
className={`p-2 rounded ${viewMode === 'grid' ? 'bg-blue-100 text-blue-600' : 'text-gray-400 hover:text-gray-600'}`}
>
<Grid className="w-4 h-4" />
</button>
<button
onClick={() => setViewMode('list')}
className={`p-2 rounded ${viewMode === 'list' ? 'bg-blue-100 text-blue-600' : 'text-gray-400 hover:text-gray-600'}`}
>
<List className="w-4 h-4" />
</button>
</div>
<Button variant="outline">
<Download className="w-4 h-4 mr-2" />
Exportar
</Button>
</div>
</div>
{/* Expanded Filters */}
{showFilters && (
<div className="mt-4 pt-4 border-t grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Estado
</label>
<select
value={filters.status}
onChange={(e) => setFilters(prev => ({ ...prev, status: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
>
{statusOptions.map(option => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Prioridad
</label>
<select
value={filters.priority}
onChange={(e) => setFilters(prev => ({ ...prev, priority: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
>
{priorityOptions.map(option => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Fecha Desde
</label>
<input
type="date"
value={filters.date_from}
onChange={(e) => setFilters(prev => ({ ...prev, date_from: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
</div>
</div>
)}
</Card>
{/* Purchase Orders List */}
<div>
{purchaseOrders.length === 0 ? (
<Card className="text-center py-12">
<Package className="w-16 h-16 text-gray-400 mx-auto mb-4" />
<h3 className="text-lg font-semibold text-gray-900 mb-2">No se encontraron pedidos</h3>
<p className="text-gray-600 mb-4">
{filters.search || filters.status || filters.priority
? 'Intenta ajustar tus filtros de búsqueda'
: 'Comienza creando tu primera orden de compra'
}
</p>
{!(filters.search || filters.status || filters.priority) && (
<Button onClick={() => setShowPurchaseOrderForm(true)}>
<Plus className="w-4 h-4 mr-2" />
Nueva Orden
</Button>
)}
</Card>
) : (
<div className={
viewMode === 'grid'
? 'grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6'
: 'space-y-4'
}>
{purchaseOrders.map(order => (
<PurchaseOrderCard
key={order.id}
order={order}
compact={viewMode === 'list'}
onEdit={(order) => {
setSelectedOrder(order);
setShowPurchaseOrderForm(true);
}}
onViewDetails={(order) => setSelectedOrder(order)}
onApprove={handleApproveOrder}
onSendToSupplier={handleSendToSupplier}
onCancel={handleCancelOrder}
/>
))}
</div>
)}
{/* Pagination */}
{purchaseOrders.length > 0 && pagination.totalPages > 1 && (
<div className="flex items-center justify-between mt-6">
<div className="text-sm text-gray-700">
Mostrando {((pagination.page - 1) * pagination.limit) + 1} a{' '}
{Math.min(pagination.page * pagination.limit, pagination.total)} de{' '}
{pagination.total} pedidos
</div>
<div className="flex items-center space-x-2">
<button
onClick={() => setPage(pagination.page - 1)}
disabled={pagination.page === 1}
className="px-3 py-2 border border-gray-300 rounded-lg disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-50"
>
Anterior
</button>
<span className="px-3 py-2 bg-blue-50 text-blue-600 rounded-lg">
{pagination.page}
</span>
<button
onClick={() => setPage(pagination.page + 1)}
disabled={pagination.page >= pagination.totalPages}
className="px-3 py-2 border border-gray-300 rounded-lg disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-50"
>
Siguiente
</button>
</div>
</div>
)}
</div>
{/* Purchase Order Form Modal */}
{showPurchaseOrderForm && (
<PurchaseOrderForm
order={selectedOrder}
isOpen={showPurchaseOrderForm}
isCreating={isCreating}
onSubmit={selectedOrder ?
async (data) => {
// Handle update logic here if needed
setShowPurchaseOrderForm(false);
setSelectedOrder(null);
} :
handleCreatePurchaseOrder
}
onClose={() => {
setShowPurchaseOrderForm(false);
setSelectedOrder(null);
}}
/>
)}
</div>
);
};
export default PurchaseOrderManagementPage;