Рабочая версия 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": {
|
||||
"express": "^4.18.2",
|
||||
"sqlite3": "^5.1.6",
|
||||
"better-sqlite3": "^8.7.0",
|
||||
"cors": "^2.8.5",
|
||||
"body-parser": "^1.20.2",
|
||||
"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 fs = require('fs');
|
||||
|
||||
@ -10,20 +10,19 @@ if (!fs.existsSync(DB_DIR)) {
|
||||
fs.mkdirSync(DB_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
const db = new sqlite3.Database(DB_PATH, (err) => {
|
||||
if (err) {
|
||||
console.log(DB_PATH);
|
||||
console.error('Error creating database:', err.message);
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.log('Connected to SQLite database');
|
||||
}
|
||||
});
|
||||
let db;
|
||||
try {
|
||||
db = new Database(DB_PATH);
|
||||
console.log('Connected to SQLite database');
|
||||
} catch (err) {
|
||||
console.error('Error creating database:', err.message);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Create tables
|
||||
db.serialize(() => {
|
||||
try {
|
||||
// Plants table
|
||||
db.run(`
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS plants (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
purchase_location TEXT,
|
||||
@ -43,7 +42,7 @@ db.serialize(() => {
|
||||
`);
|
||||
|
||||
// Plant history table
|
||||
db.run(`
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS plant_history (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
plant_id INTEGER NOT NULL,
|
||||
@ -57,7 +56,7 @@ db.serialize(() => {
|
||||
`);
|
||||
|
||||
// Harvest records table
|
||||
db.run(`
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS harvest_records (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
plant_id INTEGER NOT NULL,
|
||||
@ -72,7 +71,7 @@ db.serialize(() => {
|
||||
`);
|
||||
|
||||
// Maintenance records table
|
||||
db.run(`
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS maintenance_records (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
plant_id INTEGER NOT NULL,
|
||||
@ -87,7 +86,7 @@ db.serialize(() => {
|
||||
`);
|
||||
|
||||
// Tasks table
|
||||
db.run(`
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS tasks (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
plant_id INTEGER,
|
||||
@ -105,38 +104,62 @@ db.serialize(() => {
|
||||
|
||||
// Insert sample data
|
||||
console.log('Inserting sample data...');
|
||||
|
||||
|
||||
// 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;
|
||||
db.run(plantSql, ['Local Nursery', 12, 'tree', 'Apple - Honeycrisp', 45.0, '2022-04-15', 180.0, 'good', null, null, 'Growing well, good fruit production']);
|
||||
db.run(plantSql, ['Garden Center', 8, 'shrub', 'Blueberry - Bluecrop', 25.0, '2023-03-20', 85.0, 'needs-attention', null, null, 'Leaves showing slight discoloration']);
|
||||
db.run(plantSql, ['Online Store', 3, 'herb', 'Basil - Sweet Genovese', 8.0, '2024-05-10', 35.0, 'good', null, null, 'Producing well, regular harvests']);
|
||||
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.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
|
||||
const taskSql = `INSERT INTO tasks (plant_id, title, description, deadline, completed) VALUES (?, ?, ?, ?, ?)`;
|
||||
db.run(taskSql, [1, 'Apply fertilizer', 'Apply spring fertilizer to apple trees', '2024-03-15', 0]);
|
||||
db.run(taskSql, [2, 'Prune blueberry bushes', 'Annual pruning before spring growth', '2024-02-28', 1]);
|
||||
db.run(taskSql, [3, 'Harvest basil', 'Regular harvest to encourage growth', '2024-06-01', 0]);
|
||||
const insertTask = db.prepare(`
|
||||
INSERT INTO tasks (plant_id, title, description, deadline, completed)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
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
|
||||
const maintenanceSql = `INSERT INTO maintenance_records (plant_id, date, type, description, amount) VALUES (?, ?, ?, ?, ?)`;
|
||||
db.run(maintenanceSql, [1, '2024-01-10', 'pruning', 'Winter pruning - removed dead branches', null]);
|
||||
db.run(maintenanceSql, [2, '2024-01-05', 'fertilizer', 'Applied organic compost', '2 cups']);
|
||||
db.run(maintenanceSql, [3, '2024-05-15', 'watering', 'Deep watering during dry spell', '1 gallon']);
|
||||
const insertMaintenance = db.prepare(`
|
||||
INSERT INTO maintenance_records (plant_id, date, type, description, amount)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
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
|
||||
const harvestSql = `INSERT INTO harvest_records (plant_id, date, quantity, unit, notes) VALUES (?, ?, ?, ?, ?)`;
|
||||
db.run(harvestSql, [1, '2023-09-15', 25, 'lbs', 'Excellent harvest, apples were sweet and crisp']);
|
||||
db.run(harvestSql, [2, '2023-07-20', 3, 'cups', 'First harvest of the season, berries were plump and sweet']);
|
||||
db.run(harvestSql, [3, '2024-05-25', 0.5, 'cups', 'Fresh basil for cooking, very aromatic']);
|
||||
const insertHarvest = db.prepare(`
|
||||
INSERT INTO harvest_records (plant_id, date, quantity, unit, notes)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
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!');
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Error initializing database:', err);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
db.close((err) => {
|
||||
if (err) {
|
||||
console.error('Error closing database:', err.message);
|
||||
} else {
|
||||
console.log('Database initialization completed!');
|
||||
}
|
||||
});
|
||||
db.close();
|
||||
console.log('Database initialization completed!');
|
1006
backend/server.js
1006
backend/server.js
File diff suppressed because it is too large
Load Diff
@ -27,18 +27,7 @@ services:
|
||||
volumes:
|
||||
- ./backend/database:/app/database
|
||||
- ./backend:/app
|
||||
command: ["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
|
||||
command: ["sh", "-c", "node scripts/init-db.js && node server.js"]
|
||||
|
||||
volumes:
|
||||
db_data:
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "vite-react-typescript-starter",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --mode development",
|
||||
|
30
src/App.tsx
30
src/App.tsx
@ -1,10 +1,11 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Sprout, Calendar, CheckSquare, Activity, Plus } from 'lucide-react';
|
||||
import { Plant, Task, MaintenanceRecord, HarvestRecord } from './types';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Sprout, Calendar, CheckSquare, Activity, Plus, BookOpen } from 'lucide-react';
|
||||
import { Plant, Task, MaintenanceRecord, HarvestRecord, PlantObservation } from './types';
|
||||
import Dashboard from './components/Dashboard';
|
||||
import PlantRegistry from './components/PlantRegistry';
|
||||
import TaskPlanner from './components/TaskPlanner';
|
||||
import MaintenanceLog from './components/MaintenanceLog';
|
||||
import ObservationJournal from './components/ObservationJournal';
|
||||
import { apiService } from './services/api';
|
||||
|
||||
function App() {
|
||||
@ -13,6 +14,7 @@ function App() {
|
||||
const [tasks, setTasks] = useState<Task[]>([]);
|
||||
const [maintenanceRecords, setMaintenanceRecords] = useState<MaintenanceRecord[]>([]);
|
||||
const [harvestRecords, setHarvestRecords] = useState<HarvestRecord[]>([]);
|
||||
const [observations, setObservations] = useState<PlantObservation[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
@ -26,17 +28,19 @@ function App() {
|
||||
// Test backend connection first
|
||||
await apiService.healthCheck();
|
||||
|
||||
const [plantsData, tasksData, maintenanceData, harvestData] = await Promise.all([
|
||||
const [plantsData, tasksData, maintenanceData, harvestData, observationsData] = await Promise.all([
|
||||
apiService.getPlants(),
|
||||
apiService.getTasks(),
|
||||
apiService.getMaintenanceRecords(),
|
||||
apiService.getHarvestRecords()
|
||||
apiService.getHarvestRecords(),
|
||||
apiService.getPlantObservations()
|
||||
]);
|
||||
|
||||
setPlants(plantsData);
|
||||
setTasks(tasksData);
|
||||
setMaintenanceRecords(maintenanceData);
|
||||
setHarvestRecords(harvestData);
|
||||
setObservations(observationsData);
|
||||
} catch (error) {
|
||||
console.error('Error loading data:', error);
|
||||
// You could add a toast notification or error state here
|
||||
@ -47,10 +51,11 @@ function App() {
|
||||
};
|
||||
|
||||
const navItems = [
|
||||
{ id: 'dashboard', label: 'Dashboard', icon: Activity },
|
||||
{ id: 'plants', label: 'Plant Registry', icon: Sprout },
|
||||
{ id: 'tasks', label: 'Task Planner', icon: CheckSquare },
|
||||
{ id: 'maintenance', label: 'Maintenance Log', icon: Calendar },
|
||||
{ id: 'dashboard', label: 'Панель', icon: Activity },
|
||||
{ id: 'plants', label: 'Реестр растений', icon: Sprout },
|
||||
{ id: 'tasks', label: 'План работ', icon: CheckSquare },
|
||||
{ id: 'maintenance', label: 'Журнал', icon: Calendar },
|
||||
{ id: 'observations', label: 'Дневник', icon: BookOpen },
|
||||
];
|
||||
|
||||
if (loading) {
|
||||
@ -129,6 +134,13 @@ function App() {
|
||||
onMaintenanceChange={setMaintenanceRecords}
|
||||
/>
|
||||
)}
|
||||
{activeTab === 'observations' && (
|
||||
<ObservationJournal
|
||||
observations={observations}
|
||||
plants={plants}
|
||||
onObservationsChange={setObservations}
|
||||
/>
|
||||
)}
|
||||
</main>
|
||||
|
||||
{/* Mobile Navigation */}
|
||||
|
@ -30,28 +30,28 @@ const Dashboard: React.FC<DashboardProps> = ({
|
||||
|
||||
const stats = [
|
||||
{
|
||||
label: 'Total Plants',
|
||||
label: 'Всего растений',
|
||||
value: plants.length,
|
||||
icon: Sprout,
|
||||
color: 'text-green-600',
|
||||
bgColor: 'bg-green-100'
|
||||
},
|
||||
{
|
||||
label: 'Pending Tasks',
|
||||
label: 'Отложенные работы',
|
||||
value: tasks.filter(task => !task.completed).length,
|
||||
icon: Calendar,
|
||||
color: 'text-blue-600',
|
||||
bgColor: 'bg-blue-100'
|
||||
},
|
||||
{
|
||||
label: 'Need Attention',
|
||||
label: 'Требует внимания',
|
||||
value: plantsNeedingAttention.length,
|
||||
icon: AlertTriangle,
|
||||
color: 'text-yellow-600',
|
||||
bgColor: 'bg-yellow-100'
|
||||
},
|
||||
{
|
||||
label: 'This Year Harvests',
|
||||
label: ' Урожай этого года',
|
||||
value: harvestRecords.filter(record =>
|
||||
new Date(record.date).getFullYear() === new Date().getFullYear()
|
||||
).length,
|
||||
@ -101,12 +101,12 @@ const Dashboard: React.FC<DashboardProps> = ({
|
||||
<div className="bg-white rounded-lg shadow-sm border border-green-100">
|
||||
<div className="p-6 border-b border-green-100">
|
||||
<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
|
||||
onClick={() => onNavigate('tasks')}
|
||||
className="text-sm text-green-600 hover:text-green-700 transition-colors"
|
||||
>
|
||||
View all →
|
||||
Посмотреть все →
|
||||
</button>
|
||||
</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 ${isOverdue ? 'text-red-600' : 'text-gray-500'}`}>
|
||||
Due: {new Date(task.deadline).toLocaleDateString()}
|
||||
Срок: {new Date(task.deadline).toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -133,7 +133,7 @@ const Dashboard: React.FC<DashboardProps> = ({
|
||||
})}
|
||||
</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>
|
||||
@ -142,12 +142,12 @@ const Dashboard: React.FC<DashboardProps> = ({
|
||||
<div className="bg-white rounded-lg shadow-sm border border-green-100">
|
||||
<div className="p-6 border-b border-green-100">
|
||||
<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
|
||||
onClick={() => onNavigate('plants')}
|
||||
className="text-sm text-green-600 hover:text-green-700 transition-colors"
|
||||
>
|
||||
View all →
|
||||
Посмотреть все →
|
||||
</button>
|
||||
</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="p-6 border-b border-green-100">
|
||||
<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
|
||||
onClick={() => onNavigate('maintenance')}
|
||||
className="text-sm text-green-600 hover:text-green-700 transition-colors"
|
||||
>
|
||||
View all →
|
||||
Посмотреть все →
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -209,7 +209,7 @@ const Dashboard: React.FC<DashboardProps> = ({
|
||||
})}
|
||||
</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>
|
||||
|
@ -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="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">
|
||||
<h3 className="text-lg font-semibold text-gray-900">Log Maintenance Activity</h3>
|
||||
<h3 className="text-lg font-semibold text-gray-900">Выполненная работа</h3>
|
||||
<button
|
||||
onClick={onCancel}
|
||||
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">
|
||||
<div>
|
||||
<label htmlFor="plantId" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Plant *
|
||||
Растение *
|
||||
</label>
|
||||
<select
|
||||
id="plantId"
|
||||
@ -64,7 +64,7 @@ const MaintenanceForm: React.FC<MaintenanceFormProps> = ({ plants, onSave, onCan
|
||||
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>
|
||||
<option value="">Выбор растения</option>
|
||||
{plants.map(plant => (
|
||||
<option key={plant.id} value={plant.id}>
|
||||
{plant.variety} ({plant.type})
|
||||
@ -75,7 +75,7 @@ const MaintenanceForm: React.FC<MaintenanceFormProps> = ({ plants, onSave, onCan
|
||||
|
||||
<div>
|
||||
<label htmlFor="date" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Date *
|
||||
Дата *
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
@ -90,7 +90,7 @@ const MaintenanceForm: React.FC<MaintenanceFormProps> = ({ plants, onSave, onCan
|
||||
|
||||
<div>
|
||||
<label htmlFor="type" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Activity Type *
|
||||
Вид деятельности *
|
||||
</label>
|
||||
<select
|
||||
id="type"
|
||||
@ -100,25 +100,25 @@ const MaintenanceForm: React.FC<MaintenanceFormProps> = ({ plants, onSave, onCan
|
||||
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="other">Other</option>
|
||||
<option value="chemical">Chemical Treatment</option>
|
||||
<option value="fertilizer">Fertilizer Application</option>
|
||||
<option value="watering">Watering</option>
|
||||
<option value="pruning">Pruning</option>
|
||||
<option value="transplanting">Transplanting</option>
|
||||
<option value="other">Другое</option>
|
||||
<option value="chemical">Обработка химикатами</option>
|
||||
<option value="fertilizer">Подкормка удобрением</option>
|
||||
<option value="watering">Полив</option>
|
||||
<option value="pruning">Обрезка</option>
|
||||
<option value="transplanting">Пересадка</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="description" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Description *
|
||||
Описание *
|
||||
</label>
|
||||
<textarea
|
||||
id="description"
|
||||
name="description"
|
||||
value={formData.description}
|
||||
onChange={handleChange}
|
||||
placeholder="Describe the maintenance activity..."
|
||||
placeholder="Опишите мероприятия по работам..."
|
||||
required
|
||||
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"
|
||||
@ -127,7 +127,7 @@ const MaintenanceForm: React.FC<MaintenanceFormProps> = ({ plants, onSave, onCan
|
||||
|
||||
<div>
|
||||
<label htmlFor="amount" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Amount/Quantity (Optional)
|
||||
Количество (необязательно)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
@ -135,7 +135,7 @@ const MaintenanceForm: React.FC<MaintenanceFormProps> = ({ plants, onSave, onCan
|
||||
name="amount"
|
||||
value={formData.amount}
|
||||
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"
|
||||
/>
|
||||
</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"
|
||||
/>
|
||||
<label htmlFor="isPlanned" className="ml-2 block text-sm text-gray-700">
|
||||
Planned Activity
|
||||
Запланированная работа
|
||||
</label>
|
||||
</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"
|
||||
/>
|
||||
<label htmlFor="isCompleted" className="ml-2 block text-sm text-gray-700">
|
||||
Completed
|
||||
Завершена
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@ -176,13 +176,13 @@ const MaintenanceForm: React.FC<MaintenanceFormProps> = ({ plants, onSave, onCan
|
||||
onClick={onCancel}
|
||||
className="px-4 py-2 text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-md transition-colors"
|
||||
>
|
||||
Cancel
|
||||
Отмена
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-md transition-colors"
|
||||
>
|
||||
Log Activity
|
||||
Выполнено
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
@ -66,15 +66,15 @@ const MaintenanceLog: React.FC<MaintenanceLogProps> = ({
|
||||
<div className="space-y-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<h2 className="text-3xl font-bold text-green-800">Maintenance Log</h2>
|
||||
<p className="text-green-600 mt-1">Track all your garden maintenance activities</p>
|
||||
<h2 className="text-3xl font-bold text-green-800">Журнал выполненных работ</h2>
|
||||
<p className="text-green-600 mt-1">Отслеживайте все ваши мероприятия по уходу за садом</p>
|
||||
</div>
|
||||
<button
|
||||
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"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
<span>Log Activity</span>
|
||||
<span>Запись</span>
|
||||
</button>
|
||||
</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="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<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">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search maintenance records..."
|
||||
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"
|
||||
@ -96,30 +96,30 @@ const MaintenanceLog: React.FC<MaintenanceLogProps> = ({
|
||||
</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
|
||||
value={filterType}
|
||||
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"
|
||||
>
|
||||
<option value="all">All Types</option>
|
||||
<option value="chemical">Chemical Treatment</option>
|
||||
<option value="fertilizer">Fertilizer</option>
|
||||
<option value="watering">Watering</option>
|
||||
<option value="pruning">Pruning</option>
|
||||
<option value="transplanting">Transplanting</option>
|
||||
<option value="all">Все типы</option>
|
||||
<option value="chemical">Химическая обработка</option>
|
||||
<option value="fertilizer">Удобрение</option>
|
||||
<option value="watering">Полив</option>
|
||||
<option value="pruning">Обрезка</option>
|
||||
<option value="transplanting">Пересадка</option>
|
||||
<option value="other">Other</option>
|
||||
</select>
|
||||
</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
|
||||
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">All Plants</option>
|
||||
<option value="all">Все растения</option>
|
||||
{plants.map(plant => (
|
||||
<option key={plant.id} value={plant.id}>
|
||||
{plant.variety}
|
||||
@ -178,12 +178,12 @@ const MaintenanceLog: React.FC<MaintenanceLogProps> = ({
|
||||
|
||||
{filteredRecords.length === 0 && (
|
||||
<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
|
||||
onClick={() => setShowMaintenanceForm(true)}
|
||||
className="mt-4 text-green-600 hover:text-green-700 transition-colors"
|
||||
>
|
||||
Log your first activity →
|
||||
Запишите свое первое действие в журнал →
|
||||
</button>
|
||||
</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="flex justify-between items-center p-6 border-b border-gray-200">
|
||||
<h3 className="text-lg font-semibold text-gray-900">
|
||||
{plant ? 'Edit Plant' : 'Add New Plant'}
|
||||
{plant ? 'Редактирование данных' : 'Новое растение'}
|
||||
</h3>
|
||||
<button
|
||||
onClick={onCancel}
|
||||
@ -73,7 +73,7 @@ const PlantForm: React.FC<PlantFormProps> = ({ plant, onSave, onCancel }) => {
|
||||
<form onSubmit={handleSubmit} className="p-6 space-y-4">
|
||||
<div>
|
||||
<label htmlFor="type" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Plant Type *
|
||||
Тип растения *
|
||||
</label>
|
||||
<select
|
||||
id="type"
|
||||
@ -83,21 +83,21 @@ const PlantForm: React.FC<PlantFormProps> = ({ plant, onSave, onCancel }) => {
|
||||
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 type</option>
|
||||
<option value="tree">Tree</option>
|
||||
<option value="shrub">Shrub</option>
|
||||
<option value="herb">Herb</option>
|
||||
<option value="vegetable">Vegetable</option>
|
||||
<option value="flower">Flower</option>
|
||||
<option value="vine">Vine</option>
|
||||
<option value="grass">Grass</option>
|
||||
<option value="other">Other</option>
|
||||
<option value="">Выбор типа</option>
|
||||
<option value="tree">Дерево</option>
|
||||
<option value="shrub">Кустарник</option>
|
||||
<option value="herb">Зелень</option>
|
||||
<option value="vegetable">Овощ</option>
|
||||
<option value="flower">Цветы</option>
|
||||
<option value="vine">Виноградная лоза</option>
|
||||
<option value="grass">Трава</option>
|
||||
<option value="other">Другое</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="variety" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Variety/Species *
|
||||
Вид/Сорт *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
@ -113,7 +113,7 @@ const PlantForm: React.FC<PlantFormProps> = ({ plant, onSave, onCancel }) => {
|
||||
|
||||
<div>
|
||||
<label htmlFor="purchaseLocation" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Purchase Location *
|
||||
Место покупки *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
@ -130,7 +130,7 @@ const PlantForm: React.FC<PlantFormProps> = ({ plant, onSave, onCancel }) => {
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label htmlFor="seedlingAge" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Seedling Age (months) *
|
||||
Возраст саженца (месяцы) *
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
@ -146,7 +146,7 @@ const PlantForm: React.FC<PlantFormProps> = ({ plant, onSave, onCancel }) => {
|
||||
|
||||
<div>
|
||||
<label htmlFor="seedlingHeight" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Seedling Height (cm) *
|
||||
Высота саженца (см) *
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
@ -163,7 +163,7 @@ const PlantForm: React.FC<PlantFormProps> = ({ plant, onSave, onCancel }) => {
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="plantingDate" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Planting Date *
|
||||
Дата посадки *
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
@ -178,7 +178,7 @@ const PlantForm: React.FC<PlantFormProps> = ({ plant, onSave, onCancel }) => {
|
||||
|
||||
<div>
|
||||
<label htmlFor="currentHeight" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Current Height (cm)
|
||||
Текущая высота (см)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
@ -193,7 +193,7 @@ const PlantForm: React.FC<PlantFormProps> = ({ plant, onSave, onCancel }) => {
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="healthStatus" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Health Status
|
||||
Состояние
|
||||
</label>
|
||||
<select
|
||||
id="healthStatus"
|
||||
@ -202,15 +202,15 @@ const PlantForm: React.FC<PlantFormProps> = ({ plant, onSave, onCancel }) => {
|
||||
onChange={handleChange}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-transparent"
|
||||
>
|
||||
<option value="good">Good</option>
|
||||
<option value="needs-attention">Needs Attention</option>
|
||||
<option value="dead">Dead</option>
|
||||
<option value="good">Хорошее</option>
|
||||
<option value="needs-attention">Требует внимания</option>
|
||||
<option value="dead">Погибло</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="photoUrl" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Photo URL
|
||||
Фотография
|
||||
</label>
|
||||
<input
|
||||
type="url"
|
||||
@ -224,7 +224,7 @@ const PlantForm: React.FC<PlantFormProps> = ({ plant, onSave, onCancel }) => {
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="notes" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Special Notes
|
||||
Заметки
|
||||
</label>
|
||||
<textarea
|
||||
id="notes"
|
||||
@ -243,13 +243,13 @@ const PlantForm: React.FC<PlantFormProps> = ({ plant, onSave, onCancel }) => {
|
||||
onClick={onCancel}
|
||||
className="px-4 py-2 text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-md transition-colors"
|
||||
>
|
||||
Cancel
|
||||
Отмена
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
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>
|
||||
</div>
|
||||
</form>
|
||||
|
@ -97,15 +97,15 @@ const PlantRegistry: React.FC<PlantRegistryProps> = ({
|
||||
<div className="space-y-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<h2 className="text-3xl font-bold text-green-800">Plant Registry</h2>
|
||||
<p className="text-green-600 mt-1">Manage your garden plants and track their progress</p>
|
||||
<h2 className="text-3xl font-bold text-green-800">Рестр растений</h2>
|
||||
<p className="text-green-600 mt-1">Управляйте своими садовыми растениями и следите за их развитием</p>
|
||||
</div>
|
||||
<button
|
||||
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"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
<span>Add Plant</span>
|
||||
<span>Новое растение</span>
|
||||
</button>
|
||||
</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="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<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">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search by variety or type..."
|
||||
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"
|
||||
@ -126,29 +126,29 @@ const PlantRegistry: React.FC<PlantRegistryProps> = ({
|
||||
</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
|
||||
value={filterType}
|
||||
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"
|
||||
>
|
||||
<option value="all">All Types</option>
|
||||
<option value="all">Все виды</option>
|
||||
{plantTypes.map(type => (
|
||||
<option key={type} value={type} className="capitalize">{type}</option>
|
||||
))}
|
||||
</select>
|
||||
</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
|
||||
value={filterStatus}
|
||||
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"
|
||||
>
|
||||
<option value="all">All Status</option>
|
||||
<option value="good">Good</option>
|
||||
<option value="needs-attention">Needs Attention</option>
|
||||
<option value="dead">Dead</option>
|
||||
<option value="all">Все состояния</option>
|
||||
<option value="good">Хорошее</option>
|
||||
<option value="needs-attention">Требует внимания</option>
|
||||
<option value="dead">Погибло</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@ -169,7 +169,7 @@ const PlantRegistry: React.FC<PlantRegistryProps> = ({
|
||||
<div>
|
||||
<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-xs text-gray-500">From: {plant.purchaseLocation}</p>
|
||||
<p className="text-xs text-gray-500">Куплено в: {plant.purchaseLocation}</p>
|
||||
</div>
|
||||
<div className="flex space-x-2">
|
||||
<button
|
||||
@ -178,7 +178,7 @@ const PlantRegistry: React.FC<PlantRegistryProps> = ({
|
||||
setShowStatistics(true);
|
||||
}}
|
||||
className="p-1 text-purple-600 hover:text-purple-700 transition-colors"
|
||||
title="View Statistics"
|
||||
title="Посмотреть Статистику"
|
||||
>
|
||||
<BarChart3 className="h-4 w-4" />
|
||||
</button>
|
||||
@ -227,23 +227,23 @@ const PlantRegistry: React.FC<PlantRegistryProps> = ({
|
||||
<div className="flex items-center space-x-2">
|
||||
<Calendar className="h-4 w-4 text-gray-400" />
|
||||
<span className="text-sm text-gray-600">
|
||||
Planted: {new Date(plant.plantingDate).toLocaleDateString()}
|
||||
Посажено: {new Date(plant.plantingDate).toLocaleDateString()}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-2 text-sm text-gray-600">
|
||||
<div>Age: {plant.seedlingAge} months</div>
|
||||
<div>Initial: {plant.seedlingHeight}cm</div>
|
||||
<div>Возраст: {plant.seedlingAge} months</div>
|
||||
<div>Начальный: {plant.seedlingHeight}cm</div>
|
||||
{plant.currentHeight && (
|
||||
<>
|
||||
<div>Current: {plant.currentHeight}cm</div>
|
||||
<div>Growth: +{(plant.currentHeight - plant.seedlingHeight).toFixed(1)}cm</div>
|
||||
<div>Текущий: {plant.currentHeight}cm</div>
|
||||
<div>Прирост: +{(plant.currentHeight - plant.seedlingHeight).toFixed(1)}cm</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<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)}`}>
|
||||
{plant.healthStatus.replace('-', ' ')}
|
||||
</span>
|
||||
@ -251,7 +251,7 @@ const PlantRegistry: React.FC<PlantRegistryProps> = ({
|
||||
|
||||
{lastHarvest && (
|
||||
<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">
|
||||
{lastHarvest.quantity} {lastHarvest.unit} on {new Date(lastHarvest.date).toLocaleDateString()}
|
||||
</p>
|
||||
@ -277,7 +277,7 @@ const PlantRegistry: React.FC<PlantRegistryProps> = ({
|
||||
onClick={() => setShowPlantForm(true)}
|
||||
className="mt-4 text-green-600 hover:text-green-700 transition-colors"
|
||||
>
|
||||
Add your first plant →
|
||||
Добавьте первое растение →
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
@ -77,7 +77,7 @@ const PlantStatisticsComponent: React.FC<PlantStatisticsProps> = ({
|
||||
<h3 className="text-xl font-semibold text-gray-900">
|
||||
{plant.variety} - Statistics
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600">Year: {selectedYear}</p>
|
||||
<p className="text-sm text-gray-600">Год: {selectedYear}</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
@ -91,7 +91,7 @@ const PlantStatisticsComponent: React.FC<PlantStatisticsProps> = ({
|
||||
{/* Year Selector */}
|
||||
<div className="mb-6">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Select Year
|
||||
Выбор года
|
||||
</label>
|
||||
<select
|
||||
value={selectedYear}
|
||||
@ -109,7 +109,7 @@ const PlantStatisticsComponent: React.FC<PlantStatisticsProps> = ({
|
||||
<div className="bg-green-50 p-4 rounded-lg">
|
||||
<div className="flex items-center justify-between">
|
||||
<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">
|
||||
{statistics.totalHarvest.toFixed(1)}
|
||||
</p>
|
||||
@ -121,7 +121,7 @@ const PlantStatisticsComponent: React.FC<PlantStatisticsProps> = ({
|
||||
<div className="bg-blue-50 p-4 rounded-lg">
|
||||
<div className="flex items-center justify-between">
|
||||
<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">
|
||||
+{statistics.heightGrowth.toFixed(1)}cm
|
||||
</p>
|
||||
@ -133,7 +133,7 @@ const PlantStatisticsComponent: React.FC<PlantStatisticsProps> = ({
|
||||
<div className="bg-purple-50 p-4 rounded-lg">
|
||||
<div className="flex items-center justify-between">
|
||||
<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">
|
||||
{statistics.harvestDates.length}
|
||||
</p>
|
||||
@ -145,7 +145,7 @@ const PlantStatisticsComponent: React.FC<PlantStatisticsProps> = ({
|
||||
<div className="bg-orange-50 p-4 rounded-lg">
|
||||
<div className="flex items-center justify-between">
|
||||
<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">
|
||||
{Object.values(statistics.maintenanceCount).reduce((a, b) => a + b, 0)}
|
||||
</p>
|
||||
@ -157,23 +157,23 @@ const PlantStatisticsComponent: React.FC<PlantStatisticsProps> = ({
|
||||
|
||||
{/* Maintenance Breakdown */}
|
||||
<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="text-center">
|
||||
<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 className="text-center">
|
||||
<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 className="text-center">
|
||||
<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 className="text-center">
|
||||
<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>
|
||||
|
@ -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="flex justify-between items-center p-6 border-b border-gray-200">
|
||||
<h3 className="text-lg font-semibold text-gray-900">
|
||||
{task ? 'Edit Task' : 'Create New Task'}
|
||||
{task ? 'Редактирование данных работы' : 'Добавление новой работы'}
|
||||
</h3>
|
||||
<button
|
||||
onClick={onCancel}
|
||||
@ -64,7 +64,7 @@ const TaskForm: React.FC<TaskFormProps> = ({ task, plants, onSave, onCancel }) =
|
||||
<form onSubmit={handleSubmit} className="p-6 space-y-4">
|
||||
<div>
|
||||
<label htmlFor="title" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Task Title *
|
||||
Наименование работы *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
@ -72,7 +72,7 @@ const TaskForm: React.FC<TaskFormProps> = ({ task, plants, onSave, onCancel }) =
|
||||
name="title"
|
||||
value={formData.title}
|
||||
onChange={handleChange}
|
||||
placeholder="e.g., Apply fertilizer"
|
||||
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"
|
||||
/>
|
||||
@ -80,7 +80,7 @@ const TaskForm: React.FC<TaskFormProps> = ({ task, plants, onSave, onCancel }) =
|
||||
|
||||
<div>
|
||||
<label htmlFor="plantId" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Associated Plant (Optional)
|
||||
Растение (опционально)
|
||||
</label>
|
||||
<select
|
||||
id="plantId"
|
||||
@ -89,7 +89,7 @@ const TaskForm: React.FC<TaskFormProps> = ({ task, plants, onSave, onCancel }) =
|
||||
onChange={handleChange}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-transparent"
|
||||
>
|
||||
<option value="">Select a plant (optional)</option>
|
||||
<option value="">Выбор растения (опционально)</option>
|
||||
{plants.map(plant => (
|
||||
<option key={plant.id} value={plant.id}>
|
||||
{plant.variety} ({plant.type})
|
||||
@ -100,7 +100,7 @@ const TaskForm: React.FC<TaskFormProps> = ({ task, plants, onSave, onCancel }) =
|
||||
|
||||
<div>
|
||||
<label htmlFor="deadline" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Deadline *
|
||||
Срок исполнения *
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
@ -115,14 +115,14 @@ const TaskForm: React.FC<TaskFormProps> = ({ task, plants, onSave, onCancel }) =
|
||||
|
||||
<div>
|
||||
<label htmlFor="description" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Description
|
||||
Описание
|
||||
</label>
|
||||
<textarea
|
||||
id="description"
|
||||
name="description"
|
||||
value={formData.description}
|
||||
onChange={handleChange}
|
||||
placeholder="Detailed description of the task..."
|
||||
placeholder="Детальное описание работы..."
|
||||
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"
|
||||
/>
|
||||
@ -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"
|
||||
/>
|
||||
<label htmlFor="completed" className="ml-2 block text-sm text-gray-700">
|
||||
Mark as completed
|
||||
Отметить как выполненное
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
@ -150,13 +150,13 @@ const TaskForm: React.FC<TaskFormProps> = ({ task, plants, onSave, onCancel }) =
|
||||
onClick={onCancel}
|
||||
className="px-4 py-2 text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-md transition-colors"
|
||||
>
|
||||
Cancel
|
||||
Отмена
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
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>
|
||||
</div>
|
||||
</form>
|
||||
|
@ -88,15 +88,15 @@ const TaskPlanner: React.FC<TaskPlannerProps> = ({ tasks, plants, onTasksChange
|
||||
<div className="space-y-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<h2 className="text-3xl font-bold text-green-800">Task Planner</h2>
|
||||
<p className="text-green-600 mt-1">Plan and track your garden maintenance tasks</p>
|
||||
<h2 className="text-3xl font-bold text-green-800">Планирование работ</h2>
|
||||
<p className="text-green-600 mt-1">Планируйте и отслеживайте свои работы по уходу за садом</p>
|
||||
</div>
|
||||
<button
|
||||
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"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
<span>Add Task</span>
|
||||
<span>Новая работа</span>
|
||||
</button>
|
||||
</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="flex border-b border-green-100">
|
||||
{[
|
||||
{ key: 'all', label: 'All Tasks', count: tasks.length },
|
||||
{ key: 'pending', label: 'Pending', count: tasks.filter(t => !t.completed).length },
|
||||
{ key: 'completed', label: 'Completed', count: tasks.filter(t => t.completed).length }
|
||||
{ key: 'all', label: 'Все работы', count: tasks.length },
|
||||
{ key: 'pending', label: 'Отложено', count: tasks.filter(t => !t.completed).length },
|
||||
{ key: 'completed', label: 'Выполнено', count: tasks.filter(t => t.completed).length }
|
||||
].map((tab) => (
|
||||
<button
|
||||
key={tab.key}
|
||||
@ -165,7 +165,7 @@ const TaskPlanner: React.FC<TaskPlannerProps> = ({ tasks, plants, onTasksChange
|
||||
|
||||
{plant && (
|
||||
<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>
|
||||
)}
|
||||
|
||||
@ -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-1">
|
||||
<Calendar className="h-4 w-4" />
|
||||
<span>Due: {new Date(task.deadline).toLocaleDateString()}</span>
|
||||
<span>Срок: {new Date(task.deadline).toLocaleDateString()}</span>
|
||||
</div>
|
||||
{new Date(task.deadline) < new Date() && !task.completed && (
|
||||
<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 {
|
||||
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
|
||||
async healthCheck(): Promise<{ status: string; message: string }> {
|
||||
return this.makeRequest<{ status: string; message: string }>('/health');
|
||||
|
@ -28,6 +28,20 @@ export interface PlantHistory {
|
||||
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 {
|
||||
id: number;
|
||||
plantId: number;
|
||||
|
Loading…
x
Reference in New Issue
Block a user