305 lines
12 KiB
TypeScript
305 lines
12 KiB
TypeScript
import { useState, useEffect } from 'react';
|
||
import { Plant } from '../types';
|
||
import { Package, Edit, X, Clock, MapPin, AlertTriangle } from 'lucide-react';
|
||
import { apiService } from '../services/api';
|
||
import { processTypes } from "./HarvestProcessingModal";
|
||
|
||
interface HarvestStockManagerProps {
|
||
plants: Plant[];
|
||
}
|
||
|
||
export default function HarvestStockManager({ plants }: HarvestStockManagerProps) {
|
||
const [stocks, setStocks] = useState<any[]>([]);
|
||
const [loading, setLoading] = useState(true);
|
||
const [editingStock, setEditingStock] = useState<any | null>(null);
|
||
const [filterStatus, setFilterStatus] = useState('all');
|
||
const [filterPlant, setFilterPlant] = useState('all');
|
||
|
||
useEffect(() => {
|
||
loadStocks();
|
||
}, []);
|
||
|
||
const loadStocks = async () => {
|
||
try {
|
||
setLoading(true);
|
||
const data = await apiService.getHarvestStock();
|
||
setStocks(data);
|
||
} catch (error) {
|
||
console.error('Error loading harvest stock:', error);
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
const filteredStocks = stocks.filter(stock => {
|
||
const matchesStatus = filterStatus === 'all' || stock.status === filterStatus;
|
||
const matchesPlant = filterPlant === 'all' || stock.plantId.toString() === filterPlant;
|
||
return matchesStatus && matchesPlant;
|
||
});
|
||
|
||
const handleUpdateStock = async (stockId: number, updates: any) => {
|
||
try {
|
||
const updatedStock = await apiService.updateHarvestStock(stockId, updates);
|
||
setStocks(stocks.map(s => s.id === stockId ? updatedStock : s));
|
||
setEditingStock(null);
|
||
} catch (error) {
|
||
console.error('Error updating stock:', error);
|
||
}
|
||
};
|
||
|
||
const getStatusColor = (status: string) => {
|
||
switch (status) {
|
||
case 'available': return 'bg-green-100 text-green-800';
|
||
case 'consumed': return 'bg-gray-100 text-gray-800';
|
||
case 'expired': return 'bg-red-100 text-red-800';
|
||
case 'spoiled': return 'bg-orange-100 text-orange-800';
|
||
default: return 'bg-gray-100 text-gray-800';
|
||
}
|
||
};
|
||
|
||
const getProcessTypeColor = (type: string) => {
|
||
switch (type) {
|
||
case 'fresh': return 'bg-green-100 text-green-800';
|
||
case 'frozen': return 'bg-blue-100 text-blue-800';
|
||
case 'jam': return 'bg-purple-100 text-purple-800';
|
||
case 'dried': return 'bg-yellow-100 text-yellow-800';
|
||
case 'canned': return 'bg-orange-100 text-orange-800';
|
||
case 'juice': return 'bg-pink-100 text-pink-800';
|
||
case 'sauce': return 'bg-red-100 text-red-800';
|
||
case 'pickled': return 'bg-indigo-100 text-indigo-800';
|
||
default: return 'bg-gray-100 text-gray-800';
|
||
}
|
||
};
|
||
|
||
if (loading) {
|
||
return (
|
||
<div className="flex items-center justify-center py-12">
|
||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-green-600"></div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className="space-y-6">
|
||
<div>
|
||
<h2 className="text-3xl font-bold text-green-800">Управление запасами</h2>
|
||
<p className="text-green-600 mt-1">Отслеживайте свои запасы обработанного урожая</p>
|
||
</div>
|
||
|
||
{/* Filters */}
|
||
<div className="bg-white p-6 rounded-lg shadow-sm border border-green-100">
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-2">Состояние</label>
|
||
<select
|
||
value={filterStatus}
|
||
onChange={(e) => setFilterStatus(e.target.value)}
|
||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-green-500"
|
||
>
|
||
<option value="all">Все состояния</option>
|
||
<option value="available">Доступно</option>
|
||
<option value="consumed">Потреблено</option>
|
||
<option value="expired">Истек срок годности</option>
|
||
<option value="spoiled">Испорчено</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-2">Растение</label>
|
||
<select
|
||
value={filterPlant}
|
||
onChange={(e) => setFilterPlant(e.target.value)}
|
||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-green-500"
|
||
>
|
||
<option value="all">Все растения</option>
|
||
{plants.map(plant => (
|
||
<option key={plant.id} value={plant.id}>
|
||
{plant.variety}
|
||
</option>
|
||
))}
|
||
</select>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Stock Grid */}
|
||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||
{filteredStocks.map((stock) => {
|
||
const plant = plants.find(p => p.id === stock.plantId);
|
||
const isExpiringSoon = stock.expiryDate &&
|
||
new Date(stock.expiryDate) <= new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
|
||
|
||
return (
|
||
<div key={stock.id} className="bg-white rounded-lg shadow-sm border border-green-100 hover:shadow-md transition-shadow">
|
||
<div className="p-6">
|
||
<div className="flex justify-between items-start mb-4">
|
||
<div className="flex items-center space-x-3">
|
||
<div className="bg-blue-100 p-2 rounded-lg">
|
||
<Package className="h-5 w-5 text-blue-600" />
|
||
</div>
|
||
<div>
|
||
<h3 className="text-lg font-semibold text-gray-900">{plant?.variety}</h3>
|
||
<p className="text-sm text-gray-600">
|
||
Собрано: {new Date(stock.harvestDate).toLocaleDateString()}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
<button
|
||
onClick={() => setEditingStock(stock)}
|
||
className="p-1 text-blue-600 hover:text-blue-700 transition-colors"
|
||
>
|
||
<Edit className="h-4 w-4" />
|
||
</button>
|
||
</div>
|
||
|
||
<div className="space-y-3">
|
||
<div className="flex items-center justify-between">
|
||
<span className="text-sm text-gray-600">Тип:</span>
|
||
<span className={`px-2 py-1 rounded-full text-xs font-medium ${getProcessTypeColor(stock.processType)}`}>
|
||
{processTypes.find(t => t.value === stock.processType)?.label}
|
||
</span>
|
||
</div>
|
||
|
||
<div className="flex items-center justify-between">
|
||
<span className="text-sm text-gray-600">Количество:</span>
|
||
<span className="text-sm font-medium text-gray-900">
|
||
{stock.currentQuantity} {stock.unit}
|
||
</span>
|
||
</div>
|
||
|
||
<div className="flex items-center justify-between">
|
||
<span className="text-sm text-gray-600">Состояние:</span>
|
||
<span className={`px-2 py-1 rounded-full text-xs font-medium ${getStatusColor(stock.status)}`}>
|
||
{stock.status}
|
||
</span>
|
||
</div>
|
||
|
||
{stock.storageLocation && (
|
||
<div className="flex items-center space-x-2">
|
||
<MapPin className="h-4 w-4 text-gray-400" />
|
||
<span className="text-sm text-gray-600">{stock.storageLocation}</span>
|
||
</div>
|
||
)}
|
||
|
||
{stock.expiryDate && (
|
||
<div className={`flex items-center space-x-2 ${isExpiringSoon ? 'text-red-600' : 'text-gray-600'}`}>
|
||
<Clock className="h-4 w-4" />
|
||
<span className="text-sm">
|
||
Срок годности: {new Date(stock.expiryDate).toLocaleDateString()}
|
||
</span>
|
||
{isExpiringSoon && <AlertTriangle className="h-4 w-4" />}
|
||
</div>
|
||
)}
|
||
|
||
<div className="text-sm text-gray-500">
|
||
Обновлено: {new Date(stock.lastUpdated).toLocaleDateString()}
|
||
</div>
|
||
|
||
{stock.notes && (
|
||
<div className="border-t pt-3">
|
||
<p className="text-sm text-gray-700">{stock.notes}</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
|
||
{filteredStocks.length === 0 && (
|
||
<div className="text-center py-12">
|
||
<p className="text-gray-500 text-lg">No harvest stock found.</p>
|
||
<p className="text-sm text-gray-400 mt-2">Process some harvests to see stock here.</p>
|
||
</div>
|
||
)}
|
||
|
||
{/* Edit Stock Modal */}
|
||
{editingStock && (
|
||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
|
||
<div className="bg-white rounded-lg shadow-xl w-full max-w-md">
|
||
<div className="flex items-center justify-between p-6 border-b">
|
||
<h2 className="text-xl font-semibold text-gray-900">Изменить запасы</h2>
|
||
<button
|
||
onClick={() => setEditingStock(null)}
|
||
className="text-gray-400 hover:text-gray-600 transition-colors"
|
||
>
|
||
<X className="w-6 h-6" />
|
||
</button>
|
||
</div>
|
||
|
||
<form onSubmit={(e) => {
|
||
e.preventDefault();
|
||
const formData = new FormData(e.target as HTMLFormElement);
|
||
handleUpdateStock(editingStock.id, {
|
||
currentQuantity: parseFloat(formData.get('currentQuantity') as string),
|
||
status: formData.get('status'),
|
||
notes: formData.get('notes') || undefined
|
||
});
|
||
}} className="p-6 space-y-4">
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||
Текущее количество *
|
||
</label>
|
||
<input
|
||
type="number"
|
||
name="currentQuantity"
|
||
defaultValue={editingStock.currentQuantity}
|
||
step="0.1"
|
||
min="0"
|
||
required
|
||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-green-500"
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||
Состояние *
|
||
</label>
|
||
<select
|
||
name="status"
|
||
defaultValue={editingStock.status}
|
||
required
|
||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-green-500"
|
||
>
|
||
<option value="available">Доступно</option>
|
||
<option value="consumed">Потреблено</option>
|
||
<option value="expired">Истек срок годности</option>
|
||
<option value="spoiled">Испорчено</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||
Заметки
|
||
</label>
|
||
<textarea
|
||
name="notes"
|
||
defaultValue={editingStock.notes || ''}
|
||
rows={3}
|
||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-green-500 resize-none"
|
||
/>
|
||
</div>
|
||
|
||
<div className="flex justify-end space-x-3 pt-4 border-t">
|
||
<button
|
||
type="button"
|
||
onClick={() => setEditingStock(null)}
|
||
className="px-4 py-2 text-gray-700 bg-gray-100 rounded-md hover:bg-gray-200 transition-colors"
|
||
>
|
||
Отмена
|
||
</button>
|
||
<button
|
||
type="submit"
|
||
className="px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 transition-colors"
|
||
>
|
||
Изменить
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
} |