Рабочая версия 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,
|
||||
photo_url TEXT,
|
||||
current_photo_url TEXT,
|
||||
estimated_ripening_start INTEGER,
|
||||
estimated_ripening_end INTEGER,
|
||||
ripening_notes TEXT,
|
||||
notes TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
@ -219,6 +222,8 @@ function initializeDatabase() {
|
||||
date DATE NOT NULL,
|
||||
quantity REAL NOT NULL,
|
||||
unit TEXT NOT NULL,
|
||||
weight REAL,
|
||||
weight_unit TEXT,
|
||||
notes TEXT,
|
||||
created_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,
|
||||
photoUrl: row.photo_url,
|
||||
currentPhotoUrl: row.current_photo_url,
|
||||
estimatedRipeningStart: row.estimated_ripening_start,
|
||||
estimatedRipeningEnd: row.estimated_ripening_end,
|
||||
ripeningNotes: row.ripening_notes,
|
||||
notes: row.notes,
|
||||
createdAt: row.created_at,
|
||||
updatedAt: row.updated_at
|
||||
@ -536,6 +544,9 @@ app.post('/api/plants', (req, res) => {
|
||||
currentHeight: row.current_height,
|
||||
photoUrl: row.photo_url,
|
||||
currentPhotoUrl: row.current_photo_url,
|
||||
estimatedRipeningStart: row.estimated_ripening_start,
|
||||
estimatedRipeningEnd: row.estimated_ripening_end,
|
||||
ripeningNotes: row.ripening_notes,
|
||||
notes: row.notes,
|
||||
createdAt: row.created_at,
|
||||
updatedAt: row.updated_at
|
||||
@ -573,6 +584,9 @@ app.put('/api/plants/:id', (req, res) => {
|
||||
currentHeight: row.current_height,
|
||||
photoUrl: row.photo_url,
|
||||
currentPhotoUrl: row.current_photo_url,
|
||||
estimatedRipeningStart: row.estimated_ripening_start,
|
||||
estimatedRipeningEnd: row.estimated_ripening_end,
|
||||
ripeningNotes: row.ripening_notes,
|
||||
notes: row.notes,
|
||||
createdAt: row.created_at,
|
||||
updatedAt: row.updated_at
|
||||
@ -801,6 +815,8 @@ app.get('/api/harvests', (req, res) => {
|
||||
date: row.date,
|
||||
quantity: row.quantity,
|
||||
unit: row.unit,
|
||||
weight: row.weight,
|
||||
weightUnit: row.weight_unit,
|
||||
notes: row.notes,
|
||||
createdAt: row.created_at,
|
||||
updatedAt: row.updated_at
|
||||
@ -812,14 +828,14 @@ app.get('/api/harvests', (req, res) => {
|
||||
|
||||
app.post('/api/harvests', (req, res) => {
|
||||
try {
|
||||
const { plantId, date, quantity, unit, notes } = req.body;
|
||||
const { plantId, date, quantity, unit, weight, weightUnit, notes } = req.body;
|
||||
|
||||
const stmt = db.prepare(`
|
||||
INSERT INTO harvest_records (plant_id, date, quantity, unit, notes)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
INSERT INTO harvest_records (plant_id, date, quantity, unit, weight, weight_unit, notes)
|
||||
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 row = getStmt.get(result.lastInsertRowid);
|
||||
@ -830,6 +846,8 @@ app.post('/api/harvests', (req, res) => {
|
||||
date: row.date,
|
||||
quantity: row.quantity,
|
||||
unit: row.unit,
|
||||
weight: row.weight,
|
||||
weightUnit: row.weight_unit,
|
||||
notes: row.notes,
|
||||
createdAt: row.created_at,
|
||||
updatedAt: row.updated_at
|
||||
@ -1134,6 +1152,9 @@ app.get('/api/plants/:id', (req, res) => {
|
||||
currentHeight: row.current_height,
|
||||
photoUrl: row.photo_url,
|
||||
currentPhotoUrl: row.current_photo_url,
|
||||
estimatedRipeningStart: row.estimated_ripening_start,
|
||||
estimatedRipeningEnd: row.estimated_ripening_end,
|
||||
ripeningNotes: row.ripening_notes,
|
||||
notes: row.notes,
|
||||
createdAt: row.created_at,
|
||||
updatedAt: row.updated_at
|
||||
@ -1170,6 +1191,9 @@ app.get('/api/plants/search', (req, res) => {
|
||||
currentHeight: row.current_height,
|
||||
photoUrl: row.photo_url,
|
||||
currentPhotoUrl: row.current_photo_url,
|
||||
estimatedRipeningStart: row.estimated_ripening_start,
|
||||
estimatedRipeningEnd: row.estimated_ripening_end,
|
||||
ripeningNotes: row.ripening_notes,
|
||||
notes: row.notes,
|
||||
createdAt: row.created_at,
|
||||
updatedAt: row.updated_at
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "vite-react-typescript-starter",
|
||||
"private": true,
|
||||
"version": "0.3.0",
|
||||
"version": "0.3.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --mode development",
|
||||
|
@ -14,6 +14,8 @@ const HarvestForm: React.FC<HarvestFormProps> = ({ plant, onSave, onCancel }) =>
|
||||
date: new Date().toISOString().split('T')[0],
|
||||
quantity: '',
|
||||
unit: 'lbs',
|
||||
weight: '',
|
||||
weightUnit: 'lbs',
|
||||
notes: ''
|
||||
});
|
||||
|
||||
@ -21,7 +23,9 @@ const HarvestForm: React.FC<HarvestFormProps> = ({ plant, onSave, onCancel }) =>
|
||||
e.preventDefault();
|
||||
onSave({
|
||||
...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 }));
|
||||
};
|
||||
|
||||
// Check if current unit is countable (requires weight field)
|
||||
const countableUnits = ['pieces', 'bunches'];
|
||||
const isCountableUnit = countableUnits.includes(formData.unit);
|
||||
|
||||
return (
|
||||
<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">
|
||||
@ -99,6 +107,44 @@ const HarvestForm: React.FC<HarvestFormProps> = ({ plant, onSave, onCancel }) =>
|
||||
</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>
|
||||
<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'],
|
||||
currentHeight: '',
|
||||
photoUrl: '',
|
||||
estimatedRipeningStart: '',
|
||||
estimatedRipeningEnd: '',
|
||||
ripeningNotes: '',
|
||||
notes: ''
|
||||
});
|
||||
const [uploading, setUploading] = useState(false);
|
||||
@ -38,6 +41,9 @@ const PlantForm: React.FC<PlantFormProps> = ({ plant, onSave, onCancel }) => {
|
||||
healthStatus: plant.healthStatus,
|
||||
currentHeight: plant.currentHeight?.toString() || '',
|
||||
photoUrl: plant.photoUrl || '',
|
||||
estimatedRipeningStart: plant.estimatedRipeningStart?.toString() || '',
|
||||
estimatedRipeningEnd: plant.estimatedRipeningEnd?.toString() || '',
|
||||
ripeningNotes: plant.ripeningNotes?.toString() || '',
|
||||
notes: plant.notes
|
||||
});
|
||||
}
|
||||
@ -69,7 +75,10 @@ const PlantForm: React.FC<PlantFormProps> = ({ plant, onSave, onCancel }) => {
|
||||
seedlingAge: parseInt(formData.seedlingAge),
|
||||
seedlingHeight: parseFloat(formData.seedlingHeight),
|
||||
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>
|
||||
<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>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Plant Photo
|
||||
Фотография растения
|
||||
</label>
|
||||
<ImageUpload
|
||||
currentImageUrl={formData.photoUrl}
|
||||
|
@ -324,7 +324,9 @@ const PlantRegistry: React.FC<PlantRegistryProps> = ({
|
||||
<div className="bg-green-50 p-3 rounded-lg">
|
||||
<p className="text-sm font-medium text-green-800">Последний урожай:</p>
|
||||
<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>
|
||||
</div>
|
||||
)}
|
||||
@ -434,13 +436,36 @@ const PlantRegistry: React.FC<PlantRegistryProps> = ({
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
{lastHarvest ? (
|
||||
<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>
|
||||
) : (
|
||||
<span className="text-sm text-gray-500">No harvests</span>
|
||||
)}
|
||||
</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">
|
||||
<div className="flex justify-end space-x-2">
|
||||
<button
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
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 {
|
||||
plant: Plant;
|
||||
@ -18,6 +18,11 @@ const PlantStatisticsComponent: React.FC<PlantStatisticsProps> = ({
|
||||
const [selectedYear, setSelectedYear] = useState(new Date().getFullYear());
|
||||
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(
|
||||
new Set([
|
||||
new Date(plant.plantingDate).getFullYear(),
|
||||
@ -32,10 +37,6 @@ const PlantStatisticsComponent: React.FC<PlantStatisticsProps> = ({
|
||||
}, [selectedYear, plant, harvestRecords, maintenanceRecords]);
|
||||
|
||||
const calculateStatistics = () => {
|
||||
const yearHarvests = harvestRecords.filter(h =>
|
||||
h.plantId === plant.id && new Date(h.date).getFullYear() === selectedYear
|
||||
);
|
||||
|
||||
const yearMaintenance = maintenanceRecords.filter(m =>
|
||||
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;
|
||||
|
||||
return (
|
||||
@ -83,7 +107,7 @@ const PlantStatisticsComponent: React.FC<PlantStatisticsProps> = ({
|
||||
onClick={onClose}
|
||||
className="text-gray-400 hover:text-gray-600 transition-colors"
|
||||
>
|
||||
×
|
||||
<X className="h-6 w-6" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@ -178,6 +202,53 @@ const PlantStatisticsComponent: React.FC<PlantStatisticsProps> = ({
|
||||
</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 */}
|
||||
{harvestRecords.some(h => h.plantId === plant.id && new Date(h.date).getFullYear() === selectedYear) && (
|
||||
<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">
|
||||
<span className="text-sm font-medium text-green-700">
|
||||
{harvest.quantity} {HarvestUnits[harvest.unit || ""]}
|
||||
{harvest?.weight && (
|
||||
<div className="text-xs text-gray-500">
|
||||
({harvest.weight} {harvest.weightUnit})
|
||||
</div>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
{harvest.notes && (
|
||||
@ -247,6 +323,70 @@ const PlantStatisticsComponent: React.FC<PlantStatisticsProps> = ({
|
||||
</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>
|
||||
);
|
||||
|
@ -11,6 +11,9 @@ export interface Plant {
|
||||
currentHeight?: number;
|
||||
photoUrl?: string;
|
||||
currentPhotoUrl?: string;
|
||||
estimatedRipeningStart?: number;
|
||||
estimatedRipeningEnd?: number;
|
||||
ripeningNotes?: string;
|
||||
notes: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
@ -49,6 +52,8 @@ export interface HarvestRecord {
|
||||
date: string;
|
||||
quantity: number;
|
||||
unit: string;
|
||||
weight?: number;
|
||||
weightUnit?: string;
|
||||
notes: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
|
Loading…
x
Reference in New Issue
Block a user