garden_tracker/src/components/HarvestStockManager.tsx

305 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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