Рабочая версия 0.1.0 Добавлен журнал наблюдений. Почти полная руссификация.

This commit is contained in:
anibilag 2025-07-30 23:30:32 +03:00
parent 9d154596ff
commit fee1e116d2
21 changed files with 1434 additions and 1681 deletions

Binary file not shown.

1062
backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -11,7 +11,7 @@
}, },
"dependencies": { "dependencies": {
"express": "^4.18.2", "express": "^4.18.2",
"sqlite3": "^5.1.6", "better-sqlite3": "^8.7.0",
"cors": "^2.8.5", "cors": "^2.8.5",
"body-parser": "^1.20.2", "body-parser": "^1.20.2",
"dotenv": "^16.3.1" "dotenv": "^16.3.1"

View File

@ -0,0 +1,110 @@
const fs = require('fs');
const path = require('path');
const Database = require('better-sqlite3');
// Определяем путь
const DB_DIR = path.join(__dirname, '..', 'database');
const DB_PATH = path.join(DB_DIR, 'gardentrack.db');
// Создаём папку, если не существует
if (!fs.existsSync(DB_DIR)) {
fs.mkdirSync(DB_DIR, { recursive: true });
console.log('📁 Папка database создана');
}
// Подключаемся к базе
const db = new Database(DB_PATH);
console.log('✅ Подключено к базе:', DB_PATH);
// Создаём таблицы
db.exec(`
CREATE TABLE IF NOT EXISTS plants (
id INTEGER PRIMARY KEY AUTOINCREMENT,
purchase_location TEXT,
seedling_age INTEGER,
type TEXT NOT NULL,
variety TEXT NOT NULL,
seedling_height REAL,
planting_date DATE NOT NULL,
current_height REAL,
health_status TEXT DEFAULT 'good' CHECK(health_status IN ('good', 'needs-attention', 'dead')),
photo_url TEXT,
current_photo_url TEXT,
notes TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS plant_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
plant_id INTEGER NOT NULL,
year INTEGER NOT NULL,
blooming_date DATE,
fruiting_date DATE,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (plant_id) REFERENCES plants (id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS harvest_records (
id INTEGER PRIMARY KEY AUTOINCREMENT,
plant_id INTEGER NOT NULL,
date DATE NOT NULL,
quantity REAL NOT NULL,
unit TEXT NOT NULL,
notes TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (plant_id) REFERENCES plants (id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS maintenance_records (
id INTEGER PRIMARY KEY AUTOINCREMENT,
plant_id INTEGER NOT NULL,
date DATE NOT NULL,
type TEXT NOT NULL CHECK(type IN ('chemical', 'fertilizer', 'watering', 'pruning', 'transplanting', 'other')),
description TEXT NOT NULL,
amount TEXT,
is_planned BOOLEAN DEFAULT 0,
is_completed BOOLEAN DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (plant_id) REFERENCES plants (id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS tasks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
plant_id INTEGER,
title TEXT NOT NULL,
description TEXT,
deadline DATE NOT NULL,
completed BOOLEAN DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (plant_id) REFERENCES plants (id) ON DELETE SET NULL
);
`);
console.log('✅ Таблицы успешно созданы');
// Вставка тестовых данных
const insertPlant = db.prepare(`
INSERT INTO plants (purchase_location, seedling_age, type, variety, seedling_height, planting_date, current_height, health_status, photo_url, current_photo_url, notes)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
insertPlant.run('Local Nursery', 12, 'tree', 'Apple - Honeycrisp', 45, '2022-04-15', 180, 'good', null, null, 'Growing well');
insertPlant.run('Garden Center', 8, 'shrub', 'Blueberry - Bluecrop', 25, '2023-03-20', 85, 'needs-attention', null, null, 'Needs fertilizer');
insertPlant.run('Online Store', 3, 'herb', 'Basil - Sweet Genovese', 8, '2024-05-10', 35, 'good', null, null, 'Harvested weekly');
const insertTask = db.prepare(`
INSERT INTO tasks (plant_id, title, description, deadline, completed)
VALUES (?, ?, ?, ?, ?)
`);
insertTask.run(1, 'Apply fertilizer', 'Spring fertilizer for apple trees', '2024-03-15', 0);
insertTask.run(2, 'Prune blueberries', 'Spring pruning', '2024-02-28', 1);
insertTask.run(3, 'Harvest basil', 'Every 2 weeks', '2024-06-01', 0);
console.log('✅ Тестовые данные успешно вставлены');
console.log('📦 База данных готова');

View File

@ -1,4 +1,4 @@
const sqlite3 = require('sqlite3').verbose(); const Database = require('better-sqlite3');
const path = require('path'); const path = require('path');
const fs = require('fs'); const fs = require('fs');
@ -10,20 +10,19 @@ if (!fs.existsSync(DB_DIR)) {
fs.mkdirSync(DB_DIR, { recursive: true }); fs.mkdirSync(DB_DIR, { recursive: true });
} }
const db = new sqlite3.Database(DB_PATH, (err) => { let db;
if (err) { try {
console.log(DB_PATH); db = new Database(DB_PATH);
console.error('Error creating database:', err.message); console.log('Connected to SQLite database');
process.exit(1); } catch (err) {
} else { console.error('Error creating database:', err.message);
console.log('Connected to SQLite database'); process.exit(1);
} }
});
// Create tables // Create tables
db.serialize(() => { try {
// Plants table // Plants table
db.run(` db.exec(`
CREATE TABLE IF NOT EXISTS plants ( CREATE TABLE IF NOT EXISTS plants (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
purchase_location TEXT, purchase_location TEXT,
@ -43,7 +42,7 @@ db.serialize(() => {
`); `);
// Plant history table // Plant history table
db.run(` db.exec(`
CREATE TABLE IF NOT EXISTS plant_history ( CREATE TABLE IF NOT EXISTS plant_history (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
plant_id INTEGER NOT NULL, plant_id INTEGER NOT NULL,
@ -57,7 +56,7 @@ db.serialize(() => {
`); `);
// Harvest records table // Harvest records table
db.run(` db.exec(`
CREATE TABLE IF NOT EXISTS harvest_records ( CREATE TABLE IF NOT EXISTS harvest_records (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
plant_id INTEGER NOT NULL, plant_id INTEGER NOT NULL,
@ -72,7 +71,7 @@ db.serialize(() => {
`); `);
// Maintenance records table // Maintenance records table
db.run(` db.exec(`
CREATE TABLE IF NOT EXISTS maintenance_records ( CREATE TABLE IF NOT EXISTS maintenance_records (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
plant_id INTEGER NOT NULL, plant_id INTEGER NOT NULL,
@ -87,7 +86,7 @@ db.serialize(() => {
`); `);
// Tasks table // Tasks table
db.run(` db.exec(`
CREATE TABLE IF NOT EXISTS tasks ( CREATE TABLE IF NOT EXISTS tasks (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
plant_id INTEGER, plant_id INTEGER,
@ -107,36 +106,60 @@ db.serialize(() => {
console.log('Inserting sample data...'); console.log('Inserting sample data...');
// Sample plants // Sample plants
const plantSql = `INSERT INTO plants (purchase_location, seedling_age, type, variety, seedling_height, planting_date, current_height, health_status, photo_url, current_photo_url, notes) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`; const insertPlant = db.prepare(`
db.run(plantSql, ['Local Nursery', 12, 'tree', 'Apple - Honeycrisp', 45.0, '2022-04-15', 180.0, 'good', null, null, 'Growing well, good fruit production']); INSERT INTO plants (purchase_location, seedling_age, type, variety, seedling_height, planting_date, current_height, health_status, photo_url, current_photo_url, notes)
db.run(plantSql, ['Garden Center', 8, 'shrub', 'Blueberry - Bluecrop', 25.0, '2023-03-20', 85.0, 'needs-attention', null, null, 'Leaves showing slight discoloration']); VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
db.run(plantSql, ['Online Store', 3, 'herb', 'Basil - Sweet Genovese', 8.0, '2024-05-10', 35.0, 'good', null, null, 'Producing well, regular harvests']); `);
insertPlant.run('Local Nursery', 12, 'tree', 'Apple - Honeycrisp', 45.0, '2022-04-15', 180.0, 'good', null, null, 'Growing well, good fruit production');
insertPlant.run('Garden Center', 8, 'shrub', 'Blueberry - Bluecrop', 25.0, '2023-03-20', 85.0, 'needs-attention', null, null, 'Leaves showing slight discoloration');
insertPlant.run('Online Store', 3, 'herb', 'Basil - Sweet Genovese', 8.0, '2024-05-10', 35.0, 'good', null, null, 'Producing well, regular harvests');
// Sample tasks // Sample tasks
const taskSql = `INSERT INTO tasks (plant_id, title, description, deadline, completed) VALUES (?, ?, ?, ?, ?)`; const insertTask = db.prepare(`
db.run(taskSql, [1, 'Apply fertilizer', 'Apply spring fertilizer to apple trees', '2024-03-15', 0]); INSERT INTO tasks (plant_id, title, description, deadline, completed)
db.run(taskSql, [2, 'Prune blueberry bushes', 'Annual pruning before spring growth', '2024-02-28', 1]); VALUES (?, ?, ?, ?, ?)
db.run(taskSql, [3, 'Harvest basil', 'Regular harvest to encourage growth', '2024-06-01', 0]); `);
insertTask.run(1, 'Apply fertilizer', 'Apply spring fertilizer to apple trees', '2024-03-15', 0);
insertTask.run(2, 'Prune blueberry bushes', 'Annual pruning before spring growth', '2024-02-28', 1);
insertTask.run(3, 'Harvest basil', 'Regular harvest to encourage growth', '2024-06-01', 0);
// Sample maintenance records // Sample maintenance records
const maintenanceSql = `INSERT INTO maintenance_records (plant_id, date, type, description, amount) VALUES (?, ?, ?, ?, ?)`; const insertMaintenance = db.prepare(`
db.run(maintenanceSql, [1, '2024-01-10', 'pruning', 'Winter pruning - removed dead branches', null]); INSERT INTO maintenance_records (plant_id, date, type, description, amount)
db.run(maintenanceSql, [2, '2024-01-05', 'fertilizer', 'Applied organic compost', '2 cups']); VALUES (?, ?, ?, ?, ?)
db.run(maintenanceSql, [3, '2024-05-15', 'watering', 'Deep watering during dry spell', '1 gallon']); `);
insertMaintenance.run(1, '2024-01-10', 'pruning', 'Winter pruning - removed dead branches', null);
insertMaintenance.run(2, '2024-01-05', 'fertilizer', 'Applied organic compost', '2 cups');
insertMaintenance.run(3, '2024-05-15', 'watering', 'Deep watering during dry spell', '1 gallon');
// Sample harvest records // Sample harvest records
const harvestSql = `INSERT INTO harvest_records (plant_id, date, quantity, unit, notes) VALUES (?, ?, ?, ?, ?)`; const insertHarvest = db.prepare(`
db.run(harvestSql, [1, '2023-09-15', 25, 'lbs', 'Excellent harvest, apples were sweet and crisp']); INSERT INTO harvest_records (plant_id, date, quantity, unit, notes)
db.run(harvestSql, [2, '2023-07-20', 3, 'cups', 'First harvest of the season, berries were plump and sweet']); VALUES (?, ?, ?, ?, ?)
db.run(harvestSql, [3, '2024-05-25', 0.5, 'cups', 'Fresh basil for cooking, very aromatic']); `);
insertHarvest.run(1, '2023-09-15', 25, 'lbs', 'Excellent harvest, apples were sweet and crisp');
insertHarvest.run(2, '2023-07-20', 3, 'cups', 'First harvest of the season, berries were plump and sweet');
insertHarvest.run(3, '2024-05-25', 0.5, 'cups', 'Fresh basil for cooking, very aromatic');
// Sample observations
const insertObservation = db.prepare(`
INSERT INTO plant_observations (plant_id, date, title, observation, weather_conditions, temperature, tags)
VALUES (?, ?, ?, ?, ?, ?, ?)
`);
insertObservation.run(1, '2024-01-15', 'Winter dormancy check', 'Apple tree showing normal winter dormancy. Buds are tight and healthy looking. No signs of pest damage on bark.', 'Clear, cold', 2.5, 'dormancy,health-check,winter');
insertObservation.run(2, '2024-01-20', 'Pruning completed', 'Finished annual pruning of blueberry bushes. Removed about 20% of old wood and opened up center for better air circulation.', 'Overcast', 8.0, 'pruning,maintenance');
insertObservation.run(3, '2024-05-12', 'First true leaves', 'Basil seedlings showing first set of true leaves. Growth is vigorous and color is deep green. Ready for transplanting soon.', 'Sunny', 22.0, 'growth,seedling,transplant-ready');
console.log('Sample data inserted successfully!'); console.log('Sample data inserted successfully!');
}); } catch (err) {
console.error('Error initializing database:', err);
process.exit(1);
}
db.close((err) => { db.close();
if (err) { console.log('Database initialization completed!');
console.error('Error closing database:', err.message);
} else {
console.log('Database initialization completed!');
}
});

File diff suppressed because it is too large Load Diff

View File

@ -27,18 +27,7 @@ services:
volumes: volumes:
- ./backend/database:/app/database - ./backend/database:/app/database
- ./backend:/app - ./backend:/app
command: ["node", "server.js"] command: ["sh", "-c", "node scripts/init-db.js && node server.js"]
db-init:
build:
context: ./backend
dockerfile: Dockerfile
volumes:
- ./backend/database:/app/database
- ./backend:/app
command: ["node", "scripts/init-db.js"]
depends_on:
- backend
volumes: volumes:
db_data: db_data:

View File

@ -1,7 +1,7 @@
{ {
"name": "vite-react-typescript-starter", "name": "vite-react-typescript-starter",
"private": true, "private": true,
"version": "0.0.0", "version": "0.1.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite --mode development", "dev": "vite --mode development",

View File

@ -1,10 +1,11 @@
import { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { Sprout, Calendar, CheckSquare, Activity, Plus } from 'lucide-react'; import { Sprout, Calendar, CheckSquare, Activity, Plus, BookOpen } from 'lucide-react';
import { Plant, Task, MaintenanceRecord, HarvestRecord } from './types'; import { Plant, Task, MaintenanceRecord, HarvestRecord, PlantObservation } from './types';
import Dashboard from './components/Dashboard'; import Dashboard from './components/Dashboard';
import PlantRegistry from './components/PlantRegistry'; import PlantRegistry from './components/PlantRegistry';
import TaskPlanner from './components/TaskPlanner'; import TaskPlanner from './components/TaskPlanner';
import MaintenanceLog from './components/MaintenanceLog'; import MaintenanceLog from './components/MaintenanceLog';
import ObservationJournal from './components/ObservationJournal';
import { apiService } from './services/api'; import { apiService } from './services/api';
function App() { function App() {
@ -13,6 +14,7 @@ function App() {
const [tasks, setTasks] = useState<Task[]>([]); const [tasks, setTasks] = useState<Task[]>([]);
const [maintenanceRecords, setMaintenanceRecords] = useState<MaintenanceRecord[]>([]); const [maintenanceRecords, setMaintenanceRecords] = useState<MaintenanceRecord[]>([]);
const [harvestRecords, setHarvestRecords] = useState<HarvestRecord[]>([]); const [harvestRecords, setHarvestRecords] = useState<HarvestRecord[]>([]);
const [observations, setObservations] = useState<PlantObservation[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
useEffect(() => { useEffect(() => {
@ -26,17 +28,19 @@ function App() {
// Test backend connection first // Test backend connection first
await apiService.healthCheck(); await apiService.healthCheck();
const [plantsData, tasksData, maintenanceData, harvestData] = await Promise.all([ const [plantsData, tasksData, maintenanceData, harvestData, observationsData] = await Promise.all([
apiService.getPlants(), apiService.getPlants(),
apiService.getTasks(), apiService.getTasks(),
apiService.getMaintenanceRecords(), apiService.getMaintenanceRecords(),
apiService.getHarvestRecords() apiService.getHarvestRecords(),
apiService.getPlantObservations()
]); ]);
setPlants(plantsData); setPlants(plantsData);
setTasks(tasksData); setTasks(tasksData);
setMaintenanceRecords(maintenanceData); setMaintenanceRecords(maintenanceData);
setHarvestRecords(harvestData); setHarvestRecords(harvestData);
setObservations(observationsData);
} catch (error) { } catch (error) {
console.error('Error loading data:', error); console.error('Error loading data:', error);
// You could add a toast notification or error state here // You could add a toast notification or error state here
@ -47,10 +51,11 @@ function App() {
}; };
const navItems = [ const navItems = [
{ id: 'dashboard', label: 'Dashboard', icon: Activity }, { id: 'dashboard', label: 'Панель', icon: Activity },
{ id: 'plants', label: 'Plant Registry', icon: Sprout }, { id: 'plants', label: 'Реестр растений', icon: Sprout },
{ id: 'tasks', label: 'Task Planner', icon: CheckSquare }, { id: 'tasks', label: 'План работ', icon: CheckSquare },
{ id: 'maintenance', label: 'Maintenance Log', icon: Calendar }, { id: 'maintenance', label: 'Журнал', icon: Calendar },
{ id: 'observations', label: 'Дневник', icon: BookOpen },
]; ];
if (loading) { if (loading) {
@ -129,6 +134,13 @@ function App() {
onMaintenanceChange={setMaintenanceRecords} onMaintenanceChange={setMaintenanceRecords}
/> />
)} )}
{activeTab === 'observations' && (
<ObservationJournal
observations={observations}
plants={plants}
onObservationsChange={setObservations}
/>
)}
</main> </main>
{/* Mobile Navigation */} {/* Mobile Navigation */}

View File

@ -30,28 +30,28 @@ const Dashboard: React.FC<DashboardProps> = ({
const stats = [ const stats = [
{ {
label: 'Total Plants', label: 'Всего растений',
value: plants.length, value: plants.length,
icon: Sprout, icon: Sprout,
color: 'text-green-600', color: 'text-green-600',
bgColor: 'bg-green-100' bgColor: 'bg-green-100'
}, },
{ {
label: 'Pending Tasks', label: 'Отложенные работы',
value: tasks.filter(task => !task.completed).length, value: tasks.filter(task => !task.completed).length,
icon: Calendar, icon: Calendar,
color: 'text-blue-600', color: 'text-blue-600',
bgColor: 'bg-blue-100' bgColor: 'bg-blue-100'
}, },
{ {
label: 'Need Attention', label: 'Требует внимания',
value: plantsNeedingAttention.length, value: plantsNeedingAttention.length,
icon: AlertTriangle, icon: AlertTriangle,
color: 'text-yellow-600', color: 'text-yellow-600',
bgColor: 'bg-yellow-100' bgColor: 'bg-yellow-100'
}, },
{ {
label: 'This Year Harvests', label: ' Урожай этого года',
value: harvestRecords.filter(record => value: harvestRecords.filter(record =>
new Date(record.date).getFullYear() === new Date().getFullYear() new Date(record.date).getFullYear() === new Date().getFullYear()
).length, ).length,
@ -101,12 +101,12 @@ const Dashboard: React.FC<DashboardProps> = ({
<div className="bg-white rounded-lg shadow-sm border border-green-100"> <div className="bg-white rounded-lg shadow-sm border border-green-100">
<div className="p-6 border-b border-green-100"> <div className="p-6 border-b border-green-100">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<h3 className="text-lg font-semibold text-green-800">Upcoming Tasks</h3> <h3 className="text-lg font-semibold text-green-800">Предстоящие работы</h3>
<button <button
onClick={() => onNavigate('tasks')} onClick={() => onNavigate('tasks')}
className="text-sm text-green-600 hover:text-green-700 transition-colors" className="text-sm text-green-600 hover:text-green-700 transition-colors"
> >
View all Посмотреть все
</button> </button>
</div> </div>
</div> </div>
@ -125,7 +125,7 @@ const Dashboard: React.FC<DashboardProps> = ({
<p className="text-sm text-gray-600">{plant.variety}</p> <p className="text-sm text-gray-600">{plant.variety}</p>
)} )}
<p className={`text-sm ${isOverdue ? 'text-red-600' : 'text-gray-500'}`}> <p className={`text-sm ${isOverdue ? 'text-red-600' : 'text-gray-500'}`}>
Due: {new Date(task.deadline).toLocaleDateString()} Срок: {new Date(task.deadline).toLocaleDateString()}
</p> </p>
</div> </div>
</div> </div>
@ -133,7 +133,7 @@ const Dashboard: React.FC<DashboardProps> = ({
})} })}
</div> </div>
) : ( ) : (
<p className="text-gray-500 text-center py-8">No upcoming tasks</p> <p className="text-gray-500 text-center py-8">Никаких предстоящих работ нет</p>
)} )}
</div> </div>
</div> </div>
@ -142,12 +142,12 @@ const Dashboard: React.FC<DashboardProps> = ({
<div className="bg-white rounded-lg shadow-sm border border-green-100"> <div className="bg-white rounded-lg shadow-sm border border-green-100">
<div className="p-6 border-b border-green-100"> <div className="p-6 border-b border-green-100">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<h3 className="text-lg font-semibold text-green-800">Plants Needing Attention</h3> <h3 className="text-lg font-semibold text-green-800">Растения, требующие внимания</h3>
<button <button
onClick={() => onNavigate('plants')} onClick={() => onNavigate('plants')}
className="text-sm text-green-600 hover:text-green-700 transition-colors" className="text-sm text-green-600 hover:text-green-700 transition-colors"
> >
View all Посмотреть все
</button> </button>
</div> </div>
</div> </div>
@ -178,12 +178,12 @@ const Dashboard: React.FC<DashboardProps> = ({
<div className="bg-white rounded-lg shadow-sm border border-green-100"> <div className="bg-white rounded-lg shadow-sm border border-green-100">
<div className="p-6 border-b border-green-100"> <div className="p-6 border-b border-green-100">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<h3 className="text-lg font-semibold text-green-800">Recent Maintenance</h3> <h3 className="text-lg font-semibold text-green-800">Недавнии работы</h3>
<button <button
onClick={() => onNavigate('maintenance')} onClick={() => onNavigate('maintenance')}
className="text-sm text-green-600 hover:text-green-700 transition-colors" className="text-sm text-green-600 hover:text-green-700 transition-colors"
> >
View all Посмотреть все
</button> </button>
</div> </div>
</div> </div>
@ -209,7 +209,7 @@ const Dashboard: React.FC<DashboardProps> = ({
})} })}
</div> </div>
) : ( ) : (
<p className="text-gray-500 text-center py-8">No maintenance records yet</p> <p className="text-gray-500 text-center py-8">Записей о выполненных работах пока нет</p>
)} )}
</div> </div>
</div> </div>

View File

@ -42,7 +42,7 @@ const MaintenanceForm: React.FC<MaintenanceFormProps> = ({ plants, onSave, onCan
<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 max-h-[90vh] overflow-y-auto"> <div className="bg-white rounded-lg shadow-xl max-w-md w-full max-h-[90vh] overflow-y-auto">
<div className="flex justify-between items-center p-6 border-b border-gray-200"> <div className="flex justify-between items-center p-6 border-b border-gray-200">
<h3 className="text-lg font-semibold text-gray-900">Log Maintenance Activity</h3> <h3 className="text-lg font-semibold text-gray-900">Выполненная работа</h3>
<button <button
onClick={onCancel} onClick={onCancel}
className="text-gray-400 hover:text-gray-600 transition-colors" className="text-gray-400 hover:text-gray-600 transition-colors"
@ -54,7 +54,7 @@ const MaintenanceForm: React.FC<MaintenanceFormProps> = ({ plants, onSave, onCan
<form onSubmit={handleSubmit} className="p-6 space-y-4"> <form onSubmit={handleSubmit} className="p-6 space-y-4">
<div> <div>
<label htmlFor="plantId" className="block text-sm font-medium text-gray-700 mb-1"> <label htmlFor="plantId" className="block text-sm font-medium text-gray-700 mb-1">
Plant * Растение *
</label> </label>
<select <select
id="plantId" id="plantId"
@ -64,7 +64,7 @@ const MaintenanceForm: React.FC<MaintenanceFormProps> = ({ plants, onSave, onCan
required required
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" 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="">Select a plant</option> <option value="">Выбор растения</option>
{plants.map(plant => ( {plants.map(plant => (
<option key={plant.id} value={plant.id}> <option key={plant.id} value={plant.id}>
{plant.variety} ({plant.type}) {plant.variety} ({plant.type})
@ -75,7 +75,7 @@ const MaintenanceForm: React.FC<MaintenanceFormProps> = ({ plants, onSave, onCan
<div> <div>
<label htmlFor="date" className="block text-sm font-medium text-gray-700 mb-1"> <label htmlFor="date" className="block text-sm font-medium text-gray-700 mb-1">
Date * Дата *
</label> </label>
<input <input
type="date" type="date"
@ -90,7 +90,7 @@ const MaintenanceForm: React.FC<MaintenanceFormProps> = ({ plants, onSave, onCan
<div> <div>
<label htmlFor="type" className="block text-sm font-medium text-gray-700 mb-1"> <label htmlFor="type" className="block text-sm font-medium text-gray-700 mb-1">
Activity Type * Вид деятельности *
</label> </label>
<select <select
id="type" id="type"
@ -100,25 +100,25 @@ const MaintenanceForm: React.FC<MaintenanceFormProps> = ({ plants, onSave, onCan
required required
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" 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="other">Other</option> <option value="other">Другое</option>
<option value="chemical">Chemical Treatment</option> <option value="chemical">Обработка химикатами</option>
<option value="fertilizer">Fertilizer Application</option> <option value="fertilizer">Подкормка удобрением</option>
<option value="watering">Watering</option> <option value="watering">Полив</option>
<option value="pruning">Pruning</option> <option value="pruning">Обрезка</option>
<option value="transplanting">Transplanting</option> <option value="transplanting">Пересадка</option>
</select> </select>
</div> </div>
<div> <div>
<label htmlFor="description" className="block text-sm font-medium text-gray-700 mb-1"> <label htmlFor="description" className="block text-sm font-medium text-gray-700 mb-1">
Description * Описание *
</label> </label>
<textarea <textarea
id="description" id="description"
name="description" name="description"
value={formData.description} value={formData.description}
onChange={handleChange} onChange={handleChange}
placeholder="Describe the maintenance activity..." placeholder="Опишите мероприятия по работам..."
required required
rows={4} rows={4}
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" 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"
@ -127,7 +127,7 @@ const MaintenanceForm: React.FC<MaintenanceFormProps> = ({ plants, onSave, onCan
<div> <div>
<label htmlFor="amount" className="block text-sm font-medium text-gray-700 mb-1"> <label htmlFor="amount" className="block text-sm font-medium text-gray-700 mb-1">
Amount/Quantity (Optional) Количество (необязательно)
</label> </label>
<input <input
type="text" type="text"
@ -135,7 +135,7 @@ const MaintenanceForm: React.FC<MaintenanceFormProps> = ({ plants, onSave, onCan
name="amount" name="amount"
value={formData.amount} value={formData.amount}
onChange={handleChange} onChange={handleChange}
placeholder="e.g., 2 cups, 1 gallon, 500ml" placeholder="например, 2 чашки, 1 кг., 500 мл."
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" 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>
@ -151,7 +151,7 @@ const MaintenanceForm: React.FC<MaintenanceFormProps> = ({ plants, onSave, onCan
className="h-4 w-4 text-green-600 focus:ring-green-500 border-gray-300 rounded" className="h-4 w-4 text-green-600 focus:ring-green-500 border-gray-300 rounded"
/> />
<label htmlFor="isPlanned" className="ml-2 block text-sm text-gray-700"> <label htmlFor="isPlanned" className="ml-2 block text-sm text-gray-700">
Planned Activity Запланированная работа
</label> </label>
</div> </div>
@ -165,7 +165,7 @@ const MaintenanceForm: React.FC<MaintenanceFormProps> = ({ plants, onSave, onCan
className="h-4 w-4 text-green-600 focus:ring-green-500 border-gray-300 rounded" className="h-4 w-4 text-green-600 focus:ring-green-500 border-gray-300 rounded"
/> />
<label htmlFor="isCompleted" className="ml-2 block text-sm text-gray-700"> <label htmlFor="isCompleted" className="ml-2 block text-sm text-gray-700">
Completed Завершена
</label> </label>
</div> </div>
</div> </div>
@ -176,13 +176,13 @@ const MaintenanceForm: React.FC<MaintenanceFormProps> = ({ plants, onSave, onCan
onClick={onCancel} onClick={onCancel}
className="px-4 py-2 text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-md transition-colors" className="px-4 py-2 text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-md transition-colors"
> >
Cancel Отмена
</button> </button>
<button <button
type="submit" type="submit"
className="px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-md transition-colors" className="px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-md transition-colors"
> >
Log Activity Выполнено
</button> </button>
</div> </div>
</form> </form>

View File

@ -66,15 +66,15 @@ const MaintenanceLog: React.FC<MaintenanceLogProps> = ({
<div className="space-y-6"> <div className="space-y-6">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<div> <div>
<h2 className="text-3xl font-bold text-green-800">Maintenance Log</h2> <h2 className="text-3xl font-bold text-green-800">Журнал выполненных работ</h2>
<p className="text-green-600 mt-1">Track all your garden maintenance activities</p> <p className="text-green-600 mt-1">Отслеживайте все ваши мероприятия по уходу за садом</p>
</div> </div>
<button <button
onClick={() => setShowMaintenanceForm(true)} onClick={() => setShowMaintenanceForm(true)}
className="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-lg flex items-center space-x-2 transition-colors" className="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-lg flex items-center space-x-2 transition-colors"
> >
<Plus className="h-4 w-4" /> <Plus className="h-4 w-4" />
<span>Log Activity</span> <span>Запись</span>
</button> </button>
</div> </div>
@ -82,12 +82,12 @@ const MaintenanceLog: React.FC<MaintenanceLogProps> = ({
<div className="bg-white p-6 rounded-lg shadow-sm border border-green-100"> <div className="bg-white p-6 rounded-lg shadow-sm border border-green-100">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4"> <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-2">Search</label> <label className="block text-sm font-medium text-gray-700 mb-2">Поиск</label>
<div className="relative"> <div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" /> <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
<input <input
type="text" type="text"
placeholder="Search maintenance records..." placeholder="Поиск записей о работах..."
value={searchTerm} value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)} onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10 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" className="pl-10 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"
@ -96,30 +96,30 @@ const MaintenanceLog: React.FC<MaintenanceLogProps> = ({
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-2">Activity Type</label> <label className="block text-sm font-medium text-gray-700 mb-2">Тип работы</label>
<select <select
value={filterType} value={filterType}
onChange={(e) => setFilterType(e.target.value)} onChange={(e) => setFilterType(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 focus:border-transparent" 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="all">All Types</option> <option value="all">Все типы</option>
<option value="chemical">Chemical Treatment</option> <option value="chemical">Химическая обработка</option>
<option value="fertilizer">Fertilizer</option> <option value="fertilizer">Удобрение</option>
<option value="watering">Watering</option> <option value="watering">Полив</option>
<option value="pruning">Pruning</option> <option value="pruning">Обрезка</option>
<option value="transplanting">Transplanting</option> <option value="transplanting">Пересадка</option>
<option value="other">Other</option> <option value="other">Other</option>
</select> </select>
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-2">Plant</label> <label className="block text-sm font-medium text-gray-700 mb-2">Растение</label>
<select <select
value={filterPlant} value={filterPlant}
onChange={(e) => setFilterPlant(e.target.value)} 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 focus:border-transparent" 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="all">All Plants</option> <option value="all">Все растения</option>
{plants.map(plant => ( {plants.map(plant => (
<option key={plant.id} value={plant.id}> <option key={plant.id} value={plant.id}>
{plant.variety} {plant.variety}
@ -178,12 +178,12 @@ const MaintenanceLog: React.FC<MaintenanceLogProps> = ({
{filteredRecords.length === 0 && ( {filteredRecords.length === 0 && (
<div className="text-center py-12"> <div className="text-center py-12">
<p className="text-gray-500 text-lg">No maintenance records found.</p> <p className="text-gray-500 text-lg">Записей о работах не найдено.</p>
<button <button
onClick={() => setShowMaintenanceForm(true)} onClick={() => setShowMaintenanceForm(true)}
className="mt-4 text-green-600 hover:text-green-700 transition-colors" className="mt-4 text-green-600 hover:text-green-700 transition-colors"
> >
Log your first activity Запишите свое первое действие в журнал
</button> </button>
</div> </div>
)} )}

View File

@ -0,0 +1,229 @@
import React, { useState, useEffect } from 'react';
import { Plant, PlantObservation } from '../types';
import { X } from 'lucide-react';
interface ObservationFormProps {
observation?: PlantObservation | null;
plants: Plant[];
onSave: (observation: Omit<PlantObservation, 'id' | 'createdAt' | 'updatedAt'>) => void;
onCancel: () => void;
}
const ObservationForm: React.FC<ObservationFormProps> = ({ observation, plants, onSave, onCancel }) => {
const [formData, setFormData] = useState({
plantId: '',
date: new Date().toISOString().split('T')[0],
title: '',
observation: '',
weatherConditions: '',
temperature: '',
photoUrl: '',
tags: ''
});
useEffect(() => {
if (observation) {
setFormData({
plantId: observation.plantId.toString(),
date: observation.date,
title: observation.title,
observation: observation.observation,
weatherConditions: observation.weatherConditions || '',
temperature: observation.temperature?.toString() || '',
photoUrl: observation.photoUrl || '',
tags: observation.tags || ''
});
}
}, [observation]);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
onSave({
...formData,
plantId: parseInt(formData.plantId),
temperature: formData.temperature ? parseFloat(formData.temperature) : undefined,
weatherConditions: formData.weatherConditions || undefined,
photoUrl: formData.photoUrl || undefined,
tags: formData.tags || undefined
});
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
};
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-2xl w-full max-h-[90vh] overflow-y-auto">
<div className="flex justify-between items-center p-6 border-b border-gray-200">
<h3 className="text-lg font-semibold text-gray-900">
{observation ? 'Изменить запись' : 'Добавить запись'}
</h3>
<button
onClick={onCancel}
className="text-gray-400 hover:text-gray-600 transition-colors"
>
<X className="h-5 w-5" />
</button>
</div>
<form onSubmit={handleSubmit} className="p-6 space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label htmlFor="plantId" className="block text-sm font-medium text-gray-700 mb-1">
Растение *
</label>
<select
id="plantId"
name="plantId"
value={formData.plantId}
onChange={handleChange}
required
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="">Select a plant</option>
{plants.map(plant => (
<option key={plant.id} value={plant.id}>
{plant.variety} ({plant.type})
</option>
))}
</select>
</div>
<div>
<label htmlFor="date" className="block text-sm font-medium text-gray-700 mb-1">
Дата *
</label>
<input
type="date"
id="date"
name="date"
value={formData.date}
onChange={handleChange}
required
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>
<div>
<label htmlFor="title" className="block text-sm font-medium text-gray-700 mb-1">
Наименование *
</label>
<input
type="text"
id="title"
name="title"
value={formData.title}
onChange={handleChange}
placeholder="например, появились первые бутоны, замечены повреждения от вредителей"
required
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="observation" className="block text-sm font-medium text-gray-700 mb-1">
Наблюдение *
</label>
<textarea
id="observation"
name="observation"
value={formData.observation}
onChange={handleChange}
placeholder="Наблюдение за состоянием растения, его ростом или любыми замеченными изменениями..."
required
rows={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 resize-none"
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label htmlFor="weatherConditions" className="block text-sm font-medium text-gray-700 mb-1">
Погодные условия
</label>
<input
type="text"
id="weatherConditions"
name="weatherConditions"
value={formData.weatherConditions}
onChange={handleChange}
placeholder="например, солнечно, дождливо, пасмурно"
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="temperature" className="block text-sm font-medium text-gray-700 mb-1">
Температура (°C)
</label>
<input
type="number"
id="temperature"
name="temperature"
value={formData.temperature}
onChange={handleChange}
step="0.1"
placeholder="например, 22.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>
<div>
<label htmlFor="photoUrl" className="block text-sm font-medium text-gray-700 mb-1">
Фотография
</label>
<input
type="url"
id="photoUrl"
name="photoUrl"
value={formData.photoUrl}
onChange={handleChange}
placeholder="https://example.com/observation-photo.jpg"
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="tags" className="block text-sm font-medium text-gray-700 mb-1">
Теги
</label>
<input
type="text"
id="tags"
name="tags"
value={formData.tags}
onChange={handleChange}
placeholder="например, рост, цветение, повреждение вредителями, обрезка"
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 className="flex justify-end space-x-3 pt-4 border-t border-gray-200">
<button
type="button"
onClick={onCancel}
className="px-4 py-2 text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-md transition-colors"
>
Отмена
</button>
<button
type="submit"
className="px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-md transition-colors"
>
{observation ? 'Сохранить' : 'Добавить'}
</button>
</div>
</form>
</div>
</div>
);
};
export default ObservationForm;

View File

@ -0,0 +1,260 @@
import React, { useState } from 'react';
import { Plant, PlantObservation } from '../types';
import { Plus, Search, Edit, Trash2, Thermometer, Cloud, Tag } from 'lucide-react';
import ObservationForm from './ObservationForm';
import { apiService } from '../services/api';
interface ObservationJournalProps {
observations: PlantObservation[];
plants: Plant[];
onObservationsChange: (observations: PlantObservation[]) => void;
}
const ObservationJournal: React.FC<ObservationJournalProps> = ({
observations,
plants,
onObservationsChange
}) => {
const [showObservationForm, setShowObservationForm] = useState(false);
const [editingObservation, setEditingObservation] = useState<PlantObservation | null>(null);
const [searchTerm, setSearchTerm] = useState('');
const [filterPlant, setFilterPlant] = useState('all');
const [filterTag, setFilterTag] = useState('all');
const filteredObservations = observations.filter(observation => {
const plant = plants.find(p => p.id === observation.plantId);
const matchesSearch = observation.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
observation.observation.toLowerCase().includes(searchTerm.toLowerCase()) ||
(plant?.variety.toLowerCase().includes(searchTerm.toLowerCase()));
const matchesPlant = filterPlant === 'all' || observation.plantId.toString() === filterPlant;
const matchesTag = filterTag === 'all' || (observation.tags && observation.tags.includes(filterTag));
return matchesSearch && matchesPlant && matchesTag;
}).sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
const allTags = Array.from(new Set(
observations
.filter(obs => obs.tags)
.flatMap(obs => obs.tags!.split(',').map(tag => tag.trim()))
)).sort();
const handleSaveObservation = async (observationData: Omit<PlantObservation, 'id' | 'createdAt' | 'updatedAt'>) => {
try {
if (editingObservation) {
const updatedObservation = await apiService.updatePlantObservation(editingObservation.id, observationData);
onObservationsChange(observations.map(o => o.id === editingObservation.id ? updatedObservation : o));
} else {
const newObservation = await apiService.createPlantObservation(observationData);
onObservationsChange([...observations, newObservation]);
}
setShowObservationForm(false);
setEditingObservation(null);
} catch (error) {
console.error('Error saving observation:', error);
}
};
const handleDeleteObservation = async (observationId: number) => {
if (window.confirm('Are you sure you want to delete this observation?')) {
try {
await apiService.deletePlantObservation(observationId);
onObservationsChange(observations.filter(o => o.id !== observationId));
} catch (error) {
console.error('Error deleting observation:', error);
}
}
};
const formatTags = (tags: string) => {
return tags.split(',').map(tag => tag.trim());
};
return (
<div className="space-y-6">
<div className="flex justify-between items-center">
<div>
<h2 className="text-3xl font-bold text-green-800">Дневник наблюдений</h2>
<p className="text-green-600 mt-1">Ежедневные заметки и наблюдения за вашими растениями</p>
</div>
<button
onClick={() => setShowObservationForm(true)}
className="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-lg flex items-center space-x-2 transition-colors"
>
<Plus className="h-4 w-4" />
<span>Наблюдение</span>
</button>
</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-3 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Поиск</label>
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
<input
type="text"
placeholder="Поиск по наблюдениям..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10 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>
<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 focus:border-transparent"
>
<option value="all">Все растения</option>
{plants.map(plant => (
<option key={plant.id} value={plant.id}>
{plant.variety}
</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Тег</label>
<select
value={filterTag}
onChange={(e) => setFilterTag(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 focus:border-transparent"
>
<option value="all">Все теги</option>
{allTags.map(tag => (
<option key={tag} value={tag}>
{tag}
</option>
))}
</select>
</div>
</div>
</div>
{/* Observations List */}
<div className="space-y-4">
{filteredObservations.map((observation) => {
const plant = plants.find(p => p.id === observation.plantId);
return (
<div key={observation.id} className="bg-white rounded-lg shadow-sm border border-green-100 p-6 hover:shadow-md transition-shadow">
<div className="flex items-start justify-between mb-4">
<div className="flex-1">
<div className="flex items-center space-x-3 mb-2">
<h3 className="text-lg font-semibold text-gray-900">{observation.title}</h3>
<span className="text-sm text-gray-500">
{new Date(observation.date).toLocaleDateString()}
</span>
</div>
{plant && (
<p className="text-sm text-gray-600 mb-3">
Plant: <span className="font-medium">{plant.variety}</span>
</p>
)}
<p className="text-gray-700 mb-4 leading-relaxed">{observation.observation}</p>
<div className="flex flex-wrap items-center gap-4 text-sm text-gray-600">
{observation.weatherConditions && (
<div className="flex items-center space-x-1">
<Cloud className="h-4 w-4" />
<span>{observation.weatherConditions}</span>
</div>
)}
{observation.temperature && (
<div className="flex items-center space-x-1">
<Thermometer className="h-4 w-4" />
<span>{observation.temperature}°C</span>
</div>
)}
</div>
{observation.tags && (
<div className="flex items-center space-x-2 mt-3">
<Tag className="h-4 w-4 text-gray-400" />
<div className="flex flex-wrap gap-1">
{formatTags(observation.tags).map((tag, index) => (
<span
key={index}
className="px-2 py-1 bg-green-100 text-green-700 text-xs rounded-full"
>
{tag}
</span>
))}
</div>
</div>
)}
{observation.photoUrl && (
<div className="mt-4">
<img
src={observation.photoUrl}
alt={observation.title}
className="w-full max-w-md h-48 object-cover rounded-lg"
onError={(e) => {
(e.target as HTMLImageElement).style.display = 'none';
}}
/>
</div>
)}
</div>
<div className="flex space-x-2 ml-4">
<button
onClick={() => {
setEditingObservation(observation);
setShowObservationForm(true);
}}
className="p-1 text-blue-600 hover:text-blue-700 transition-colors"
>
<Edit className="h-4 w-4" />
</button>
<button
onClick={() => handleDeleteObservation(observation.id)}
className="p-1 text-red-600 hover:text-red-700 transition-colors"
>
<Trash2 className="h-4 w-4" />
</button>
</div>
</div>
</div>
);
})}
</div>
{filteredObservations.length === 0 && (
<div className="text-center py-12">
<p className="text-gray-500 text-lg">Записи о наблюдениях не найдены.</p>
<button
onClick={() => setShowObservationForm(true)}
className="mt-4 text-green-600 hover:text-green-700 transition-colors"
>
Добавте первое наблюдение
</button>
</div>
)}
{/* Observation Form Modal */}
{showObservationForm && (
<ObservationForm
observation={editingObservation}
plants={plants}
onSave={handleSaveObservation}
onCancel={() => {
setShowObservationForm(false);
setEditingObservation(null);
}}
/>
)}
</div>
);
};
export default ObservationJournal;

View File

@ -60,7 +60,7 @@ const PlantForm: React.FC<PlantFormProps> = ({ plant, onSave, onCancel }) => {
<div className="bg-white rounded-lg shadow-xl max-w-md w-full max-h-[90vh] overflow-y-auto"> <div className="bg-white rounded-lg shadow-xl max-w-md w-full max-h-[90vh] overflow-y-auto">
<div className="flex justify-between items-center p-6 border-b border-gray-200"> <div className="flex justify-between items-center p-6 border-b border-gray-200">
<h3 className="text-lg font-semibold text-gray-900"> <h3 className="text-lg font-semibold text-gray-900">
{plant ? 'Edit Plant' : 'Add New Plant'} {plant ? 'Редактирование данных' : 'Новое растение'}
</h3> </h3>
<button <button
onClick={onCancel} onClick={onCancel}
@ -73,7 +73,7 @@ const PlantForm: React.FC<PlantFormProps> = ({ plant, onSave, onCancel }) => {
<form onSubmit={handleSubmit} className="p-6 space-y-4"> <form onSubmit={handleSubmit} className="p-6 space-y-4">
<div> <div>
<label htmlFor="type" className="block text-sm font-medium text-gray-700 mb-1"> <label htmlFor="type" className="block text-sm font-medium text-gray-700 mb-1">
Plant Type * Тип растения *
</label> </label>
<select <select
id="type" id="type"
@ -83,21 +83,21 @@ const PlantForm: React.FC<PlantFormProps> = ({ plant, onSave, onCancel }) => {
required required
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" 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="">Select a type</option> <option value="">Выбор типа</option>
<option value="tree">Tree</option> <option value="tree">Дерево</option>
<option value="shrub">Shrub</option> <option value="shrub">Кустарник</option>
<option value="herb">Herb</option> <option value="herb">Зелень</option>
<option value="vegetable">Vegetable</option> <option value="vegetable">Овощ</option>
<option value="flower">Flower</option> <option value="flower">Цветы</option>
<option value="vine">Vine</option> <option value="vine">Виноградная лоза</option>
<option value="grass">Grass</option> <option value="grass">Трава</option>
<option value="other">Other</option> <option value="other">Другое</option>
</select> </select>
</div> </div>
<div> <div>
<label htmlFor="variety" className="block text-sm font-medium text-gray-700 mb-1"> <label htmlFor="variety" className="block text-sm font-medium text-gray-700 mb-1">
Variety/Species * Вид/Сорт *
</label> </label>
<input <input
type="text" type="text"
@ -113,7 +113,7 @@ const PlantForm: React.FC<PlantFormProps> = ({ plant, onSave, onCancel }) => {
<div> <div>
<label htmlFor="purchaseLocation" className="block text-sm font-medium text-gray-700 mb-1"> <label htmlFor="purchaseLocation" className="block text-sm font-medium text-gray-700 mb-1">
Purchase Location * Место покупки *
</label> </label>
<input <input
type="text" type="text"
@ -130,7 +130,7 @@ const PlantForm: React.FC<PlantFormProps> = ({ plant, onSave, onCancel }) => {
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div> <div>
<label htmlFor="seedlingAge" className="block text-sm font-medium text-gray-700 mb-1"> <label htmlFor="seedlingAge" className="block text-sm font-medium text-gray-700 mb-1">
Seedling Age (months) * Возраст саженца (месяцы) *
</label> </label>
<input <input
type="number" type="number"
@ -146,7 +146,7 @@ const PlantForm: React.FC<PlantFormProps> = ({ plant, onSave, onCancel }) => {
<div> <div>
<label htmlFor="seedlingHeight" className="block text-sm font-medium text-gray-700 mb-1"> <label htmlFor="seedlingHeight" className="block text-sm font-medium text-gray-700 mb-1">
Seedling Height (cm) * Высота саженца (см) *
</label> </label>
<input <input
type="number" type="number"
@ -163,7 +163,7 @@ const PlantForm: React.FC<PlantFormProps> = ({ plant, onSave, onCancel }) => {
</div> </div>
<div> <div>
<label htmlFor="plantingDate" className="block text-sm font-medium text-gray-700 mb-1"> <label htmlFor="plantingDate" className="block text-sm font-medium text-gray-700 mb-1">
Planting Date * Дата посадки *
</label> </label>
<input <input
type="date" type="date"
@ -178,7 +178,7 @@ const PlantForm: React.FC<PlantFormProps> = ({ plant, onSave, onCancel }) => {
<div> <div>
<label htmlFor="currentHeight" className="block text-sm font-medium text-gray-700 mb-1"> <label htmlFor="currentHeight" className="block text-sm font-medium text-gray-700 mb-1">
Current Height (cm) Текущая высота (см)
</label> </label>
<input <input
type="number" type="number"
@ -193,7 +193,7 @@ const PlantForm: React.FC<PlantFormProps> = ({ plant, onSave, onCancel }) => {
</div> </div>
<div> <div>
<label htmlFor="healthStatus" className="block text-sm font-medium text-gray-700 mb-1"> <label htmlFor="healthStatus" className="block text-sm font-medium text-gray-700 mb-1">
Health Status Состояние
</label> </label>
<select <select
id="healthStatus" id="healthStatus"
@ -202,15 +202,15 @@ const PlantForm: React.FC<PlantFormProps> = ({ plant, onSave, onCancel }) => {
onChange={handleChange} 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" 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="good">Good</option> <option value="good">Хорошее</option>
<option value="needs-attention">Needs Attention</option> <option value="needs-attention">Требует внимания</option>
<option value="dead">Dead</option> <option value="dead">Погибло</option>
</select> </select>
</div> </div>
<div> <div>
<label htmlFor="photoUrl" className="block text-sm font-medium text-gray-700 mb-1"> <label htmlFor="photoUrl" className="block text-sm font-medium text-gray-700 mb-1">
Photo URL Фотография
</label> </label>
<input <input
type="url" type="url"
@ -224,7 +224,7 @@ const PlantForm: React.FC<PlantFormProps> = ({ plant, onSave, onCancel }) => {
</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">
Special Notes Заметки
</label> </label>
<textarea <textarea
id="notes" id="notes"
@ -243,13 +243,13 @@ const PlantForm: React.FC<PlantFormProps> = ({ plant, onSave, onCancel }) => {
onClick={onCancel} onClick={onCancel}
className="px-4 py-2 text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-md transition-colors" className="px-4 py-2 text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-md transition-colors"
> >
Cancel Отмена
</button> </button>
<button <button
type="submit" type="submit"
className="px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-md transition-colors" className="px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-md transition-colors"
> >
{plant ? 'Update Plant' : 'Add Plant'} {plant ? 'Сохранить' : 'Добавить'}
</button> </button>
</div> </div>
</form> </form>

View File

@ -97,15 +97,15 @@ const PlantRegistry: React.FC<PlantRegistryProps> = ({
<div className="space-y-6"> <div className="space-y-6">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<div> <div>
<h2 className="text-3xl font-bold text-green-800">Plant Registry</h2> <h2 className="text-3xl font-bold text-green-800">Рестр растений</h2>
<p className="text-green-600 mt-1">Manage your garden plants and track their progress</p> <p className="text-green-600 mt-1">Управляйте своими садовыми растениями и следите за их развитием</p>
</div> </div>
<button <button
onClick={() => setShowPlantForm(true)} onClick={() => setShowPlantForm(true)}
className="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-lg flex items-center space-x-2 transition-colors" className="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-lg flex items-center space-x-2 transition-colors"
> >
<Plus className="h-4 w-4" /> <Plus className="h-4 w-4" />
<span>Add Plant</span> <span>Новое растение</span>
</button> </button>
</div> </div>
@ -113,12 +113,12 @@ const PlantRegistry: React.FC<PlantRegistryProps> = ({
<div className="bg-white p-6 rounded-lg shadow-sm border border-green-100"> <div className="bg-white p-6 rounded-lg shadow-sm border border-green-100">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4"> <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-2">Search</label> <label className="block text-sm font-medium text-gray-700 mb-2">Поиск</label>
<div className="relative"> <div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" /> <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
<input <input
type="text" type="text"
placeholder="Search by variety or type..." placeholder="Поиск по сорту или типу..."
value={searchTerm} value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)} onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10 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" className="pl-10 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"
@ -126,29 +126,29 @@ const PlantRegistry: React.FC<PlantRegistryProps> = ({
</div> </div>
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-2">Plant Type</label> <label className="block text-sm font-medium text-gray-700 mb-2">Вид растения</label>
<select <select
value={filterType} value={filterType}
onChange={(e) => setFilterType(e.target.value)} onChange={(e) => setFilterType(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 focus:border-transparent" 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="all">All Types</option> <option value="all">Все виды</option>
{plantTypes.map(type => ( {plantTypes.map(type => (
<option key={type} value={type} className="capitalize">{type}</option> <option key={type} value={type} className="capitalize">{type}</option>
))} ))}
</select> </select>
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-2">Health Status</label> <label className="block text-sm font-medium text-gray-700 mb-2">Состояние</label>
<select <select
value={filterStatus} value={filterStatus}
onChange={(e) => setFilterStatus(e.target.value)} 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 focus:border-transparent" 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="all">All Status</option> <option value="all">Все состояния</option>
<option value="good">Good</option> <option value="good">Хорошее</option>
<option value="needs-attention">Needs Attention</option> <option value="needs-attention">Требует внимания</option>
<option value="dead">Dead</option> <option value="dead">Погибло</option>
</select> </select>
</div> </div>
</div> </div>
@ -169,7 +169,7 @@ const PlantRegistry: React.FC<PlantRegistryProps> = ({
<div> <div>
<h3 className="text-lg font-semibold text-gray-900">{plant.variety}</h3> <h3 className="text-lg font-semibold text-gray-900">{plant.variety}</h3>
<p className="text-sm text-gray-600 capitalize">{plant.type}</p> <p className="text-sm text-gray-600 capitalize">{plant.type}</p>
<p className="text-xs text-gray-500">From: {plant.purchaseLocation}</p> <p className="text-xs text-gray-500">Куплено в: {plant.purchaseLocation}</p>
</div> </div>
<div className="flex space-x-2"> <div className="flex space-x-2">
<button <button
@ -178,7 +178,7 @@ const PlantRegistry: React.FC<PlantRegistryProps> = ({
setShowStatistics(true); setShowStatistics(true);
}} }}
className="p-1 text-purple-600 hover:text-purple-700 transition-colors" className="p-1 text-purple-600 hover:text-purple-700 transition-colors"
title="View Statistics" title="Посмотреть Статистику"
> >
<BarChart3 className="h-4 w-4" /> <BarChart3 className="h-4 w-4" />
</button> </button>
@ -227,23 +227,23 @@ const PlantRegistry: React.FC<PlantRegistryProps> = ({
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Calendar className="h-4 w-4 text-gray-400" /> <Calendar className="h-4 w-4 text-gray-400" />
<span className="text-sm text-gray-600"> <span className="text-sm text-gray-600">
Planted: {new Date(plant.plantingDate).toLocaleDateString()} Посажено: {new Date(plant.plantingDate).toLocaleDateString()}
</span> </span>
</div> </div>
<div className="grid grid-cols-2 gap-2 text-sm text-gray-600"> <div className="grid grid-cols-2 gap-2 text-sm text-gray-600">
<div>Age: {plant.seedlingAge} months</div> <div>Возраст: {plant.seedlingAge} months</div>
<div>Initial: {plant.seedlingHeight}cm</div> <div>Начальный: {plant.seedlingHeight}cm</div>
{plant.currentHeight && ( {plant.currentHeight && (
<> <>
<div>Current: {plant.currentHeight}cm</div> <div>Текущий: {plant.currentHeight}cm</div>
<div>Growth: +{(plant.currentHeight - plant.seedlingHeight).toFixed(1)}cm</div> <div>Прирост: +{(plant.currentHeight - plant.seedlingHeight).toFixed(1)}cm</div>
</> </>
)} )}
</div> </div>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className="text-sm text-gray-600">Status:</span> <span className="text-sm text-gray-600">Состояние:</span>
<span className={`px-2 py-1 rounded-full text-xs font-medium ${getStatusColor(plant.healthStatus)}`}> <span className={`px-2 py-1 rounded-full text-xs font-medium ${getStatusColor(plant.healthStatus)}`}>
{plant.healthStatus.replace('-', ' ')} {plant.healthStatus.replace('-', ' ')}
</span> </span>
@ -251,7 +251,7 @@ const PlantRegistry: React.FC<PlantRegistryProps> = ({
{lastHarvest && ( {lastHarvest && (
<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">Last Harvest:</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} {lastHarvest.unit} on {new Date(lastHarvest.date).toLocaleDateString()} {lastHarvest.quantity} {lastHarvest.unit} on {new Date(lastHarvest.date).toLocaleDateString()}
</p> </p>
@ -277,7 +277,7 @@ const PlantRegistry: React.FC<PlantRegistryProps> = ({
onClick={() => setShowPlantForm(true)} onClick={() => setShowPlantForm(true)}
className="mt-4 text-green-600 hover:text-green-700 transition-colors" className="mt-4 text-green-600 hover:text-green-700 transition-colors"
> >
Add your first plant Добавьте первое растение
</button> </button>
</div> </div>
)} )}

View File

@ -77,7 +77,7 @@ const PlantStatisticsComponent: React.FC<PlantStatisticsProps> = ({
<h3 className="text-xl font-semibold text-gray-900"> <h3 className="text-xl font-semibold text-gray-900">
{plant.variety} - Statistics {plant.variety} - Statistics
</h3> </h3>
<p className="text-sm text-gray-600">Year: {selectedYear}</p> <p className="text-sm text-gray-600">Год: {selectedYear}</p>
</div> </div>
<button <button
onClick={onClose} onClick={onClose}
@ -91,7 +91,7 @@ const PlantStatisticsComponent: React.FC<PlantStatisticsProps> = ({
{/* Year Selector */} {/* Year Selector */}
<div className="mb-6"> <div className="mb-6">
<label className="block text-sm font-medium text-gray-700 mb-2"> <label className="block text-sm font-medium text-gray-700 mb-2">
Select Year Выбор года
</label> </label>
<select <select
value={selectedYear} value={selectedYear}
@ -109,7 +109,7 @@ const PlantStatisticsComponent: React.FC<PlantStatisticsProps> = ({
<div className="bg-green-50 p-4 rounded-lg"> <div className="bg-green-50 p-4 rounded-lg">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<p className="text-sm font-medium text-green-600">Total Harvest</p> <p className="text-sm font-medium text-green-600">Общий урожай</p>
<p className="text-2xl font-bold text-green-800"> <p className="text-2xl font-bold text-green-800">
{statistics.totalHarvest.toFixed(1)} {statistics.totalHarvest.toFixed(1)}
</p> </p>
@ -121,7 +121,7 @@ const PlantStatisticsComponent: React.FC<PlantStatisticsProps> = ({
<div className="bg-blue-50 p-4 rounded-lg"> <div className="bg-blue-50 p-4 rounded-lg">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<p className="text-sm font-medium text-blue-600">Height Growth</p> <p className="text-sm font-medium text-blue-600">Прибавка в росте</p>
<p className="text-2xl font-bold text-blue-800"> <p className="text-2xl font-bold text-blue-800">
+{statistics.heightGrowth.toFixed(1)}cm +{statistics.heightGrowth.toFixed(1)}cm
</p> </p>
@ -133,7 +133,7 @@ const PlantStatisticsComponent: React.FC<PlantStatisticsProps> = ({
<div className="bg-purple-50 p-4 rounded-lg"> <div className="bg-purple-50 p-4 rounded-lg">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<p className="text-sm font-medium text-purple-600">Harvest Events</p> <p className="text-sm font-medium text-purple-600">Сбор урожая</p>
<p className="text-2xl font-bold text-purple-800"> <p className="text-2xl font-bold text-purple-800">
{statistics.harvestDates.length} {statistics.harvestDates.length}
</p> </p>
@ -145,7 +145,7 @@ const PlantStatisticsComponent: React.FC<PlantStatisticsProps> = ({
<div className="bg-orange-50 p-4 rounded-lg"> <div className="bg-orange-50 p-4 rounded-lg">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<p className="text-sm font-medium text-orange-600">Maintenance</p> <p className="text-sm font-medium text-orange-600">Работы</p>
<p className="text-2xl font-bold text-orange-800"> <p className="text-2xl font-bold text-orange-800">
{Object.values(statistics.maintenanceCount).reduce((a, b) => a + b, 0)} {Object.values(statistics.maintenanceCount).reduce((a, b) => a + b, 0)}
</p> </p>
@ -157,23 +157,23 @@ const PlantStatisticsComponent: React.FC<PlantStatisticsProps> = ({
{/* Maintenance Breakdown */} {/* Maintenance Breakdown */}
<div className="bg-gray-50 p-6 rounded-lg mb-6"> <div className="bg-gray-50 p-6 rounded-lg mb-6">
<h4 className="text-lg font-semibold text-gray-900 mb-4">Maintenance Activities</h4> <h4 className="text-lg font-semibold text-gray-900 mb-4">Выполненные работы</h4>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4"> <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="text-center"> <div className="text-center">
<p className="text-2xl font-bold text-red-600">{statistics.maintenanceCount.chemical}</p> <p className="text-2xl font-bold text-red-600">{statistics.maintenanceCount.chemical}</p>
<p className="text-sm text-gray-600">Chemical Treatments</p> <p className="text-sm text-gray-600">Обработано химикатами</p>
</div> </div>
<div className="text-center"> <div className="text-center">
<p className="text-2xl font-bold text-green-600">{statistics.maintenanceCount.fertilizer}</p> <p className="text-2xl font-bold text-green-600">{statistics.maintenanceCount.fertilizer}</p>
<p className="text-sm text-gray-600">Fertilizations</p> <p className="text-sm text-gray-600">Внесено удобрений</p>
</div> </div>
<div className="text-center"> <div className="text-center">
<p className="text-2xl font-bold text-blue-600">{statistics.maintenanceCount.watering}</p> <p className="text-2xl font-bold text-blue-600">{statistics.maintenanceCount.watering}</p>
<p className="text-sm text-gray-600">Watering</p> <p className="text-sm text-gray-600">Полито</p>
</div> </div>
<div className="text-center"> <div className="text-center">
<p className="text-2xl font-bold text-yellow-600">{statistics.maintenanceCount.pruning}</p> <p className="text-2xl font-bold text-yellow-600">{statistics.maintenanceCount.pruning}</p>
<p className="text-sm text-gray-600">Pruning</p> <p className="text-sm text-gray-600">Обрезано</p>
</div> </div>
</div> </div>
</div> </div>

View File

@ -51,7 +51,7 @@ const TaskForm: React.FC<TaskFormProps> = ({ task, plants, onSave, onCancel }) =
<div className="bg-white rounded-lg shadow-xl max-w-md w-full max-h-[90vh] overflow-y-auto"> <div className="bg-white rounded-lg shadow-xl max-w-md w-full max-h-[90vh] overflow-y-auto">
<div className="flex justify-between items-center p-6 border-b border-gray-200"> <div className="flex justify-between items-center p-6 border-b border-gray-200">
<h3 className="text-lg font-semibold text-gray-900"> <h3 className="text-lg font-semibold text-gray-900">
{task ? 'Edit Task' : 'Create New Task'} {task ? 'Редактирование данных работы' : 'Добавление новой работы'}
</h3> </h3>
<button <button
onClick={onCancel} onClick={onCancel}
@ -64,7 +64,7 @@ const TaskForm: React.FC<TaskFormProps> = ({ task, plants, onSave, onCancel }) =
<form onSubmit={handleSubmit} className="p-6 space-y-4"> <form onSubmit={handleSubmit} className="p-6 space-y-4">
<div> <div>
<label htmlFor="title" className="block text-sm font-medium text-gray-700 mb-1"> <label htmlFor="title" className="block text-sm font-medium text-gray-700 mb-1">
Task Title * Наименование работы *
</label> </label>
<input <input
type="text" type="text"
@ -72,7 +72,7 @@ const TaskForm: React.FC<TaskFormProps> = ({ task, plants, onSave, onCancel }) =
name="title" name="title"
value={formData.title} value={formData.title}
onChange={handleChange} onChange={handleChange}
placeholder="e.g., Apply fertilizer" placeholder="например, Внести удобрение"
required required
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" 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"
/> />
@ -80,7 +80,7 @@ const TaskForm: React.FC<TaskFormProps> = ({ task, plants, onSave, onCancel }) =
<div> <div>
<label htmlFor="plantId" className="block text-sm font-medium text-gray-700 mb-1"> <label htmlFor="plantId" className="block text-sm font-medium text-gray-700 mb-1">
Associated Plant (Optional) Растение (опционально)
</label> </label>
<select <select
id="plantId" id="plantId"
@ -89,7 +89,7 @@ const TaskForm: React.FC<TaskFormProps> = ({ task, plants, onSave, onCancel }) =
onChange={handleChange} 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" 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="">Select a plant (optional)</option> <option value="">Выбор растения (опционально)</option>
{plants.map(plant => ( {plants.map(plant => (
<option key={plant.id} value={plant.id}> <option key={plant.id} value={plant.id}>
{plant.variety} ({plant.type}) {plant.variety} ({plant.type})
@ -100,7 +100,7 @@ const TaskForm: React.FC<TaskFormProps> = ({ task, plants, onSave, onCancel }) =
<div> <div>
<label htmlFor="deadline" className="block text-sm font-medium text-gray-700 mb-1"> <label htmlFor="deadline" className="block text-sm font-medium text-gray-700 mb-1">
Deadline * Срок исполнения *
</label> </label>
<input <input
type="date" type="date"
@ -115,14 +115,14 @@ const TaskForm: React.FC<TaskFormProps> = ({ task, plants, onSave, onCancel }) =
<div> <div>
<label htmlFor="description" className="block text-sm font-medium text-gray-700 mb-1"> <label htmlFor="description" className="block text-sm font-medium text-gray-700 mb-1">
Description Описание
</label> </label>
<textarea <textarea
id="description" id="description"
name="description" name="description"
value={formData.description} value={formData.description}
onChange={handleChange} onChange={handleChange}
placeholder="Detailed description of the task..." placeholder="Детальное описание работы..."
rows={4} rows={4}
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" 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"
/> />
@ -139,7 +139,7 @@ const TaskForm: React.FC<TaskFormProps> = ({ task, plants, onSave, onCancel }) =
className="h-4 w-4 text-green-600 focus:ring-green-500 border-gray-300 rounded" className="h-4 w-4 text-green-600 focus:ring-green-500 border-gray-300 rounded"
/> />
<label htmlFor="completed" className="ml-2 block text-sm text-gray-700"> <label htmlFor="completed" className="ml-2 block text-sm text-gray-700">
Mark as completed Отметить как выполненное
</label> </label>
</div> </div>
)} )}
@ -150,13 +150,13 @@ const TaskForm: React.FC<TaskFormProps> = ({ task, plants, onSave, onCancel }) =
onClick={onCancel} onClick={onCancel}
className="px-4 py-2 text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-md transition-colors" className="px-4 py-2 text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-md transition-colors"
> >
Cancel Отмена
</button> </button>
<button <button
type="submit" type="submit"
className="px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-md transition-colors" className="px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-md transition-colors"
> >
{task ? 'Update Task' : 'Create Task'} {task ? 'Сохранить' : 'Создать'}
</button> </button>
</div> </div>
</form> </form>

View File

@ -88,15 +88,15 @@ const TaskPlanner: React.FC<TaskPlannerProps> = ({ tasks, plants, onTasksChange
<div className="space-y-6"> <div className="space-y-6">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<div> <div>
<h2 className="text-3xl font-bold text-green-800">Task Planner</h2> <h2 className="text-3xl font-bold text-green-800">Планирование работ</h2>
<p className="text-green-600 mt-1">Plan and track your garden maintenance tasks</p> <p className="text-green-600 mt-1">Планируйте и отслеживайте свои работы по уходу за садом</p>
</div> </div>
<button <button
onClick={() => setShowTaskForm(true)} onClick={() => setShowTaskForm(true)}
className="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-lg flex items-center space-x-2 transition-colors" className="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-lg flex items-center space-x-2 transition-colors"
> >
<Plus className="h-4 w-4" /> <Plus className="h-4 w-4" />
<span>Add Task</span> <span>Новая работа</span>
</button> </button>
</div> </div>
@ -104,9 +104,9 @@ const TaskPlanner: React.FC<TaskPlannerProps> = ({ tasks, plants, onTasksChange
<div className="bg-white rounded-lg shadow-sm border border-green-100"> <div className="bg-white rounded-lg shadow-sm border border-green-100">
<div className="flex border-b border-green-100"> <div className="flex border-b border-green-100">
{[ {[
{ key: 'all', label: 'All Tasks', count: tasks.length }, { key: 'all', label: 'Все работы', count: tasks.length },
{ key: 'pending', label: 'Pending', count: tasks.filter(t => !t.completed).length }, { key: 'pending', label: 'Отложено', count: tasks.filter(t => !t.completed).length },
{ key: 'completed', label: 'Completed', count: tasks.filter(t => t.completed).length } { key: 'completed', label: 'Выполнено', count: tasks.filter(t => t.completed).length }
].map((tab) => ( ].map((tab) => (
<button <button
key={tab.key} key={tab.key}
@ -165,7 +165,7 @@ const TaskPlanner: React.FC<TaskPlannerProps> = ({ tasks, plants, onTasksChange
{plant && ( {plant && (
<p className="text-sm text-gray-600 mb-2"> <p className="text-sm text-gray-600 mb-2">
Plant: <span className="font-medium">{plant.variety}</span> Растение: <span className="font-medium">{plant.variety}</span>
</p> </p>
)} )}
@ -174,7 +174,7 @@ const TaskPlanner: React.FC<TaskPlannerProps> = ({ tasks, plants, onTasksChange
<div className="flex items-center space-x-4 text-sm text-gray-500"> <div className="flex items-center space-x-4 text-sm text-gray-500">
<div className="flex items-center space-x-1"> <div className="flex items-center space-x-1">
<Calendar className="h-4 w-4" /> <Calendar className="h-4 w-4" />
<span>Due: {new Date(task.deadline).toLocaleDateString()}</span> <span>Срок: {new Date(task.deadline).toLocaleDateString()}</span>
</div> </div>
{new Date(task.deadline) < new Date() && !task.completed && ( {new Date(task.deadline) < new Date() && !task.completed && (
<span className="text-red-600 font-medium">Overdue</span> <span className="text-red-600 font-medium">Overdue</span>

View File

@ -1,4 +1,4 @@
import { Plant, Task, MaintenanceRecord, HarvestRecord, PlantHistory } from '../types'; import { Plant, Task, MaintenanceRecord, HarvestRecord, PlantHistory, PlantObservation } from '../types';
class ApiService { class ApiService {
private baseUrl: string; private baseUrl: string;
@ -192,6 +192,36 @@ class ApiService {
}); });
} }
// Plant Observations API methods
async getPlantObservations(): Promise<PlantObservation[]> {
return this.makeRequest<PlantObservation[]>('/observations');
}
async createPlantObservation(
observation: Omit<PlantObservation, 'id' | 'createdAt' | 'updatedAt'>
): Promise<PlantObservation> {
return this.makeRequest<PlantObservation>('/observations', {
method: 'POST',
body: JSON.stringify(observation),
});
}
async updatePlantObservation(
id: number,
observation: Partial<PlantObservation>
): Promise<PlantObservation> {
return this.makeRequest<PlantObservation>(`/observations/${id}`, {
method: 'PUT',
body: JSON.stringify(observation),
});
}
async deletePlantObservation(id: number): Promise<void> {
return this.makeRequest<void>(`/observations/${id}`, {
method: 'DELETE',
});
}
// Health check // Health check
async healthCheck(): Promise<{ status: string; message: string }> { async healthCheck(): Promise<{ status: string; message: string }> {
return this.makeRequest<{ status: string; message: string }>('/health'); return this.makeRequest<{ status: string; message: string }>('/health');

View File

@ -28,6 +28,20 @@ export interface PlantHistory {
updatedAt: string; updatedAt: string;
} }
export interface PlantObservation {
id: number;
plantId: number;
date: string;
title: string;
observation: string;
weatherConditions?: string;
temperature?: number;
photoUrl?: string;
tags?: string;
createdAt: string;
updatedAt: string;
}
export interface HarvestRecord { export interface HarvestRecord {
id: number; id: number;
plantId: number; plantId: number;