360 lines
21 KiB
PHP
360 lines
21 KiB
PHP
<?php
|
|
$current_project = 'plants';
|
|
require_once __DIR__ . '/../auth.php';
|
|
|
|
session_start();
|
|
require_once 'db.php';
|
|
require_once __DIR__ . '/vendor/autoload.php';
|
|
|
|
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
|
|
$dotenv->load();
|
|
|
|
if (!isset($_SESSION['user_id'])) {
|
|
http_response_code(401);
|
|
echo json_encode(['error' => 'Benutzer nicht authentifiziert.']);
|
|
exit;
|
|
}
|
|
$currentUserId = $_SESSION['user_id'];
|
|
|
|
// === HELPER FUNCTIONS ===
|
|
function callGeminiAPI(array $payload, string $apiKey): array {
|
|
$googleApiUrl = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent';
|
|
$fullApiUrl = $googleApiUrl . '?key=' . $apiKey;
|
|
$ch = curl_init();
|
|
curl_setopt_array($ch, [
|
|
CURLOPT_URL => $fullApiUrl,
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
CURLOPT_POST => true,
|
|
CURLOPT_POSTFIELDS => json_encode($payload),
|
|
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
|
|
CURLOPT_TIMEOUT => 90,
|
|
]);
|
|
$response_body = curl_exec($ch);
|
|
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
if (curl_errno($ch) || $http_code >= 400) {
|
|
$error_message = curl_errno($ch) ? curl_error($ch) : 'KI-API-Fehler mit Status ' . $http_code . ' - ' . $response_body;
|
|
curl_close($ch);
|
|
throw new Exception('Fehler bei der Kommunikation mit der KI: ' . $error_message);
|
|
}
|
|
curl_close($ch);
|
|
return json_decode($response_body, true);
|
|
}
|
|
|
|
function processUploadedPhoto(array $fileInfo, int $targetWidth = 800): ?string {
|
|
if ($fileInfo['error'] !== UPLOAD_ERR_OK) return null;
|
|
$allowedMimeTypes = ['image/jpeg', 'image/png', 'image/webp'];
|
|
if (!in_array(mime_content_type($fileInfo['tmp_name']), $allowedMimeTypes) || $fileInfo['size'] > (10 * 1024 * 1024)) return null;
|
|
$uploadDir = 'uploads/';
|
|
if (!is_dir($uploadDir)) mkdir($uploadDir, 0755, true);
|
|
$fileName = uniqid() . '-' . pathinfo($fileInfo['name'], PATHINFO_FILENAME) . '.webp';
|
|
$targetFilePath = $uploadDir . $fileName;
|
|
$sourceImage = null;
|
|
switch (exif_imagetype($fileInfo['tmp_name'])) {
|
|
case IMAGETYPE_JPEG: $sourceImage = imagecreatefromjpeg($fileInfo['tmp_name']); break;
|
|
case IMAGETYPE_PNG: $sourceImage = imagecreatefrompng($fileInfo['tmp_name']); break;
|
|
case IMAGETYPE_WEBP: $sourceImage = imagecreatefromwebp($fileInfo['tmp_name']); break;
|
|
}
|
|
if (!$sourceImage) return null;
|
|
$resizedImage = (imagesx($sourceImage) > $targetWidth) ? imagescale($sourceImage, $targetWidth) : $sourceImage;
|
|
imagewebp($resizedImage, $targetFilePath, 85);
|
|
imagedestroy($sourceImage);
|
|
if ($resizedImage !== $sourceImage) imagedestroy($resizedImage);
|
|
return $targetFilePath;
|
|
}
|
|
|
|
function getPlantWithDueDates($pdo, $plantId, $userId) {
|
|
$stmt = $pdo->prepare("SELECT p.id, p.name, p.latin_name, p.photo, p.last_watered, p.last_fertilized, p.watering_interval_days, p.fertilizing_interval_days, p.care_tips FROM plants p WHERE p.id = ? AND p.user_id = ?");
|
|
$stmt->execute([$plantId, $userId]);
|
|
$plant = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
if ($plant) {
|
|
$plant['watering_interval'] = $plant['watering_interval_days'];
|
|
$plant['fertilizing_interval'] = $plant['fertilizing_interval_days'];
|
|
$isValidDate = fn($dateStr) => !empty($dateStr) && $dateStr !== '0000-00-00';
|
|
if ((int)$plant['watering_interval'] > 0 && $isValidDate($plant['last_watered'])) {
|
|
$plant['next_watering_due'] = (new DateTime($plant['last_watered']))->add(new DateInterval('P' . $plant['watering_interval'] . 'D'))->format('Y-m-d');
|
|
} else { $plant['next_watering_due'] = null; }
|
|
if ((int)$plant['fertilizing_interval'] > 0 && $isValidDate($plant['last_fertilized'])) {
|
|
$plant['next_fertilizing_due'] = (new DateTime($plant['last_fertilized']))->add(new DateInterval('P' . $plant['fertilizing_interval'] . 'D'))->format('Y-m-d');
|
|
} else { $plant['next_fertilizing_due'] = null; }
|
|
}
|
|
return $plant;
|
|
}
|
|
|
|
// === CREDIT MANAGEMENT ===
|
|
try {
|
|
$stmt = $pdo->prepare("SELECT ai_credits_last_reset FROM users WHERE id = ?");
|
|
$stmt->execute([$currentUserId]);
|
|
$lastReset = $stmt->fetchColumn();
|
|
$now = new DateTime();
|
|
if ($lastReset === null || (new DateTime($lastReset))->format('Y-m') < $now->format('Y-m')) {
|
|
$stmt = $pdo->prepare("UPDATE users SET ai_credits = 100, ai_credits_last_reset = NOW() WHERE id = ?");
|
|
$stmt->execute([$currentUserId]);
|
|
}
|
|
} catch (Exception $e) { /* non-blocking */ }
|
|
|
|
// === API ROUTING ===
|
|
$method = $_SERVER['REQUEST_METHOD'];
|
|
$json_data = json_decode(file_get_contents('php://input'), true);
|
|
header('Content-Type: application/json');
|
|
|
|
try {
|
|
// --- Handle JSON-based POST requests FIRST ---
|
|
if ($method === 'POST' && !empty($json_data)) {
|
|
$request_type = $json_data['request_type'] ?? '';
|
|
switch ($request_type) {
|
|
case 'ai_identification':
|
|
case 'ai_enrichment':
|
|
$pdo->beginTransaction();
|
|
$stmt = $pdo->prepare("UPDATE users SET ai_credits = ai_credits - 1 WHERE id = ? AND ai_credits >= 1");
|
|
$stmt->execute([$currentUserId]);
|
|
if ($stmt->rowCount() === 0) throw new Exception('Nicht genügend KI-Credits (1 benötigt).', 402);
|
|
$response_data = callGeminiAPI($json_data['payload'], $_ENV['GEMINI_API_KEY']);
|
|
$pdo->commit();
|
|
echo json_encode($response_data);
|
|
break;
|
|
case 'update_user_settings':
|
|
$settings = $json_data['settings'] ?? [];
|
|
$allowedKeys = ['theme', 'allow_photo_sharing', 'auto_fill_watering', 'auto_fill_fertilizing', 'auto_fill_care_tips'];
|
|
$sqlParts = []; $params = [];
|
|
foreach ($settings as $key => $value) {
|
|
if (in_array($key, $allowedKeys)) {
|
|
$sqlParts[] = "$key = ?";
|
|
$params[] = is_bool($value) ? (int)$value : $value;
|
|
}
|
|
}
|
|
if (empty($sqlParts)) throw new Exception('Keine gültigen Einstellungsfelder.', 400);
|
|
$params[] = $currentUserId;
|
|
$stmt = $pdo->prepare("UPDATE users SET " . implode(', ', $sqlParts) . " WHERE id = ?");
|
|
$stmt->execute($params);
|
|
echo json_encode(['success' => true]);
|
|
break;
|
|
case 'care_action':
|
|
$plantId = $json_data['id'];
|
|
$column = ($json_data['action'] === 'water') ? 'last_watered' : 'last_fertilized';
|
|
$entryType = ($json_data['action'] === 'water') ? 'watered' : 'fertilized';
|
|
$pdo->beginTransaction();
|
|
$stmt = $pdo->prepare("UPDATE plants SET $column = CURDATE() WHERE id = ? AND user_id = ?");
|
|
$stmt->execute([$plantId, $currentUserId]);
|
|
$stmt = $pdo->prepare("INSERT INTO plant_diary (plant_id, user_id, entry_type) VALUES (?, ?, ?)");
|
|
$stmt->execute([$plantId, $currentUserId, $entryType]);
|
|
$pdo->commit();
|
|
echo json_encode(['success' => true, 'plant' => getPlantWithDueDates($pdo, $plantId, $currentUserId)]);
|
|
break;
|
|
case 'set_profile_picture':
|
|
$stmt = $pdo->prepare("UPDATE users SET profile_image = ? WHERE id = ?");
|
|
$stmt->execute([$json_data['photo_path'], $currentUserId]);
|
|
echo json_encode(['success' => true, 'new_image' => $json_data['photo_path']]);
|
|
break;
|
|
default:
|
|
throw new Exception('Unbekannter JSON-Request-Typ.', 400);
|
|
}
|
|
exit;
|
|
}
|
|
|
|
// --- Handle GET, DELETE, and multipart/form-data POST requests ---
|
|
switch ($method) {
|
|
case 'GET':
|
|
$action = $_GET['action'] ?? 'get_plants';
|
|
switch ($action) {
|
|
case 'get_plants':
|
|
$stmt = $pdo->prepare("SELECT id FROM plants WHERE user_id = ? ORDER BY id DESC");
|
|
$stmt->execute([$currentUserId]);
|
|
$plant_ids = $stmt->fetchAll(PDO::FETCH_COLUMN);
|
|
echo json_encode(array_map(fn($id) => getPlantWithDueDates($pdo, $id, $currentUserId), $plant_ids));
|
|
break;
|
|
case 'get_diary':
|
|
$stmt = $pdo->prepare("SELECT * FROM plant_diary WHERE plant_id = ? AND user_id = ? ORDER BY entry_date DESC");
|
|
$stmt->execute([$_GET['plant_id'], $currentUserId]);
|
|
echo json_encode($stmt->fetchAll());
|
|
break;
|
|
case 'get_user_settings':
|
|
$stmt = $pdo->prepare("SELECT username, email, profile_image, theme, allow_photo_sharing, ai_credits, auto_fill_watering, auto_fill_fertilizing, auto_fill_care_tips FROM users WHERE id = ?");
|
|
$stmt->execute([$currentUserId]);
|
|
$userSettings = $stmt->fetch();
|
|
$stmt_count = $pdo->prepare("SELECT COUNT(id) as plant_count FROM plants WHERE user_id = ?");
|
|
$stmt_count->execute([$currentUserId]);
|
|
$userSettings['plant_count'] = $stmt_count->fetchColumn();
|
|
echo json_encode($userSettings);
|
|
break;
|
|
case 'get_lexicon_entries':
|
|
$stmt = $pdo->prepare("SELECT pl.name, pl.latin_name, lp.photo_path as photo_url FROM plant_lexicon pl LEFT JOIN (SELECT plant_latin_name, photo_path, ROW_NUMBER() OVER(PARTITION BY plant_latin_name ORDER BY ai_photo_quality_score DESC, id DESC) as rn FROM lexicon_photos WHERE status = 'approved') lp ON pl.latin_name = lp.plant_latin_name AND lp.rn = 1 WHERE pl.entry_count > 0 ORDER BY pl.name ASC");
|
|
$stmt->execute();
|
|
echo json_encode($stmt->fetchAll());
|
|
break;
|
|
case 'get_lexicon_entry_details':
|
|
$latinName = $_GET['latin_name'] ?? '';
|
|
if (empty($latinName)) throw new Exception('Kein botanischer Name angegeben.', 400);
|
|
$stmt = $pdo->prepare("SELECT * FROM plant_lexicon WHERE latin_name = ?");
|
|
$stmt->execute([$latinName]);
|
|
$plantDetails = $stmt->fetch();
|
|
if (!$plantDetails) throw new Exception('Pflanze nicht im Lexikon gefunden.', 404);
|
|
$stmtPhotos = $pdo->prepare("SELECT photo_path FROM lexicon_photos WHERE plant_latin_name = ? AND status = 'approved' ORDER BY ai_photo_quality_score DESC, id DESC LIMIT 10");
|
|
$stmtPhotos->execute([$latinName]);
|
|
$plantDetails['photo_gallery'] = $stmtPhotos->fetchAll(PDO::FETCH_COLUMN);
|
|
echo json_encode($plantDetails);
|
|
break;
|
|
default:
|
|
throw new Exception('Ungültige GET-Aktion.', 400);
|
|
}
|
|
break;
|
|
|
|
case 'POST':
|
|
$action = $_POST['action'] ?? 'save_plant';
|
|
switch ($action) {
|
|
case 'add_diary_entry':
|
|
$photoPath = isset($_FILES['photo']) ? processUploadedPhoto($_FILES['photo'], 600) : null;
|
|
$stmt = $pdo->prepare("INSERT INTO plant_diary (plant_id, user_id, notes, photo, entry_type) VALUES (?, ?, ?, ?, ?)");
|
|
$stmt->execute([$_POST['plant_id'], $currentUserId, $_POST['notes'] ?? null, $photoPath, isset($_POST['is_milestone']) ? 'milestone' : 'manual']);
|
|
$newEntryId = $pdo->lastInsertId();
|
|
$stmt = $pdo->prepare("SELECT * FROM plant_diary WHERE id = ?");
|
|
$stmt->execute([$newEntryId]);
|
|
echo json_encode(['success' => true, 'entry' => $stmt->fetch()]);
|
|
break;
|
|
case 'save_plant':
|
|
$name = trim($_POST['name'] ?? '');
|
|
if (empty($name)) throw new Exception('Der Name der Pflanze darf nicht leer sein.', 400);
|
|
|
|
$plantId = $_POST['id'] ?? null;
|
|
$latinName = trim($_POST['latin_name'] ?? '') ?: null;
|
|
$photoPath = isset($_FILES['photo']) ? processUploadedPhoto($_FILES['photo']) : null;
|
|
$finalPhotoPath = $photoPath ?? $_POST['current_photo'] ?? null;
|
|
|
|
// ROBUSTE PRÜFUNG DER INTERVALLE
|
|
$wateringInterval = (isset($_POST['watering_interval']) && is_numeric($_POST['watering_interval'])) ? (int)$_POST['watering_interval'] : null;
|
|
$fertilizingInterval = (isset($_POST['fertilizing_interval']) && is_numeric($_POST['fertilizing_interval'])) ? (int)$_POST['fertilizing_interval'] : null;
|
|
|
|
$careTips = trim($_POST['care_tips'] ?? '') ?: null;
|
|
|
|
$pdo->beginTransaction();
|
|
|
|
$oldLatinName = null;
|
|
if ($plantId) { // Edit
|
|
$stmtOld = $pdo->prepare("SELECT latin_name FROM plants WHERE id = ? AND user_id = ?");
|
|
$stmtOld->execute([$plantId, $currentUserId]);
|
|
$oldLatinName = $stmtOld->fetchColumn();
|
|
|
|
$stmt = $pdo->prepare("UPDATE plants SET name = ?, latin_name = ?, photo = ?, watering_interval_days = ?, fertilizing_interval_days = ?, care_tips = ? WHERE id = ? AND user_id = ?");
|
|
$stmt->execute([$name, $latinName, $finalPhotoPath, $wateringInterval, $fertilizingInterval, $careTips, $plantId, $currentUserId]);
|
|
} else { // Add
|
|
$stmt = $pdo->prepare("INSERT INTO plants (user_id, name, latin_name, photo, last_watered, last_fertilized, watering_interval_days, fertilizing_interval_days, care_tips) VALUES (?, ?, ?, ?, CURDATE(), CURDATE(), ?, ?, ?)");
|
|
$stmt->execute([$currentUserId, $name, $latinName, $finalPhotoPath, $wateringInterval, $fertilizingInterval, $careTips]);
|
|
$plantId = $pdo->lastInsertId();
|
|
$stmt = $pdo->prepare("INSERT INTO plant_diary (plant_id, user_id, entry_type, notes) VALUES (?, ?, 'milestone', 'Pflanze hinzugefügt')");
|
|
$stmt->execute([$plantId, $currentUserId]);
|
|
}
|
|
|
|
// Handle lexicon entry count changes
|
|
if ($oldLatinName && $oldLatinName !== $latinName) {
|
|
$stmtDec = $pdo->prepare("UPDATE plant_lexicon SET entry_count = entry_count - 1 WHERE latin_name = ? AND entry_count > 0");
|
|
$stmtDec->execute([$oldLatinName]);
|
|
}
|
|
if ($latinName && (!$oldLatinName || $oldLatinName !== $latinName)) {
|
|
$stmt = $pdo->prepare("INSERT INTO plant_lexicon (latin_name, name) VALUES (?, ?) ON DUPLICATE KEY UPDATE entry_count = entry_count + 1, name = VALUES(name)");
|
|
$stmt->execute([$latinName, $name]);
|
|
}
|
|
|
|
// Process AI data for lexicon if available
|
|
$enrichmentData = json_decode($_POST['ai_enrichment_json'] ?? '{}', true);
|
|
|
|
if(!empty($enrichmentData)) {
|
|
$stmt = $pdo->prepare("UPDATE plant_lexicon SET plant_family = :plant_family, plant_genus = :plant_genus, description_full = :description_full, care_tips = :care_tips, watering_interval_days = :watering_interval_days, fertilizing_interval_days = :fertilizing_interval_days, light_requirement = :light_requirement, water_requirement = :water_requirement, humidity_requirement = :humidity_requirement, care_difficulty = :care_difficulty, growth_habit = :growth_habit, mature_size = :mature_size, toxicity = :toxicity, edibility = :edibility, temperature_preference = :temperature_preference, flowering_period = :flowering_period, substrate_recommendation = :substrate_recommendation, native_to = :native_to, tags = :tags, updated_at = NOW() WHERE latin_name = :latin_name AND description_full IS NULL");
|
|
$stmt->execute([
|
|
'plant_family' => $enrichmentData['plant_family'] ?? null,
|
|
'plant_genus' => $enrichmentData['plant_genus'] ?? null,
|
|
'description_full' => $enrichmentData['description_full'] ?? null,
|
|
'care_tips' => $enrichmentData['care_tips'] ?? null,
|
|
'watering_interval_days' => $enrichmentData['watering_interval_days'] ?? null,
|
|
'fertilizing_interval_days' => $enrichmentData['fertilizing_interval_days'] ?? null,
|
|
'light_requirement' => $enrichmentData['light_requirement'] ?? null,
|
|
'water_requirement' => $enrichmentData['water_requirement'] ?? null,
|
|
'humidity_requirement' => $enrichmentData['humidity_requirement'] ?? null,
|
|
'care_difficulty' => $enrichmentData['care_difficulty'] ?? null,
|
|
'growth_habit' => $enrichmentData['growth_habit'] ?? null,
|
|
'mature_size' => $enrichmentData['mature_size'] ?? null,
|
|
'toxicity' => $enrichmentData['toxicity'] ?? null,
|
|
'edibility' => $enrichmentData['edibility'] ?? null,
|
|
'temperature_preference' => $enrichmentData['temperature_preference'] ?? null,
|
|
'flowering_period' => $enrichmentData['flowering_period'] ?? null,
|
|
'substrate_recommendation' => $enrichmentData['substrate_recommendation'] ?? null,
|
|
'native_to' => $enrichmentData['native_to'] ?? null,
|
|
'tags' => $enrichmentData['tags'] ?? null,
|
|
'latin_name' => $latinName
|
|
]);
|
|
}
|
|
|
|
// Process photo for lexicon if allowed and available
|
|
$fullAiResponse = json_decode($_POST['ai_full_response_json'] ?? '{}', true);
|
|
$stmtAllow = $pdo->prepare("SELECT allow_photo_sharing FROM users WHERE id = ?");
|
|
$stmtAllow->execute([$currentUserId]);
|
|
if ($stmtAllow->fetchColumn() && $finalPhotoPath && $photoPath && !empty($fullAiResponse['photo_analysis'])) {
|
|
$photoData = $fullAiResponse['photo_analysis'];
|
|
$isHighQuality = $photoData['is_high_quality'] ?? false;
|
|
|
|
$stmt = $pdo->prepare("INSERT INTO lexicon_photos (user_id, plant_latin_name, photo_path, status, rejection_reason, ai_confidence, photo_type, focal_point_x, focal_point_y, dominant_color_hex, ai_photo_quality_score) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
|
$stmt->execute([
|
|
$currentUserId,
|
|
$latinName,
|
|
$finalPhotoPath,
|
|
($isHighQuality ? 'approved' : 'rejected'),
|
|
$photoData['rejection_reason'] ?? null,
|
|
$fullAiResponse['identification']['confidence_score'] ?? null,
|
|
$photoData['photo_type'] ?? null,
|
|
$photoData['focal_point']['x'] ?? null,
|
|
$photoData['focal_point']['y'] ?? null,
|
|
$photoData['dominant_color_hex'] ?? null,
|
|
$photoData['photo_quality_score'] ?? null
|
|
]);
|
|
}
|
|
|
|
$pdo->commit();
|
|
echo json_encode(['success' => true, 'plant' => getPlantWithDueDates($pdo, $plantId, $currentUserId)]);
|
|
break;
|
|
default:
|
|
throw new Exception('Ungültige Formular-Aktion.', 400);
|
|
}
|
|
break;
|
|
|
|
case 'DELETE':
|
|
$plantId = $_GET['id'] ?? null;
|
|
if ($plantId) {
|
|
$pdo->beginTransaction();
|
|
|
|
$stmtLatin = $pdo->prepare("SELECT latin_name FROM plants WHERE id = ? AND user_id = ?");
|
|
$stmtLatin->execute([$plantId, $currentUserId]);
|
|
$latinNameToDecrement = $stmtLatin->fetchColumn();
|
|
|
|
$stmt = $pdo->prepare("DELETE FROM plants WHERE id = ? AND user_id = ?");
|
|
$stmt->execute([$plantId, $currentUserId]);
|
|
|
|
if ($latinNameToDecrement) {
|
|
$stmtDec = $pdo->prepare("UPDATE plant_lexicon SET entry_count = entry_count - 1 WHERE latin_name = ? AND entry_count > 0");
|
|
$stmtDec->execute([$latinNameToDecrement]);
|
|
}
|
|
|
|
$pdo->commit();
|
|
echo json_encode(['success' => true]);
|
|
} else if (isset($_GET['diary_entry_id'])) {
|
|
$entryId = $_GET['diary_entry_id'];
|
|
$stmt = $pdo->prepare("DELETE FROM plant_diary WHERE id = ? AND user_id = ?");
|
|
$stmt->execute([$entryId, $currentUserId]);
|
|
echo json_encode(['success' => $stmt->rowCount() > 0]);
|
|
}
|
|
else {
|
|
throw new Exception('Keine ID zum Löschen angegeben.', 400);
|
|
}
|
|
break;
|
|
|
|
default:
|
|
throw new Exception('Methode nicht erlaubt.', 405);
|
|
}
|
|
} catch (Exception $e) {
|
|
if ($pdo->inTransaction()) $pdo->rollBack();
|
|
$code = $e->getCode();
|
|
if (!is_int($code) || $code < 400 || $code >= 600) {
|
|
$code = 500;
|
|
}
|
|
http_response_code($code);
|
|
echo json_encode(['error' => $e->getMessage()]);
|
|
}
|
|
?>
|
|
|