Рабочая версия 0.3.1 Добавлены сроки созревания в днях года. При вводе урожая если он в штуках, то можно задать общий вес.
This commit is contained in:
parent
5f83aee336
commit
fe1878ff3e
Binary file not shown.
@ -93,6 +93,9 @@ function initializeDatabase() {
|
|||||||
current_height REAL,
|
current_height REAL,
|
||||||
photo_url TEXT,
|
photo_url TEXT,
|
||||||
current_photo_url TEXT,
|
current_photo_url TEXT,
|
||||||
|
estimated_ripening_start INTEGER,
|
||||||
|
estimated_ripening_end INTEGER,
|
||||||
|
ripening_notes TEXT,
|
||||||
notes TEXT,
|
notes TEXT,
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
@ -219,6 +222,8 @@ function initializeDatabase() {
|
|||||||
date DATE NOT NULL,
|
date DATE NOT NULL,
|
||||||
quantity REAL NOT NULL,
|
quantity REAL NOT NULL,
|
||||||
unit TEXT NOT NULL,
|
unit TEXT NOT NULL,
|
||||||
|
weight REAL,
|
||||||
|
weight_unit TEXT,
|
||||||
notes TEXT,
|
notes TEXT,
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
@ -500,6 +505,9 @@ app.get('/api/plants', (req, res) => {
|
|||||||
currentHeight: row.current_height,
|
currentHeight: row.current_height,
|
||||||
photoUrl: row.photo_url,
|
photoUrl: row.photo_url,
|
||||||
currentPhotoUrl: row.current_photo_url,
|
currentPhotoUrl: row.current_photo_url,
|
||||||
|
estimatedRipeningStart: row.estimated_ripening_start,
|
||||||
|
estimatedRipeningEnd: row.estimated_ripening_end,
|
||||||
|
ripeningNotes: row.ripening_notes,
|
||||||
notes: row.notes,
|
notes: row.notes,
|
||||||
createdAt: row.created_at,
|
createdAt: row.created_at,
|
||||||
updatedAt: row.updated_at
|
updatedAt: row.updated_at
|
||||||
@ -536,6 +544,9 @@ app.post('/api/plants', (req, res) => {
|
|||||||
currentHeight: row.current_height,
|
currentHeight: row.current_height,
|
||||||
photoUrl: row.photo_url,
|
photoUrl: row.photo_url,
|
||||||
currentPhotoUrl: row.current_photo_url,
|
currentPhotoUrl: row.current_photo_url,
|
||||||
|
estimatedRipeningStart: row.estimated_ripening_start,
|
||||||
|
estimatedRipeningEnd: row.estimated_ripening_end,
|
||||||
|
ripeningNotes: row.ripening_notes,
|
||||||
notes: row.notes,
|
notes: row.notes,
|
||||||
createdAt: row.created_at,
|
createdAt: row.created_at,
|
||||||
updatedAt: row.updated_at
|
updatedAt: row.updated_at
|
||||||
@ -573,6 +584,9 @@ app.put('/api/plants/:id', (req, res) => {
|
|||||||
currentHeight: row.current_height,
|
currentHeight: row.current_height,
|
||||||
photoUrl: row.photo_url,
|
photoUrl: row.photo_url,
|
||||||
currentPhotoUrl: row.current_photo_url,
|
currentPhotoUrl: row.current_photo_url,
|
||||||
|
estimatedRipeningStart: row.estimated_ripening_start,
|
||||||
|
estimatedRipeningEnd: row.estimated_ripening_end,
|
||||||
|
ripeningNotes: row.ripening_notes,
|
||||||
notes: row.notes,
|
notes: row.notes,
|
||||||
createdAt: row.created_at,
|
createdAt: row.created_at,
|
||||||
updatedAt: row.updated_at
|
updatedAt: row.updated_at
|
||||||
@ -801,6 +815,8 @@ app.get('/api/harvests', (req, res) => {
|
|||||||
date: row.date,
|
date: row.date,
|
||||||
quantity: row.quantity,
|
quantity: row.quantity,
|
||||||
unit: row.unit,
|
unit: row.unit,
|
||||||
|
weight: row.weight,
|
||||||
|
weightUnit: row.weight_unit,
|
||||||
notes: row.notes,
|
notes: row.notes,
|
||||||
createdAt: row.created_at,
|
createdAt: row.created_at,
|
||||||
updatedAt: row.updated_at
|
updatedAt: row.updated_at
|
||||||
@ -812,14 +828,14 @@ app.get('/api/harvests', (req, res) => {
|
|||||||
|
|
||||||
app.post('/api/harvests', (req, res) => {
|
app.post('/api/harvests', (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { plantId, date, quantity, unit, notes } = req.body;
|
const { plantId, date, quantity, unit, weight, weightUnit, notes } = req.body;
|
||||||
|
|
||||||
const stmt = db.prepare(`
|
const stmt = db.prepare(`
|
||||||
INSERT INTO harvest_records (plant_id, date, quantity, unit, notes)
|
INSERT INTO harvest_records (plant_id, date, quantity, unit, weight, weight_unit, notes)
|
||||||
VALUES (?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const result = stmt.run(plantId, date, quantity, unit, notes);
|
const result = stmt.run(plantId, date, quantity, unit, weight || null, weightUnit || null, notes);
|
||||||
|
|
||||||
const getStmt = db.prepare('SELECT * FROM harvest_records WHERE id = ? ORDER BY date DESC');
|
const getStmt = db.prepare('SELECT * FROM harvest_records WHERE id = ? ORDER BY date DESC');
|
||||||
const row = getStmt.get(result.lastInsertRowid);
|
const row = getStmt.get(result.lastInsertRowid);
|
||||||
@ -830,6 +846,8 @@ app.post('/api/harvests', (req, res) => {
|
|||||||
date: row.date,
|
date: row.date,
|
||||||
quantity: row.quantity,
|
quantity: row.quantity,
|
||||||
unit: row.unit,
|
unit: row.unit,
|
||||||
|
weight: row.weight,
|
||||||
|
weightUnit: row.weight_unit,
|
||||||
notes: row.notes,
|
notes: row.notes,
|
||||||
createdAt: row.created_at,
|
createdAt: row.created_at,
|
||||||
updatedAt: row.updated_at
|
updatedAt: row.updated_at
|
||||||
@ -1134,6 +1152,9 @@ app.get('/api/plants/:id', (req, res) => {
|
|||||||
currentHeight: row.current_height,
|
currentHeight: row.current_height,
|
||||||
photoUrl: row.photo_url,
|
photoUrl: row.photo_url,
|
||||||
currentPhotoUrl: row.current_photo_url,
|
currentPhotoUrl: row.current_photo_url,
|
||||||
|
estimatedRipeningStart: row.estimated_ripening_start,
|
||||||
|
estimatedRipeningEnd: row.estimated_ripening_end,
|
||||||
|
ripeningNotes: row.ripening_notes,
|
||||||
notes: row.notes,
|
notes: row.notes,
|
||||||
createdAt: row.created_at,
|
createdAt: row.created_at,
|
||||||
updatedAt: row.updated_at
|
updatedAt: row.updated_at
|
||||||
@ -1170,6 +1191,9 @@ app.get('/api/plants/search', (req, res) => {
|
|||||||
currentHeight: row.current_height,
|
currentHeight: row.current_height,
|
||||||
photoUrl: row.photo_url,
|
photoUrl: row.photo_url,
|
||||||
currentPhotoUrl: row.current_photo_url,
|
currentPhotoUrl: row.current_photo_url,
|
||||||
|
estimatedRipeningStart: row.estimated_ripening_start,
|
||||||
|
estimatedRipeningEnd: row.estimated_ripening_end,
|
||||||
|
ripeningNotes: row.ripening_notes,
|
||||||
notes: row.notes,
|
notes: row.notes,
|
||||||
createdAt: row.created_at,
|
createdAt: row.created_at,
|
||||||
updatedAt: row.updated_at
|
updatedAt: row.updated_at
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "vite-react-typescript-starter",
|
"name": "vite-react-typescript-starter",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.3.0",
|
"version": "0.3.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite --mode development",
|
"dev": "vite --mode development",
|
||||||
|
@ -14,6 +14,8 @@ const HarvestForm: React.FC<HarvestFormProps> = ({ plant, onSave, onCancel }) =>
|
|||||||
date: new Date().toISOString().split('T')[0],
|
date: new Date().toISOString().split('T')[0],
|
||||||
quantity: '',
|
quantity: '',
|
||||||
unit: 'lbs',
|
unit: 'lbs',
|
||||||
|
weight: '',
|
||||||
|
weightUnit: 'lbs',
|
||||||
notes: ''
|
notes: ''
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -21,7 +23,9 @@ const HarvestForm: React.FC<HarvestFormProps> = ({ plant, onSave, onCancel }) =>
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
onSave({
|
onSave({
|
||||||
...formData,
|
...formData,
|
||||||
quantity: parseFloat(formData.quantity)
|
quantity: parseFloat(formData.quantity),
|
||||||
|
weight: formData.weight ? parseFloat(formData.weight) : undefined,
|
||||||
|
weightUnit: formData.weight ? formData.weightUnit : undefined
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -30,6 +34,10 @@ const HarvestForm: React.FC<HarvestFormProps> = ({ plant, onSave, onCancel }) =>
|
|||||||
setFormData(prev => ({ ...prev, [name]: value }));
|
setFormData(prev => ({ ...prev, [name]: value }));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Check if current unit is countable (requires weight field)
|
||||||
|
const countableUnits = ['pieces', 'bunches'];
|
||||||
|
const isCountableUnit = countableUnits.includes(formData.unit);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
||||||
<div className="bg-white rounded-lg shadow-xl max-w-md w-full">
|
<div className="bg-white rounded-lg shadow-xl max-w-md w-full">
|
||||||
@ -99,6 +107,44 @@ const HarvestForm: React.FC<HarvestFormProps> = ({ plant, onSave, onCancel }) =>
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Weight fields - only show for countable units */}
|
||||||
|
{isCountableUnit && (
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label htmlFor="weight" className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Общий вес (не обязательно)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
id="weight"
|
||||||
|
name="weight"
|
||||||
|
value={formData.weight}
|
||||||
|
onChange={handleChange}
|
||||||
|
step="0.1"
|
||||||
|
min="0"
|
||||||
|
placeholder="например, 2.5"
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-transparent"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor="weightUnit" className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Единицы веса
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
id="weightUnit"
|
||||||
|
name="weightUnit"
|
||||||
|
value={formData.weightUnit}
|
||||||
|
onChange={handleChange}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-transparent"
|
||||||
|
>
|
||||||
|
<option value="kg">Килограмм (kg)</option>
|
||||||
|
<option value="g">Грамм (g)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="notes" className="block text-sm font-medium text-gray-700 mb-1">
|
<label htmlFor="notes" className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
Заметки
|
Заметки
|
||||||
|
@ -21,6 +21,9 @@ const PlantForm: React.FC<PlantFormProps> = ({ plant, onSave, onCancel }) => {
|
|||||||
healthStatus: 'good' as Plant['healthStatus'],
|
healthStatus: 'good' as Plant['healthStatus'],
|
||||||
currentHeight: '',
|
currentHeight: '',
|
||||||
photoUrl: '',
|
photoUrl: '',
|
||||||
|
estimatedRipeningStart: '',
|
||||||
|
estimatedRipeningEnd: '',
|
||||||
|
ripeningNotes: '',
|
||||||
notes: ''
|
notes: ''
|
||||||
});
|
});
|
||||||
const [uploading, setUploading] = useState(false);
|
const [uploading, setUploading] = useState(false);
|
||||||
@ -38,6 +41,9 @@ const PlantForm: React.FC<PlantFormProps> = ({ plant, onSave, onCancel }) => {
|
|||||||
healthStatus: plant.healthStatus,
|
healthStatus: plant.healthStatus,
|
||||||
currentHeight: plant.currentHeight?.toString() || '',
|
currentHeight: plant.currentHeight?.toString() || '',
|
||||||
photoUrl: plant.photoUrl || '',
|
photoUrl: plant.photoUrl || '',
|
||||||
|
estimatedRipeningStart: plant.estimatedRipeningStart?.toString() || '',
|
||||||
|
estimatedRipeningEnd: plant.estimatedRipeningEnd?.toString() || '',
|
||||||
|
ripeningNotes: plant.ripeningNotes?.toString() || '',
|
||||||
notes: plant.notes
|
notes: plant.notes
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -69,7 +75,10 @@ const PlantForm: React.FC<PlantFormProps> = ({ plant, onSave, onCancel }) => {
|
|||||||
seedlingAge: parseInt(formData.seedlingAge),
|
seedlingAge: parseInt(formData.seedlingAge),
|
||||||
seedlingHeight: parseFloat(formData.seedlingHeight),
|
seedlingHeight: parseFloat(formData.seedlingHeight),
|
||||||
currentHeight: formData.currentHeight ? parseFloat(formData.currentHeight) : undefined,
|
currentHeight: formData.currentHeight ? parseFloat(formData.currentHeight) : undefined,
|
||||||
photoUrl: finalPhotoUrl || undefined
|
photoUrl: finalPhotoUrl || undefined,
|
||||||
|
estimatedRipeningStart: parseInt(formData.estimatedRipeningStart?.toString() || "0"),
|
||||||
|
estimatedRipeningEnd: parseInt(formData.estimatedRipeningEnd?.toString() || "0"),
|
||||||
|
ripeningNotes: formData.ripeningNotes?.toString() || ''
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -244,9 +253,65 @@ const PlantForm: React.FC<PlantFormProps> = ({ plant, onSave, onCancel }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h4 className="text-md font-medium text-gray-900 mb-3">Информация о созревании</h4>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
||||||
|
<div>
|
||||||
|
<label htmlFor="estimatedRipeningStart" className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Начало созревания (день года)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
id="estimatedRipeningStart"
|
||||||
|
name="estimatedRipeningStart"
|
||||||
|
value={formData.estimatedRipeningStart}
|
||||||
|
onChange={handleChange}
|
||||||
|
min="1"
|
||||||
|
max="365"
|
||||||
|
placeholder="например, 150 (30 мая)"
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-transparent"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-gray-500 mt-1">День 1 = 1 января, День 365 = 31 декабря</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor="estimatedRipeningEnd" className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Завершение созревания (день года)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
id="estimatedRipeningEnd"
|
||||||
|
name="estimatedRipeningEnd"
|
||||||
|
value={formData.estimatedRipeningEnd}
|
||||||
|
onChange={handleChange}
|
||||||
|
min="1"
|
||||||
|
max="365"
|
||||||
|
placeholder="например, 240 (28 августа)"
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-transparent"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-gray-500 mt-1">Должен быть больше, чем день начала созревания</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor="ripeningNotes" className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Заметки о созревании
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
id="ripeningNotes"
|
||||||
|
name="ripeningNotes"
|
||||||
|
value={formData.ripeningNotes}
|
||||||
|
onChange={handleChange}
|
||||||
|
placeholder="Заметки о способах созревания, признаках спелости, сроках сбора урожая..."
|
||||||
|
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 focus:border-transparent resize-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
Plant Photo
|
Фотография растения
|
||||||
</label>
|
</label>
|
||||||
<ImageUpload
|
<ImageUpload
|
||||||
currentImageUrl={formData.photoUrl}
|
currentImageUrl={formData.photoUrl}
|
||||||
|
@ -324,7 +324,9 @@ const PlantRegistry: React.FC<PlantRegistryProps> = ({
|
|||||||
<div className="bg-green-50 p-3 rounded-lg">
|
<div className="bg-green-50 p-3 rounded-lg">
|
||||||
<p className="text-sm font-medium text-green-800">Последний урожай:</p>
|
<p className="text-sm font-medium text-green-800">Последний урожай:</p>
|
||||||
<p className="text-sm text-green-600">
|
<p className="text-sm text-green-600">
|
||||||
{lastHarvest.quantity} {HarvestUnits[lastHarvest.unit]} {new Date(lastHarvest.date).toLocaleDateString()}
|
{lastHarvest.quantity} {HarvestUnits[lastHarvest.unit]}
|
||||||
|
{lastHarvest.weight && ` (${lastHarvest.weight} ${lastHarvest.weightUnit})`}
|
||||||
|
{' '} {new Date(lastHarvest.date).toLocaleDateString()}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -434,13 +436,36 @@ const PlantRegistry: React.FC<PlantRegistryProps> = ({
|
|||||||
<td className="px-6 py-4 whitespace-nowrap">
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
{lastHarvest ? (
|
{lastHarvest ? (
|
||||||
<div className="text-sm text-gray-900">
|
<div className="text-sm text-gray-900">
|
||||||
<div>{lastHarvest.quantity} {lastHarvest.unit}</div>
|
<div>
|
||||||
|
{lastHarvest.quantity} {lastHarvest.unit}
|
||||||
|
{lastHarvest.weight && (
|
||||||
|
<div className="text-gray-500">({lastHarvest.weight} {lastHarvest.weightUnit})</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<div className="text-gray-500">{new Date(lastHarvest.date).toLocaleDateString()}</div>
|
<div className="text-gray-500">{new Date(lastHarvest.date).toLocaleDateString()}</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-sm text-gray-500">No harvests</span>
|
<span className="text-sm text-gray-500">No harvests</span>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
|
{(plant.estimatedRipeningStart || plant.estimatedRipeningEnd) ? (
|
||||||
|
<div className="text-sm text-gray-900">
|
||||||
|
<div>
|
||||||
|
{plant.estimatedRipeningStart && `Day ${plant.estimatedRipeningStart}`}
|
||||||
|
{plant.estimatedRipeningStart && plant.estimatedRipeningEnd && ' - '}
|
||||||
|
{plant.estimatedRipeningEnd && `Day ${plant.estimatedRipeningEnd}`}
|
||||||
|
</div>
|
||||||
|
{plant.ripeningNotes && (
|
||||||
|
<div className="text-xs text-gray-500 truncate max-w-32" title={plant.ripeningNotes}>
|
||||||
|
{plant.ripeningNotes}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<span className="text-sm text-gray-500">Not set</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||||
<div className="flex justify-end space-x-2">
|
<div className="flex justify-end space-x-2">
|
||||||
<button
|
<button
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import {Plant, PlantStatistics, HarvestRecord, MaintenanceRecord, HarvestUnits} from '../types';
|
import {Plant, PlantStatistics, HarvestRecord, MaintenanceRecord, HarvestUnits} from '../types';
|
||||||
import { BarChart3, TrendingUp, Calendar, Award } from 'lucide-react';
|
import { BarChart3, TrendingUp, Calendar, Award, X } from 'lucide-react';
|
||||||
|
|
||||||
interface PlantStatisticsProps {
|
interface PlantStatisticsProps {
|
||||||
plant: Plant;
|
plant: Plant;
|
||||||
@ -18,6 +18,11 @@ const PlantStatisticsComponent: React.FC<PlantStatisticsProps> = ({
|
|||||||
const [selectedYear, setSelectedYear] = useState(new Date().getFullYear());
|
const [selectedYear, setSelectedYear] = useState(new Date().getFullYear());
|
||||||
const [statistics, setStatistics] = useState<PlantStatistics | null>(null);
|
const [statistics, setStatistics] = useState<PlantStatistics | null>(null);
|
||||||
|
|
||||||
|
// Get harvests for the selected year
|
||||||
|
const yearHarvests = harvestRecords.filter(h =>
|
||||||
|
h.plantId === plant.id && new Date(h.date).getFullYear() === selectedYear
|
||||||
|
);
|
||||||
|
|
||||||
const availableYears = Array.from(
|
const availableYears = Array.from(
|
||||||
new Set([
|
new Set([
|
||||||
new Date(plant.plantingDate).getFullYear(),
|
new Date(plant.plantingDate).getFullYear(),
|
||||||
@ -32,10 +37,6 @@ const PlantStatisticsComponent: React.FC<PlantStatisticsProps> = ({
|
|||||||
}, [selectedYear, plant, harvestRecords, maintenanceRecords]);
|
}, [selectedYear, plant, harvestRecords, maintenanceRecords]);
|
||||||
|
|
||||||
const calculateStatistics = () => {
|
const calculateStatistics = () => {
|
||||||
const yearHarvests = harvestRecords.filter(h =>
|
|
||||||
h.plantId === plant.id && new Date(h.date).getFullYear() === selectedYear
|
|
||||||
);
|
|
||||||
|
|
||||||
const yearMaintenance = maintenanceRecords.filter(m =>
|
const yearMaintenance = maintenanceRecords.filter(m =>
|
||||||
m.plantId === plant.id && new Date(m.date).getFullYear() === selectedYear
|
m.plantId === plant.id && new Date(m.date).getFullYear() === selectedYear
|
||||||
);
|
);
|
||||||
@ -67,6 +68,29 @@ const PlantStatisticsComponent: React.FC<PlantStatisticsProps> = ({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Prepare chart data
|
||||||
|
const prepareChartData = () => {
|
||||||
|
if (yearHarvests.length === 0) return [];
|
||||||
|
|
||||||
|
// Group harvests by date and sum quantities
|
||||||
|
const harvestsByDate = yearHarvests.reduce((acc, harvest) => {
|
||||||
|
const date = harvest.date;
|
||||||
|
if (!acc[date]) {
|
||||||
|
acc[date] = { date, quantity: 0, unit: harvest.unit };
|
||||||
|
}
|
||||||
|
acc[date].quantity += harvest.quantity;
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<string, { date: string; quantity: number; unit: string }>);
|
||||||
|
|
||||||
|
// Convert to array and sort by date
|
||||||
|
return Object.values(harvestsByDate).sort((a, b) =>
|
||||||
|
new Date(a.date).getTime() - new Date(b.date).getTime()
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const chartData = prepareChartData();
|
||||||
|
const maxQuantity = Math.max(...chartData.map(d => d.quantity), 0);
|
||||||
|
|
||||||
if (!statistics) return null;
|
if (!statistics) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -83,7 +107,7 @@ const PlantStatisticsComponent: React.FC<PlantStatisticsProps> = ({
|
|||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="text-gray-400 hover:text-gray-600 transition-colors"
|
className="text-gray-400 hover:text-gray-600 transition-colors"
|
||||||
>
|
>
|
||||||
×
|
<X className="h-6 w-6" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -178,6 +202,53 @@ const PlantStatisticsComponent: React.FC<PlantStatisticsProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Ripening Information */}
|
||||||
|
{(plant.estimatedRipeningStart || plant.estimatedRipeningEnd || plant.ripeningNotes) && (
|
||||||
|
<div className="bg-yellow-50 p-6 rounded-lg mb-6">
|
||||||
|
<h4 className="text-lg font-semibold text-gray-900 mb-4">Информация о созревании</h4>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{(plant.estimatedRipeningStart || plant.estimatedRipeningEnd) && (
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-yellow-800 mb-2">Estimated Ripening Period:</p>
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
{plant.estimatedRipeningStart && (
|
||||||
|
<div className="text-center">
|
||||||
|
<p className="text-lg font-bold text-yellow-700">Day {plant.estimatedRipeningStart}</p>
|
||||||
|
<p className="text-xs text-yellow-600">
|
||||||
|
{new Date(selectedYear, 0, plant.estimatedRipeningStart).toLocaleDateString('en-US', {
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric'
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{plant.estimatedRipeningStart && plant.estimatedRipeningEnd && (
|
||||||
|
<span className="text-yellow-600">→</span>
|
||||||
|
)}
|
||||||
|
{plant.estimatedRipeningEnd && (
|
||||||
|
<div className="text-center">
|
||||||
|
<p className="text-lg font-bold text-yellow-700">Day {plant.estimatedRipeningEnd}</p>
|
||||||
|
<p className="text-xs text-yellow-600">
|
||||||
|
{new Date(selectedYear, 0, plant.estimatedRipeningEnd).toLocaleDateString('en-US', {
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric'
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{plant.ripeningNotes && (
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-yellow-800 mb-1">Notes:</p>
|
||||||
|
<p className="text-sm text-yellow-700">{plant.ripeningNotes}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Harvest Timeline */}
|
{/* Harvest Timeline */}
|
||||||
{harvestRecords.some(h => h.plantId === plant.id && new Date(h.date).getFullYear() === selectedYear) && (
|
{harvestRecords.some(h => h.plantId === plant.id && new Date(h.date).getFullYear() === selectedYear) && (
|
||||||
<div className="bg-green-50 p-6 rounded-lg">
|
<div className="bg-green-50 p-6 rounded-lg">
|
||||||
@ -232,6 +303,11 @@ const PlantStatisticsComponent: React.FC<PlantStatisticsProps> = ({
|
|||||||
<div className="flex justify-between items-start">
|
<div className="flex justify-between items-start">
|
||||||
<span className="text-sm font-medium text-green-700">
|
<span className="text-sm font-medium text-green-700">
|
||||||
{harvest.quantity} {HarvestUnits[harvest.unit || ""]}
|
{harvest.quantity} {HarvestUnits[harvest.unit || ""]}
|
||||||
|
{harvest?.weight && (
|
||||||
|
<div className="text-xs text-gray-500">
|
||||||
|
({harvest.weight} {harvest.weightUnit})
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{harvest.notes && (
|
{harvest.notes && (
|
||||||
@ -247,6 +323,70 @@ const PlantStatisticsComponent: React.FC<PlantStatisticsProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{/* Harvest Chart */}
|
||||||
|
{chartData.length > 0 && (
|
||||||
|
<div className="bg-gray-50 p-6 rounded-lg">
|
||||||
|
<h4 className="text-lg font-semibold text-gray-900 mb-4">Диаграмма урожая</h4>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between text-sm text-gray-600">
|
||||||
|
<span>Размер урожая ({chartData[0]?.unit})</span>
|
||||||
|
<span>Максимально: {maxQuantity.toFixed(1)} {chartData[0]?.unit}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative">
|
||||||
|
{/* Y-axis labels */}
|
||||||
|
<div className="absolute left-0 top-0 bottom-0 w-12 flex flex-col justify-between text-xs text-gray-500">
|
||||||
|
<span>{maxQuantity.toFixed(1)}</span>
|
||||||
|
<span>{(maxQuantity * 0.75).toFixed(1)}</span>
|
||||||
|
<span>{(maxQuantity * 0.5).toFixed(1)}</span>
|
||||||
|
<span>{(maxQuantity * 0.25).toFixed(1)}</span>
|
||||||
|
<span>0</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Chart area */}
|
||||||
|
<div className="ml-14 border-l border-b border-gray-300">
|
||||||
|
<div className="flex items-end space-x-1 p-4 min-h-[200px]">
|
||||||
|
{chartData.map((data, index) => {
|
||||||
|
const height = maxQuantity > 0 ? (data.quantity / maxQuantity) * 160 : 0;
|
||||||
|
return (
|
||||||
|
<div key={index} className="flex flex-col items-center group relative">
|
||||||
|
{/* Bar */}
|
||||||
|
<div
|
||||||
|
className="bg-green-500 hover:bg-green-600 transition-colors rounded-t min-w-[20px] cursor-pointer"
|
||||||
|
style={{
|
||||||
|
height: `${height}px`,
|
||||||
|
width: `${Math.max(20, 300 / chartData.length)}px`
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Tooltip */}
|
||||||
|
<div className="absolute bottom-full mb-2 hidden group-hover:block bg-gray-800 text-white text-xs rounded px-2 py-1 whitespace-nowrap z-10">
|
||||||
|
<div>{new Date(data.date).toLocaleDateString()}</div>
|
||||||
|
<div>{data.quantity.toFixed(1)} {data.unit}</div>
|
||||||
|
<div className="absolute top-full left-1/2 transform -translate-x-1/2 border-4 border-transparent border-t-gray-800"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* X-axis label */}
|
||||||
|
<div className="text-xs text-gray-500 mt-2 transform -rotate-45 origin-top-left whitespace-nowrap">
|
||||||
|
{new Date(data.date).toLocaleDateString('en-US', {
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric'
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* X-axis title */}
|
||||||
|
<div className="text-center text-sm text-gray-600 mt-4">
|
||||||
|
Даты сбора урожая
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -11,6 +11,9 @@ export interface Plant {
|
|||||||
currentHeight?: number;
|
currentHeight?: number;
|
||||||
photoUrl?: string;
|
photoUrl?: string;
|
||||||
currentPhotoUrl?: string;
|
currentPhotoUrl?: string;
|
||||||
|
estimatedRipeningStart?: number;
|
||||||
|
estimatedRipeningEnd?: number;
|
||||||
|
ripeningNotes?: string;
|
||||||
notes: string;
|
notes: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
@ -49,6 +52,8 @@ export interface HarvestRecord {
|
|||||||
date: string;
|
date: string;
|
||||||
quantity: number;
|
quantity: number;
|
||||||
unit: string;
|
unit: string;
|
||||||
|
weight?: number;
|
||||||
|
weightUnit?: string;
|
||||||
notes: string;
|
notes: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user