Рабочая версия 0.1.0 Добавлен журнал наблюдений. Почти полная руссификация.
This commit is contained in:
parent
9d154596ff
commit
fee1e116d2
BIN
backend/database/gardentrack.db
Normal file
BIN
backend/database/gardentrack.db
Normal file
Binary file not shown.
1062
backend/package-lock.json
generated
1062
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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"
|
||||||
|
110
backend/scripts/init-db-better.js
Normal file
110
backend/scripts/init-db-better.js
Normal 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('📦 База данных готова');
|
@ -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
@ -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:
|
@ -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",
|
||||||
|
30
src/App.tsx
30
src/App.tsx
@ -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 */}
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
229
src/components/ObservationForm.tsx
Normal file
229
src/components/ObservationForm.tsx
Normal 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;
|
260
src/components/ObservationJournal.tsx
Normal file
260
src/components/ObservationJournal.tsx
Normal 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;
|
@ -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>
|
||||||
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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');
|
||||||
|
@ -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;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user