Рабочая версия 0.3.1 Добавлены сроки созревания в днях года. При вводе урожая если он в штуках, то можно задать общий вес.

This commit is contained in:
anibilag 2025-09-13 19:28:21 +03:00
parent 5f83aee336
commit fe1878ff3e
8 changed files with 324 additions and 19 deletions

Binary file not shown.

View File

@ -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

View File

@ -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",

View File

@ -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">
Заметки Заметки

View File

@ -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}

View File

@ -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

View File

@ -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,11 +37,7 @@ const PlantStatisticsComponent: React.FC<PlantStatisticsProps> = ({
}, [selectedYear, plant, harvestRecords, maintenanceRecords]); }, [selectedYear, plant, harvestRecords, maintenanceRecords]);
const calculateStatistics = () => { const calculateStatistics = () => {
const yearHarvests = harvestRecords.filter(h => const yearMaintenance = maintenanceRecords.filter(m =>
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 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>
); );

View File

@ -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;