commit 61ede4c32511160b28027285eb56521be2e8970b Author: Gemini Bot Date: Sun Dec 7 17:09:16 2025 +0000 Initial commit: Dockerize Cazubu diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..042def9 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,7 @@ +.git +.gitignore +.gitea +Dockerfile +README.md +*.swp +.DS_Store diff --git a/.gitea/workflows/build-push.yaml b/.gitea/workflows/build-push.yaml new file mode 100644 index 0000000..9118618 --- /dev/null +++ b/.gitea/workflows/build-push.yaml @@ -0,0 +1,59 @@ +name: Docker Build & Push +run-name: ${{ gitea.actor }} zwingt es zum Laufen 🔨 + +on: + push: + branches: + - main + - master + +jobs: + build-and-push: + runs-on: ubuntu-latest + steps: + # ----------------------------------------------------------- + # 1. DER NETZWERK-RETTER (MUSS WIEDER REIN!) + # Ersetzt "gitea:3000" durch die IP, damit der Container es findet. + # ----------------------------------------------------------- + - name: Fix Git URL Resolution + run: git config --global url."http://172.30.1.213/".insteadOf "http://gitea:3000/" + + # 2. Checkout (Klappt jetzt, weil URL umgebogen wird) + - name: Checkout Code + uses: actions/checkout@v3 + + # 3. Login (Mit dem globalen Token aus deinen User-Settings) + - name: Login bei Registry + run: docker login 172.30.1.213 -u ${{ gitea.actor }} -p ${{ secrets.TOKEN_RUNNER }} + + # 4. Feuer frei + - name: Build & Push + run: | + REPO_LOWER=$(echo "${{ gitea.repository }}" | tr '[:upper:]' '[:lower:]') + IMAGE_TAG="172.30.1.213/$REPO_LOWER:latest" + + echo "Baue Image: $IMAGE_TAG" + + docker build -t $IMAGE_TAG . + docker push $IMAGE_TAG + + - name: Webhook an Node-RED + if: always() + run: | + # Status setzen + if [ "${{ job.status }}" == "success" ]; then + STATUS="success" + else + STATUS="failed" + fi + + # JSON Payload basteln + # Wir nutzen printf, um sauberes JSON zu bauen + JSON_DATA=$(printf '{"status": "%s", "repo": "%s", "actor": "%s"}' "$STATUS" "${{ gitea.repository }}" "${{ gitea.actor }}") + + # Abfeuern an Node-RED + # Ersetze mit der IP deines Node-RED (intern reicht!) + curl -v -H "Content-Type: application/json" \ + -X POST \ + -d "$JSON_DATA" \ + http://172.30.80.246:1880/gitea-status diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..12118d3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +vendor/ +.idea/ +.vscode/ +*.log +.DS_Store +Thumbs.db diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a2c67cd --- /dev/null +++ b/Dockerfile @@ -0,0 +1,28 @@ +FROM php:8.2-apache + +# Install system dependencies and PHP extensions +RUN apt-get update && apt-get install -y \ + libpng-dev \ + libjpeg-dev \ + libfreetype6-dev \ + zip \ + unzip \ + && docker-php-ext-configure gd --with-freetype --with-jpeg \ + && docker-php-ext-install -j$(nproc) gd mysqli pdo pdo_mysql \ + && a2enmod rewrite + +# Clean up apt cache +RUN apt-get clean && rm -rf /var/lib/apt/lists/* + +# Set working directory +WORKDIR /var/www/html + +# Copy application source +COPY . /var/www/html/ + +# Set permissions for the web server +RUN chown -R www-data:www-data /var/www/html \ + && chmod -R 755 /var/www/html + +# Expose port 80 +EXPOSE 80 diff --git a/README.md b/README.md new file mode 100644 index 0000000..7b8b9bd --- /dev/null +++ b/README.md @@ -0,0 +1,56 @@ +# Cazubu - Pflanzenverwaltung + +Cazubu ist eine Webanwendung zur Verwaltung von Pflanzen, Inventar, Samen und Sensoren. + +## Funktionen +- Pflanzenübersicht und Details +- Inventarverwaltung +- Samen-Datenbank +- Sensor-Integration +- Benutzerverwaltung (Login/Register) + +## Installation & Betrieb + +### Voraussetzungen +- Docker +- Externe MySQL-Datenbank + +### Docker Build & Run + +Das Projekt ist für den Betrieb im Docker-Container vorbereitet. + +1. **Image bauen:** + ```bash + docker build -t cazubu . + ``` + +2. **Container starten:** + Es müssen die Datenbank-Verbindungsdaten als Umgebungsvariablen übergeben werden. + + ```bash + docker run -d -p 8080:80 \ + -e DB_SERVER="deine-db-ip" \ + -e DB_USERNAME="dein-db-user" \ + -e DB_PASSWORD="dein-db-passwort" \ + -e DB_NAME="cazubu" \ + --name cazubu-app \ + cazubu + ``` + +### Umgebungsvariablen + +| Variable | Beschreibung | Standard | +|----------|--------------|----------| +| `DB_SERVER` | IP oder Hostname des Datenbankservers | `172.30.242.130` | +| `DB_USERNAME` | Datenbank-Benutzer | `cazubu` | +| `DB_PASSWORD` | Datenbank-Passwort | (interner Standard) | +| `DB_NAME` | Name der Datenbank | `cazubu` | + +## Changelog + +### 2025-12-07 - Dockerisierung +- `Dockerfile` erstellt (PHP 8.2 + Apache + MySQLi/GD). +- `.dockerignore` hinzugefügt. +- CI/CD Pipeline Konfiguration `.gitea/workflows/build-push.yaml` hinzugefügt. +- `includes/db_connect.php` angepasst: Unterstützung für Umgebungsvariablen hinzugefügt. +- `README.md` erstellt. diff --git a/ajax_handler.php b/ajax_handler.php new file mode 100644 index 0000000..f27b22f --- /dev/null +++ b/ajax_handler.php @@ -0,0 +1,357 @@ + false, 'message' => 'Name darf nicht leer sein.']); exit; } + $sql = "INSERT INTO zones (user_id, name) VALUES (?, ?)"; + if ($stmt = $mysqli->prepare($sql)) { $stmt->bind_param("is", $user_id, trim($_POST['name'])); $stmt->execute(); echo json_encode(['success' => true]); } + break; + case 'edit_zone': + if (empty(trim($_POST['name'])) || empty($_POST['id'])) { echo json_encode(['success' => false, 'message' => 'Unvollständige Daten.']); exit; } + $sql = "UPDATE zones SET name = ? WHERE id = ? AND user_id = ?"; + if ($stmt = $mysqli->prepare($sql)) { $stmt->bind_param("sii", trim($_POST['name']), $_POST['id'], $user_id); $stmt->execute(); echo json_encode(['success' => true]); } + break; + case 'delete_zone': + if (empty($_POST['id'])) { echo json_encode(['success' => false, 'message' => 'Fehlende ID.']); exit; } + $sql = "DELETE FROM zones WHERE id = ? AND user_id = ?"; + if ($stmt = $mysqli->prepare($sql)) { $stmt->bind_param("ii", $_POST['id'], $user_id); $stmt->execute(); echo json_encode(['success' => true]); } + break; + case 'add_container': + if (empty(trim($_POST['name'])) || empty($_POST['zone_id'])) { echo json_encode(['success' => false, 'message' => 'Name und Zone sind Pflichtfelder.']); exit; } + $sql = "INSERT INTO containers (user_id, name, zone_id) VALUES (?, ?, ?)"; + if ($stmt = $mysqli->prepare($sql)) { $stmt->bind_param("isi", $user_id, trim($_POST['name']), $_POST['zone_id']); $stmt->execute(); echo json_encode(['success' => true]); } + break; + case 'edit_container': + if (empty(trim($_POST['name'])) || empty($_POST['zone_id']) || empty($_POST['id'])) { echo json_encode(['success' => false, 'message' => 'Alle Felder sind Pflicht.']); exit; } + $sql = "UPDATE containers SET name = ?, zone_id = ? WHERE id = ? AND user_id = ?"; + if ($stmt = $mysqli->prepare($sql)) { $stmt->bind_param("siii", trim($_POST['name']), $_POST['zone_id'], $_POST['id'], $user_id); $stmt->execute(); echo json_encode(['success' => true]); } + break; + case 'delete_container': + if (empty($_POST['id'])) { echo json_encode(['success' => false, 'message' => 'Fehlende ID.']); exit; } + $sql = "DELETE FROM containers WHERE id = ? AND user_id = ?"; + if ($stmt = $mysqli->prepare($sql)) { $stmt->bind_param("ii", $_POST['id'], $user_id); $stmt->execute(); echo json_encode(['success' => true]); } + break; + case 'add_seed': + if (empty(trim($_POST['strain_name']))) { echo json_encode(['success' => false, 'message' => 'Sortenname ist ein Pflichtfeld.']); exit; } + $sql = "INSERT INTO seeds (user_id, strain_name, internal_name, info_url, ratio_sativa, is_autoflower, stock_count, description) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"; + if ($stmt = $mysqli->prepare($sql)) { $is_autoflower = isset($_POST['is_autoflower']) ? 1 : 0; $stmt->bind_param("isssiiis", $user_id, $_POST['strain_name'], $_POST['internal_name'], $_POST['info_url'], $_POST['ratio_sativa'], $is_autoflower, $_POST['stock_count'], $_POST['description']); $stmt->execute(); echo json_encode(['success' => true]); } + break; + case 'edit_seed': + if (empty(trim($_POST['strain_name'])) || empty($_POST['id'])) { echo json_encode(['success' => false, 'message' => 'ID und Sortenname sind Pflichtfelder.']); exit; } + $sql = "UPDATE seeds SET strain_name = ?, internal_name = ?, info_url = ?, ratio_sativa = ?, is_autoflower = ?, stock_count = ?, description = ? WHERE id = ? AND user_id = ?"; + if ($stmt = $mysqli->prepare($sql)) { $is_autoflower = isset($_POST['is_autoflower']) ? 1 : 0; $stmt->bind_param("sssiiisii", $_POST['strain_name'], $_POST['internal_name'], $_POST['info_url'], $_POST['ratio_sativa'], $is_autoflower, $_POST['stock_count'], $_POST['description'], $_POST['id'], $user_id); $stmt->execute(); echo json_encode(['success' => true]); } + break; + case 'delete_seed': + if (empty($_POST['id'])) { echo json_encode(['success' => false, 'message' => 'Fehlende ID.']); exit; } + $sql = "DELETE FROM seeds WHERE id = ? AND user_id = ?"; + if ($stmt = $mysqli->prepare($sql)) { $stmt->bind_param("ii", $_POST['id'], $user_id); if ($stmt->execute()) { echo json_encode(['success' => true]); } } + break; + case 'get_containers_by_zone': + if (empty($_GET['zone_id']) || !is_numeric($_GET['zone_id'])) { echo json_encode(['success' => false, 'message' => 'Keine oder ungültige Zone ID.']); exit; } + $zone_id = $_GET['zone_id']; + $containers = []; + $sql = "SELECT id, name FROM containers WHERE user_id = ? AND zone_id = ? AND id NOT IN (SELECT container_id FROM plants WHERE status = 'Eingepflanzt')"; + if ($stmt = $mysqli->prepare($sql)) { $stmt->bind_param("ii", $user_id, $zone_id); $stmt->execute(); $result = $stmt->get_result(); while($row = $result->fetch_assoc()) { $containers[] = $row; } $stmt->close(); } + echo json_encode(['success' => true, 'data' => $containers]); + break; + case 'add_plant': + if (empty($_POST['zone_id']) || empty($_POST['container_id']) || empty($_POST['seed_id']) || empty($_POST['plant_date'])) { echo json_encode(['success' => false, 'message' => 'Bitte alle Felder ausfüllen.']); exit; } + $mysqli->begin_transaction(); + $sql_insert = "INSERT INTO plants (user_id, zone_id, container_id, seed_id, plant_date, phase, status) VALUES (?, ?, ?, ?, ?, 'Keimend', 'Eingepflanzt')"; + $stmt_insert = $mysqli->prepare($sql_insert); + $stmt_insert->bind_param("iiiis", $user_id, $_POST['zone_id'], $_POST['container_id'], $_POST['seed_id'], $_POST['plant_date']); + $stmt_insert->execute(); + $new_plant_id = $stmt_insert->insert_id; + $stmt_insert->close(); + if (isset($_POST['reduce_seed_stock']) && $_POST['reduce_seed_stock'] == '1') { + $sql_update = "UPDATE seeds SET stock_count = stock_count - 1 WHERE id = ? AND user_id = ? AND stock_count > 0"; + $stmt_update = $mysqli->prepare($sql_update); + $stmt_update->bind_param("ii", $_POST['seed_id'], $user_id); + $stmt_update->execute(); + $stmt_update->close(); + } + $sql_activity = "INSERT INTO plant_activities (plant_id, activity_type, note, activity_date) VALUES (?, 'Pflanzung', 'Pflanze wurde erstellt.', ?)"; + $stmt_activity = $mysqli->prepare($sql_activity); + $stmt_activity->bind_param("is", $new_plant_id, $_POST['plant_date']); + $stmt_activity->execute(); + $stmt_activity->close(); + $mysqli->commit(); + echo json_encode(['success' => true]); + break; + case 'edit_plant': + if (empty($_POST['plant_id']) || empty($_POST['phase']) || empty($_POST['plant_date']) || empty($_POST['zone_id']) || empty($_POST['container_id'])) { echo json_encode(['success' => false, 'message' => 'Alle Felder sind Pflichtfelder.']); exit; } + $mysqli->begin_transaction(); + $old_phase_stmt = $mysqli->prepare("SELECT phase FROM plants WHERE id = ? AND user_id = ?"); + $old_phase_stmt->bind_param("ii", $_POST['plant_id'], $user_id); + $old_phase_stmt->execute(); + $old_phase_result = $old_phase_stmt->get_result(); + $old_phase = ($old_phase_result->num_rows > 0) ? $old_phase_result->fetch_assoc()['phase'] : null; + $old_phase_stmt->close(); + $sql = "UPDATE plants SET phase = ?, plant_date = ?, zone_id = ?, container_id = ? WHERE id = ? AND user_id = ?"; + $stmt = $mysqli->prepare($sql); + $stmt->bind_param("ssiiii", $_POST['phase'], $_POST['plant_date'], $_POST['zone_id'], $_POST['container_id'], $_POST['plant_id'], $user_id); + $stmt->execute(); + $stmt->close(); + if ($old_phase !== null && $old_phase !== $_POST['phase']) { + $note = "Phase geändert von '{$old_phase}' zu '{$_POST['phase']}'."; + $sql_activity = "INSERT INTO plant_activities (plant_id, activity_type, note, activity_date) VALUES (?, 'Phasenwechsel', ?, NOW())"; + $stmt_activity = $mysqli->prepare($sql_activity); + $stmt_activity->bind_param("is", $_POST['plant_id'], $note); + $stmt_activity->execute(); + $stmt_activity->close(); + } + $mysqli->commit(); + echo json_encode(['success' => true]); + break; + case 'delete_plant': + if (empty($_POST['id']) || !is_numeric($_POST['id'])) { echo json_encode(['success' => false, 'message' => 'Ungültige Pflanzen-ID.']); exit; } + $plant_id = $_POST['id']; + $sql = "DELETE FROM plants WHERE id = ? AND user_id = ?"; + if ($stmt = $mysqli->prepare($sql)) { + $stmt->bind_param("ii", $plant_id, $user_id); + if ($stmt->execute()) { + if ($stmt->affected_rows > 0) { echo json_encode(['success' => true]); } + else { echo json_encode(['success' => false, 'message' => 'Pflanze nicht gefunden oder keine Berechtigung.']); } + } + } + break; + case 'upload_plant_image': + $plant_id = $_POST['plant_id'] ?? 0; + if (empty($plant_id) || !isset($_FILES['plant_image'])) { echo json_encode(['success' => false, 'message' => 'Fehlende Daten.']); exit; } + $sql_check = "SELECT id FROM plants WHERE id = ? AND user_id = ?"; + $stmt_check = $mysqli->prepare($sql_check); $stmt_check->bind_param("ii", $plant_id, $user_id); $stmt_check->execute(); + if ($stmt_check->get_result()->num_rows === 0) { echo json_encode(['success' => false, 'message' => 'Keine Berechtigung.']); exit; } + $stmt_check->close(); + $file = $_FILES['plant_image']; + if ($file['error'] !== UPLOAD_ERR_OK) { echo json_encode(['success' => false, 'message' => 'Fehler beim Upload: ' . $file['error']]); exit; } + $allowed_types = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']; + if (!in_array($file['type'], $allowed_types)) { echo json_encode(['success' => false, 'message' => 'Ungültiger Dateityp.']); exit; } + $upload_dir = 'uploads/user_' . $user_id . '/'; + if (!is_dir($upload_dir)) { mkdir($upload_dir, 0755, true); } + $file_extension = pathinfo($file['name'], PATHINFO_EXTENSION); + $new_filename = uniqid('plant_' . $plant_id . '_', true) . '.' . $file_extension; + $destination = $upload_dir . $new_filename; + if (move_uploaded_file($file['tmp_name'], $destination)) { + $sql_insert = "INSERT INTO plant_images (plant_id, user_id, file_path) VALUES (?, ?, ?)"; + $stmt_insert = $mysqli->prepare($sql_insert); + $stmt_insert->bind_param("iis", $plant_id, $user_id, $destination); + $stmt_insert->execute(); + $stmt_insert->close(); + echo json_encode(['success' => true]); + } else { echo json_encode(['success' => false, 'message' => 'Datei konnte nicht verschoben werden.']); } + break; + case 'delete_plant_image': + if (empty($_POST['id']) || !is_numeric($_POST['id'])) { echo json_encode(['success' => false, 'message' => 'Ungültige Bild-ID.']); exit; } + $image_id = $_POST['id']; $file_path = ''; + $sql_select = "SELECT pi.file_path FROM plant_images pi WHERE pi.id = ? AND pi.user_id = ?"; + if ($stmt_select = $mysqli->prepare($sql_select)) { $stmt_select->bind_param("ii", $image_id, $user_id); $stmt_select->execute(); $result = $stmt_select->get_result(); if ($result->num_rows === 1) { $file_path = $result->fetch_assoc()['file_path']; } $stmt_select->close(); } + if (empty($file_path)) { echo json_encode(['success' => false, 'message' => 'Bild nicht gefunden oder keine Berechtigung.']); exit; } + $sql_delete = "DELETE FROM plant_images WHERE id = ? AND user_id = ?"; + if ($stmt_delete = $mysqli->prepare($sql_delete)) { + $stmt_delete->bind_param("ii", $image_id, $user_id); + $stmt_delete->execute(); + if ($stmt_delete->affected_rows > 0) { + if (file_exists($file_path)) { unlink($file_path); } + echo json_encode(['success' => true]); + } else { echo json_encode(['success' => false, 'message' => 'Fehler beim Löschen des Datenbankeintrags.']); } + $stmt_delete->close(); + } + break; + case 'harvest_plant': + if (empty($_POST['plant_id']) || !is_numeric($_POST['plant_id'])) { echo json_encode(['success' => false, 'message' => 'Ungültige Pflanzen-ID.']); exit; } + $plant_id = $_POST['plant_id']; $mysqli->begin_transaction(); + $sql = "UPDATE plants SET status = 'Trocknend', phase = 'Ernte' WHERE id = ? AND user_id = ? AND status = 'Eingepflanzt'"; + if ($stmt = $mysqli->prepare($sql)) { + $stmt->bind_param("ii", $plant_id, $user_id); + $stmt->execute(); + if ($stmt->affected_rows > 0) { + $note = "Pflanze geerntet, Status auf 'Trocknend' gesetzt."; + $sql_activity = "INSERT INTO plant_activities (plant_id, activity_type, note, activity_date) VALUES (?, 'Ernte', ?, NOW())"; + $stmt_activity = $mysqli->prepare($sql_activity); + $stmt_activity->bind_param("is", $plant_id, $note); + $stmt_activity->execute(); + $stmt_activity->close(); + $mysqli->commit(); + echo json_encode(['success' => true]); + } else { $mysqli->rollback(); echo json_encode(['success' => false, 'message' => 'Aktion konnte nicht ausgeführt werden.']); } + $stmt->close(); + } + break; + case 'finish_drying': + if (empty($_POST['plant_id']) || !is_numeric($_POST['plant_id'])) { echo json_encode(['success' => false, 'message' => 'Ungültige Pflanzen-ID.']); exit; } + $plant_id = $_POST['plant_id']; $mysqli->begin_transaction(); + $sql = "UPDATE plants SET status = 'Geerntet', phase = 'Getrocknet' WHERE id = ? AND user_id = ? AND status = 'Trocknend'"; + if ($stmt = $mysqli->prepare($sql)) { + $stmt->bind_param("ii", $plant_id, $user_id); + $stmt->execute(); + if ($stmt->affected_rows > 0) { + $note = "Trocknung abgeschlossen, Status auf 'Geerntet' gesetzt."; + $sql_activity = "INSERT INTO plant_activities (plant_id, activity_type, note, activity_date) VALUES (?, 'Trocknung', ?, NOW())"; + $stmt_activity = $mysqli->prepare($sql_activity); + $stmt_activity->bind_param("is", $plant_id, $note); + $stmt_activity->execute(); + $stmt_activity->close(); + $mysqli->commit(); + echo json_encode(['success' => true]); + } else { $mysqli->rollback(); echo json_encode(['success' => false, 'message' => 'Aktion konnte nicht ausgeführt werden.']); } + $stmt->close(); + } + break; + case 'add_activity': + if (empty($_POST['plant_id']) || empty($_POST['activity_type']) || empty($_POST['activity_date'])) { echo json_encode(['success' => false, 'message' => 'Unvollständige Daten.']); exit; } + $sql = "INSERT INTO plant_activities (plant_id, activity_type, note, activity_date) VALUES (?, ?, ?, ?)"; + if ($stmt = $mysqli->prepare($sql)) { + $stmt->bind_param("isss", $_POST['plant_id'], $_POST['activity_type'], $_POST['note'], $_POST['activity_date']); + $stmt->execute(); + echo json_encode(['success' => true]); + } + break; + case 'add_measurement': + if (empty($_POST['plant_id']) || !isset($_POST['height_cm']) || empty($_POST['measurement_date'])) { echo json_encode(['success' => false, 'message' => 'Unvollständige Daten.']); exit; } + $sql = "INSERT INTO plant_height_measurements (plant_id, height_cm, measurement_date) VALUES (?, ?, ?)"; + if ($stmt = $mysqli->prepare($sql)) { + $stmt->bind_param("ids", $_POST['plant_id'], $_POST['height_cm'], $_POST['measurement_date']); + $stmt->execute(); + echo json_encode(['success' => true]); + } + break; + case 'generate_api_key': + $new_key = bin2hex(random_bytes(16)); + $sql = "UPDATE users SET api_key = ? WHERE id = ?"; + if ($stmt = $mysqli->prepare($sql)) { + $stmt->bind_param("si", $new_key, $user_id); + if ($stmt->execute()) { echo json_encode(['success' => true, 'message' => 'Neuer API-Key wurde generiert.']); } + } + break; + case 'change_username': + if (empty(trim($_POST['username']))) { echo json_encode(['success' => false, 'message' => 'Benutzername darf nicht leer sein.']); exit; } + $new_username = trim($_POST['username']); + $sql_check = "SELECT id FROM users WHERE username = ? AND id != ?"; + $stmt_check = $mysqli->prepare($sql_check); + $stmt_check->bind_param("si", $new_username, $user_id); + $stmt_check->execute(); + if ($stmt_check->get_result()->num_rows > 0) { echo json_encode(['success' => false, 'message' => 'Dieser Benutzername ist bereits vergeben.']); exit; } + $stmt_check->close(); + $sql_update = "UPDATE users SET username = ? WHERE id = ?"; + $stmt_update = $mysqli->prepare($sql_update); + $stmt_update->bind_param("si", $new_username, $user_id); + $stmt_update->execute(); + $_SESSION['username'] = $new_username; + echo json_encode(['success' => true, 'message' => 'Benutzername erfolgreich geändert.']); + break; + case 'change_password': + if (empty($_POST['current_password']) || empty($_POST['new_password']) || empty($_POST['confirm_new_password'])) { echo json_encode(['success' => false, 'message' => 'Alle Felder sind erforderlich.']); exit; } + if ($_POST['new_password'] !== $_POST['confirm_new_password']) { echo json_encode(['success' => false, 'message' => 'Die neuen Passwörter stimmen nicht überein.']); exit; } + if (strlen($_POST['new_password']) < 6) { echo json_encode(['success' => false, 'message' => 'Das neue Passwort muss mindestens 6 Zeichen lang sein.']); exit; } + $sql_pass = "SELECT password_hash FROM users WHERE id = ?"; + $stmt_pass = $mysqli->prepare($sql_pass); + $stmt_pass->bind_param("i", $user_id); + $stmt_pass->execute(); + $result_pass = $stmt_pass->get_result()->fetch_assoc(); + $stmt_pass->close(); + if (!password_verify($_POST['current_password'], $result_pass['password_hash'])) { echo json_encode(['success' => false, 'message' => 'Das aktuelle Passwort ist nicht korrekt.']); exit; } + $new_password_hash = password_hash($_POST['new_password'], PASSWORD_DEFAULT); + $sql_update_pass = "UPDATE users SET password_hash = ? WHERE id = ?"; + $stmt_update_pass = $mysqli->prepare($sql_update_pass); + $stmt_update_pass->bind_param("si", $new_password_hash, $user_id); + $stmt_update_pass->execute(); + echo json_encode(['success' => true, 'message' => 'Passwort erfolgreich geändert.']); + break; + case 'get_all_zones': + $zones = []; + $sql = "SELECT id, name FROM zones WHERE user_id = ? ORDER BY name ASC"; + if ($stmt = $mysqli->prepare($sql)) { + $stmt->bind_param("i", $user_id); + $stmt->execute(); + $result = $stmt->get_result(); + while($row = $result->fetch_assoc()) { $zones[] = $row; } + $stmt->close(); + } + echo json_encode(['success' => true, 'data' => $zones]); + break; + case 'get_plants_by_zone': + $zone_id = $_GET['zone_id'] ?? 'all'; + $plants = []; + $sql = "SELECT p.id, COALESCE(s.strain_name, '[Sorte gelöscht]') as strain_name, COALESCE(c.name, '[Gefäß gelöscht]') as container_name FROM plants p LEFT JOIN seeds s ON p.seed_id = s.id LEFT JOIN containers c ON p.container_id = c.id WHERE p.user_id = ? AND p.status = 'Eingepflanzt'"; + if (is_numeric($zone_id)) { + $sql .= " AND p.zone_id = ?"; + $stmt = $mysqli->prepare($sql); + $stmt->bind_param("ii", $user_id, $zone_id); + } else { + $stmt = $mysqli->prepare($sql); + $stmt->bind_param("i", $user_id); + } + $stmt->execute(); + $result = $stmt->get_result(); + while($row = $result->fetch_assoc()) { + $plants[] = $row; + } + $stmt->close(); + echo json_encode(['success' => true, 'data' => $plants]); + break; + case 'add_global_activity': + if (empty($_POST['activity_type']) || empty($_POST['activity_date']) || empty($_POST['plant_ids'])) { echo json_encode(['success' => false, 'message' => 'Unvollständige Daten.']); exit; } + $plant_ids = $_POST['plant_ids']; + $mysqli->begin_transaction(); + $sql = "INSERT INTO plant_activities (plant_id, activity_type, note, activity_date) VALUES (?, ?, ?, ?)"; + $stmt = $mysqli->prepare($sql); + foreach($plant_ids as $plant_id) { + $stmt->bind_param("isss", $plant_id, $_POST['activity_type'], $_POST['note'], $_POST['activity_date']); + $stmt->execute(); + } + $stmt->close(); + $mysqli->commit(); + echo json_encode(['success' => true]); + break; + case 'get_sensor_data': + if (empty($_GET['plant_id']) || !is_numeric($_GET['plant_id'])) { echo json_encode(['success' => false, 'message' => 'Ungültige Pflanzen-ID.']); exit; } + $plant_id = $_GET['plant_id']; + $sql_check = "SELECT id FROM plants WHERE id = ? AND user_id = ?"; + $stmt_check = $mysqli->prepare($sql_check); + $stmt_check->bind_param("ii", $plant_id, $user_id); + $stmt_check->execute(); + if ($stmt_check->get_result()->num_rows === 0) { echo json_encode(['success' => false, 'message' => 'Keine Berechtigung.']); exit; } + $stmt_check->close(); + $sql = "SELECT sensor_type, value, timestamp FROM sensor_data WHERE plant_id = ? ORDER BY timestamp ASC"; + $stmt = $mysqli->prepare($sql); + $stmt->bind_param("i", $plant_id); + $stmt->execute(); + $result = $stmt->get_result(); + $raw_data = []; + while ($row = $result->fetch_assoc()) { $raw_data[] = $row; } + $stmt->close(); + $data_by_timestamp = []; + foreach ($raw_data as $row) { + $timestamp = $row['timestamp']; + if (!isset($data_by_timestamp[$timestamp])) { $data_by_timestamp[$timestamp] = ['temperatur' => null, 'feuchtigkeit' => null]; } + if ($row['sensor_type'] == 'Temperatur') { $data_by_timestamp[$timestamp]['temperatur'] = (float)$row['value']; } + elseif ($row['sensor_type'] == 'Feuchtigkeit') { $data_by_timestamp[$timestamp]['feuchtigkeit'] = (float)$row['value']; } + } + ksort($data_by_timestamp); + $chart_data = ['labels' => [], 'temperature' => [], 'humidity' => []]; + foreach ($data_by_timestamp as $timestamp => $values) { + $chart_data['labels'][] = date('d.m H:i', strtotime($timestamp)); + $chart_data['temperature'][] = $values['temperatur']; + $chart_data['humidity'][] = $values['feuchtigkeit']; + } + echo json_encode(['success' => true, 'data' => $chart_data]); + break; + default: + echo json_encode(['success' => false, 'message' => 'Unbekannte Aktion: ' . $action]); + break; + } +} catch (mysqli_sql_exception $exception) { + if ($mysqli->ping()) { $mysqli->rollback(); } + error_log("[Cazubu Error] Datenbankfehler: " . $exception->getMessage()); + echo json_encode(['success' => false, 'message' => 'Datenbankfehler: ' . $exception->getMessage()]); +} +$mysqli->close(); +?> diff --git a/api.php b/api.php new file mode 100644 index 0000000..bc5e0c4 --- /dev/null +++ b/api.php @@ -0,0 +1,81 @@ +prepare($sql_user)) { + $stmt_user->bind_param("s", $api_key); + $stmt_user->execute(); + $result_user = $stmt_user->get_result(); + if ($result_user->num_rows === 1) { + $user_id = $result_user->fetch_assoc()['id']; + } + $stmt_user->close(); +} + +if ($user_id === null) { + http_response_code(401); // Unauthorized + die('Error: Invalid API Key.'); +} + +// --- Pflanzen-Zugehörigkeit prüfen --- +$sql_plant = "SELECT id FROM plants WHERE id = ? AND user_id = ?"; +if ($stmt_plant = $mysqli->prepare($sql_plant)) { + $stmt_plant->bind_param("ii", $plant_id, $user_id); + $stmt_plant->execute(); + if ($stmt_plant->get_result()->num_rows === 0) { + http_response_code(403); // Forbidden + die('Error: Plant does not belong to user.'); + } + $stmt_plant->close(); +} + +// --- Sensor-Typ mappen --- +$sensor_map = [ + 'temp' => 'Temperatur', + 'tmp' => 'Temperatur', + 'temperatur' => 'Temperatur', + 'feuchtigkeit' => 'Feuchtigkeit', + 'humidity' => 'Feuchtigkeit', + 'feucht' => 'Feuchtigkeit' +]; + +if (!array_key_exists($sensor_key, $sensor_map)) { + http_response_code(400); + die('Error: Unknown sensor type. Use temp or humidity.'); +} +$db_sensor_type = $sensor_map[$sensor_key]; + +// --- Daten in die Datenbank einfügen --- +$sql_insert = "INSERT INTO sensor_data (plant_id, sensor_type, value) VALUES (?, ?, ?)"; +if ($stmt_insert = $mysqli->prepare($sql_insert)) { + $stmt_insert->bind_param("isd", $plant_id, $db_sensor_type, $value); + if ($stmt_insert->execute()) { + http_response_code(200); + echo "OK"; + } else { + http_response_code(500); + die("Error: Could not save data."); + } + $stmt_insert->close(); +} + +$mysqli->close(); +?> diff --git a/assets/dummy_plant.png b/assets/dummy_plant.png new file mode 100644 index 0000000..73a9da6 Binary files /dev/null and b/assets/dummy_plant.png differ diff --git a/css/style.css b/css/style.css new file mode 100644 index 0000000..bbe2d3c --- /dev/null +++ b/css/style.css @@ -0,0 +1,72 @@ +/* + * CAZUBU Custom Stylesheet + * Version 9.2 - Finaler Feinschliff + */ + +/* Globale Stile & Layout */ +body, html { height: 100%; } +body { background-image: url('../wallpaper.png'); background-size: cover; background-position: center; background-attachment: fixed; } +.site-container { display: flex; min-height: 100vh; } +.side-menu { width: 280px; background-color: #2f3640; color: #f5f6fa; } +.project-name { font-weight: bold; color: white; } +.nav-link-header { font-weight: bold; color: #888; padding: .5rem 1rem; font-size: 0.8rem; text-transform: uppercase; } +.site-logo { width: 80%; max-width: 180px; height: auto; margin-bottom: 10px; } +.main-content { flex-grow: 1; padding: 30px; } +.header-card { background-color: #343a40 !important; color: #f8f9fa; border-radius: 0.75rem !important; } +.modal-content { background-color: rgba(233, 236, 239, 0.95) !important; color: #212529; } +.main-content h3.text-white, .main-content h4.text-white { text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.8); } + +/* Login/Register Seiten */ +.auth-container { min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 20px; } +.auth-form-wrapper { max-width: 450px; width: 100%; padding: 30px; border-radius: 0.75rem; background-color: rgba(248, 249, 250, 0.95); box-shadow: 0 5px 15px rgba(0,0,0,0.2); border: 1px solid rgba(255, 255, 255, 0.2); } + +/* Buttons & Icons */ +.btn-cazubu { --bs-btn-color: #fff; --bs-btn-bg: #556B2F; --bs-btn-border-color: #556B2F; --bs-btn-hover-color: #fff; --bs-btn-hover-bg: #6B8E23; --bs-btn-hover-border-color: #6B8E23; } +.popover { max-width: 220px; } +.popover-body { padding: 0.5rem; } +.info-link, .info-link:hover { text-decoration: none !important; color: inherit !important; } +.notes-icon { cursor: help; color: #6c757d; margin-left: 5px; } + +/* Das einheitliche, rahmenlose Cazubu Tabellen-Design */ +.cazubu-table-frameless { width: 100%; border-collapse: separate; border-spacing: 0; border-radius: 0.75rem; overflow: hidden; } +.cazubu-table-frameless thead th { background-color: #2f3640; color: white; border: none; text-align: left !important; vertical-align: middle; } +.cazubu-table-frameless tbody td { background-color: rgba(233, 236, 239, 0.92) !important; color: #212529; border-top: 1px solid rgba(47, 54, 64, 0.15); text-align: left !important; vertical-align: middle; } +.cazubu-table-frameless tbody tr:first-child td { border-top: none; } +.cazubu-table-frameless.table-hover tbody tr:hover td { background-color: rgba(220, 223, 226, 0.95) !important; } +.cazubu-table-frameless .text-muted { color: #495057 !important; } +.cazubu-table-frameless .btn-outline-dark { border-color: #2f3640; color: #2f3640; } +.cazubu-table-frameless .btn-outline-dark:hover { background-color: #2f3640; color: white; } + +/* Registerkarten */ +.plant-tabs { margin-bottom: 1.5rem; } +.plant-tabs .nav-link { color: #fff; font-weight: 500; background-color: rgba(47, 54, 64, 0.7); margin-right: 5px; border-radius: .375rem; } +.plant-tabs .nav-link.active { background-color: #556B2F; color: #fff; font-weight: bold; } +.cazubu-table-frameless .nav-tabs { border-bottom: none; } +.cazubu-table-frameless .nav-tabs .nav-link { border: 1px solid transparent; color: #a9a9a9; padding: 0.75rem 1rem; } +.cazubu-table-frameless .nav-tabs .nav-link:hover { border-color: transparent; color: #fff; } +.cazubu-table-frameless .nav-tabs .nav-link.active { background-color: rgba(233, 236, 239, 0.92); color: #212529; font-weight: bold; border-color: transparent; border-top-left-radius: 0.5rem; border-top-right-radius: 0.5rem; } + +/* Detailseite */ +.plant-image-frame { background-color: #fff; padding: 0.75rem; border-radius: 0.75rem; box-shadow: 0 4px 15px rgba(0,0,0,0.1); height: 300px; display: flex; align-items: center; justify-content: center; } +.plant-image-frame img { border-radius: 0.5rem; max-width: 100%; max-height: 100%; object-fit: contain; } +.image-gallery { display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 1rem; } +.gallery-item { position: relative; } +.gallery-item .gallery-image { width: 100%; height: 120px; object-fit: cover; border-radius: 0.5rem; border: 1px solid #dee2e6; } +.gallery-item .gallery-date { font-size: 0.75rem; color: #6c757d; text-align: left; margin-top: 0.25rem; padding-left: 0.25rem; } +.gallery-item .delete-image-btn { position: absolute; bottom: 5px; right: 5px; opacity: 0.6; transition: opacity 0.2s ease-in-out; width: 30px; height: 30px; padding: 0; display: inline-flex; align-items: center; justify-content: center; } +.gallery-item:hover .delete-image-btn { opacity: 1; } + +/* Minimalistisches DataTables Design */ +.dataTables_wrapper .dataTables_length, +.dataTables_wrapper .dataTables_filter, +.dataTables_wrapper .dataTables_info, +.dataTables_wrapper .dataTables_paginate { display: none !important; } +table.dataTable.no-footer { border-bottom: none !important; } +table.dataTable thead > tr > th.sorting:before, +table.dataTable thead > tr > th.sorting:after, +table.dataTable thead > tr > th.sorting_asc:before, +table.dataTable thead > tr > th.sorting_asc:after, +table.dataTable thead > tr > th.sorting_desc:before, +table.dataTable thead > tr > th.sorting_desc:after { + opacity: 0.5; +} diff --git a/css/style.css.0629Uhr b/css/style.css.0629Uhr new file mode 100644 index 0000000..bdac7da --- /dev/null +++ b/css/style.css.0629Uhr @@ -0,0 +1,135 @@ +/* + * CAZUBU Custom Stylesheet + * Version 8.1 - Finaler Design-Stand (Rollback) + */ + +/* Globale Stile & Layout */ +body, html { height: 100%; } +body { background-image: url('../wallpaper.png'); background-size: cover; background-position: center; background-attachment: fixed; } +.site-container { display: flex; min-height: 100vh; } +.side-menu { width: 280px; background-color: #2f3640; color: #f5f6fa; } +.project-name { font-weight: bold; color: white; } +.nav-link-header { font-weight: bold; color: #888; padding: .5rem 1rem; font-size: 0.8rem; text-transform: uppercase; } +.site-logo { width: 80%; max-width: 180px; height: auto; margin-bottom: 10px; } +.main-content { flex-grow: 1; padding: 30px; } +.header-card { background-color: #343a40 !important; color: #f8f9fa; border-radius: 0.75rem !important; } +.modal-content { background-color: rgba(233, 236, 239, 0.95) !important; } +.main-content h3.text-white, .main-content h4.text-white { text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.8); } + +/* Login/Register Seiten */ +.auth-container { min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 20px; } +.auth-form-wrapper { max-width: 450px; width: 100%; padding: 30px; border-radius: 0.75rem; background-color: rgba(248, 249, 250, 0.95); box-shadow: 0 5px 15px rgba(0,0,0,0.2); border: 1px solid rgba(255, 255, 255, 0.2); } + +/* Button Farben & Icons */ +.btn-cazubu { --bs-btn-color: #fff; --bs-btn-bg: #556B2F; --bs-btn-border-color: #556B2F; --bs-btn-hover-color: #fff; --bs-btn-hover-bg: #6B8E23; --bs-btn-hover-border-color: #6B8E23; } +.popover { max-width: 220px; } +.popover-body { padding: 0.5rem; } +.info-link, .info-link:hover { text-decoration: none; color: inherit; } +.notes-icon { cursor: help; color: #6c757d; margin-left: 5px; } + +/* Das einheitliche, rahmenlose Cazubu Tabellen-Design */ +.cazubu-table-frameless { + width: 100%; + border-collapse: separate; + border-spacing: 0; + border-radius: 0.75rem; + overflow: hidden; +} +.cazubu-table-frameless thead th { + background-color: #2f3640; + color: white; + border: none; +} +.cazubu-table-frameless tbody td { + background-color: rgba(233, 236, 239, 0.92) !important; + color: #212529; + border-top: 1px solid rgba(47, 54, 64, 0.15); +} +.cazubu-table-frameless tbody tr:first-child td { + border-top: none; +} +.cazubu-table-frameless.table-hover tbody tr:hover td { + background-color: rgba(220, 223, 226, 0.95) !important; +} +.cazubu-table-frameless .text-muted { + color: #6c757d !important; +} + +/* Styling für die Registerkarten auf der plants.php */ +.plant-tabs { + margin-bottom: 1.5rem; +} +.plant-tabs .nav-link { + color: #fff; + font-weight: 500; + background-color: rgba(47, 54, 64, 0.7); + margin-right: 5px; + border-radius: .375rem; +} +.plant-tabs .nav-link.active { + background-color: #556B2F; + color: #fff; + font-weight: bold; +} + +/* Styling für den Bild-Rahmen auf der Detailseite */ +.plant-image-frame { + background-color: #fff; + padding: 0.75rem; + border-radius: 0.75rem; + box-shadow: 0 4px 15px rgba(0,0,0,0.1); + height: 300px; + display: flex; + align-items: center; + justify-content: center; +} +.plant-image-frame img { + border-radius: 0.5rem; + max-width: 100%; + max-height: 100%; + object-fit: contain; +} + +/* Styling für die Registerkarten, wenn sie im Tabellenkopf sind (plant_detail.php) */ +.cazubu-table-frameless .nav-tabs { + border-bottom: none; +} +.cazubu-table-frameless .nav-tabs .nav-link { + border: 1px solid transparent; + color: #a9a9a9; + padding: 0.75rem 1rem; +} +.cazubu-table-frameless .nav-tabs .nav-link:hover { + border-color: transparent; + color: #fff; +} +.cazubu-table-frameless .nav-tabs .nav-link.active { + background-color: rgba(233, 236, 239, 0.92); + color: #212529; + font-weight: bold; + border-color: transparent; + border-top-left-radius: 0.5rem; + border-top-right-radius: 0.5rem; +} + +/* Styling für die Bildergalerie */ +.image-gallery { display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 1rem; } +.gallery-item { position: relative; } +.gallery-item .gallery-image { width: 100%; height: 120px; object-fit: cover; border-radius: 0.5rem; border: 1px solid #dee2e6; } +.gallery-item .gallery-date { font-size: 0.75rem; color: #6c757d; text-align: left; margin-top: 0.25rem; padding-left: 0.25rem; } +.gallery-item .delete-image-btn { + position: absolute; + bottom: 5px; + right: 5px; + opacity: 0.6; + transition: opacity 0.2s ease-in-out; + width: 30px; + height: 30px; + padding: 0; + display: inline-flex; + align-items: center; + justify-content: center; +} +.gallery-item:hover .delete-image-btn { + opacity: 1; +} diff --git a/css/style.css.1413Uhr b/css/style.css.1413Uhr new file mode 100644 index 0000000..8d7c81c --- /dev/null +++ b/css/style.css.1413Uhr @@ -0,0 +1,58 @@ +/* + * CAZUBU Custom Stylesheet + * Version 8.0 - Finaler Stand + */ + +/* Globale Stile & Layout */ +body, html { height: 100%; } +body { background-image: url('../wallpaper.png'); background-size: cover; background-position: center; background-attachment: fixed; } +.site-container { display: flex; min-height: 100vh; } +.side-menu { width: 280px; background-color: #2f3640; color: #f5f6fa; } +.project-name { font-weight: bold; color: white; } +.nav-link-header { font-weight: bold; color: #888; padding: .5rem 1rem; font-size: 0.8rem; text-transform: uppercase; } +.site-logo { width: 80%; height: auto; margin-bottom: 10px; } +.main-content { flex-grow: 1; padding: 30px; } +.header-card { background-color: #343a40 !important; color: #f8f9fa; border-radius: 0.75rem !important; } +.modal-content { background-color: rgba(233, 236, 239, 0.95) !important; } +.main-content h3.text-white, .main-content h4.text-white { text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.8); } + +/* Login/Register Seiten */ +.auth-container { min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 20px; } +.auth-form-wrapper { max-width: 450px; width: 100%; padding: 30px; border-radius: 0.75rem; background-color: rgba(248, 249, 250, 0.95); box-shadow: 0 5px 15px rgba(0,0,0,0.2); border: 1px solid rgba(255, 255, 255, 0.2); } + +/* Button Farben & Icons */ +.btn-cazubu { --bs-btn-color: #fff; --bs-btn-bg: #556B2F; --bs-btn-border-color: #556B2F; --bs-btn-hover-color: #fff; --bs-btn-hover-bg: #6B8E23; --bs-btn-hover-border-color: #6B8E23; } +.popover { max-width: 220px; } +.popover-body { padding: 0.5rem; } +.info-link, .info-link:hover { text-decoration: none; color: inherit; } +.notes-icon { cursor: help; color: #6c757d; margin-left: 5px; } + +/* Das einheitliche, rahmenlose Cazubu Tabellen-Design */ +.cazubu-table-frameless { width: 100%; border-collapse: separate; border-spacing: 0; border-radius: 0.75rem; overflow: hidden; } +.cazubu-table-frameless thead th { background-color: #2f3640; color: white; border: none; } +.cazubu-table-frameless tbody td { background-color: rgba(233, 236, 239, 0.92) !important; color: #212529; border-top: 1px solid rgba(47, 54, 64, 0.15); } +.cazubu-table-frameless tbody tr:first-child td { border-top: none; } +.cazubu-table-frameless.table-hover tbody tr:hover td { background-color: rgba(220, 223, 226, 0.95) !important; } + +/* Styling für den Bild-Rahmen auf der Detailseite */ +.plant-image-frame { background-color: #fff; padding: 0.75rem; border-radius: 0.75rem; box-shadow: 0 4px 15px rgba(0,0,0,0.1); height: 300px; display: flex; align-items: center; justify-content: center; } +.plant-image-frame img { border-radius: 0.5rem; max-width: 100%; max-height: 100%; } + +/* Styling für die Registerkarten (Tabs), die über einer Tabelle schweben */ +.plant-tabs { margin-bottom: 1.5rem; } +.plant-tabs .nav-link { color: #fff; font-weight: 500; background-color: rgba(47, 54, 64, 0.7); margin-right: 5px; border-radius: .375rem; } +.plant-tabs .nav-link.active { background-color: #556B2F; color: #fff; font-weight: bold; } + +/* Styling für die Registerkarten, wenn sie im Tabellenkopf sind (plant_detail.php) */ +.cazubu-table-frameless .nav-tabs { border-bottom: none; } +.cazubu-table-frameless .nav-tabs .nav-link { border: 1px solid transparent; color: #a9a9a9; padding: 0.75rem 1rem; } +.cazubu-table-frameless .nav-tabs .nav-link:hover { border-color: transparent; color: #fff; } +.cazubu-table-frameless .nav-tabs .nav-link.active { background-color: rgba(233, 236, 239, 0.92); color: #212529; font-weight: bold; border-color: transparent; border-top-left-radius: 0.5rem; border-top-right-radius: 0.5rem; } + +/* Styling für die Bildergalerie */ +.image-gallery { display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 1rem; } +.gallery-item { position: relative; } +.gallery-item .gallery-image { width: 100%; height: 120px; object-fit: cover; border-radius: 0.5rem; border: 1px solid #dee2e6; } +.gallery-item .gallery-date { font-size: 0.75rem; color: #6c757d; text-align: left; margin-top: 0.25rem; padding-left: 0.25rem; } +.gallery-item .delete-image-btn { position: absolute; bottom: 5px; right: 5px; opacity: 0.6; transition: opacity 0.2s ease-in-out; width: 30px; height: 30px; padding: 0; display: inline-flex; align-items: center; justify-content: center; } +.gallery-item:hover .delete-image-btn { opacity: 1; } diff --git a/css/style.css.1908Uhr b/css/style.css.1908Uhr new file mode 100644 index 0000000..26a22cf --- /dev/null +++ b/css/style.css.1908Uhr @@ -0,0 +1,61 @@ +/* + * CAZUBU Custom Stylesheet + * Version 7.0 - Dashboard & Animation + */ + +/* Globale Stile & Layout */ +body, html { height: 100%; } +body { background-color: #212529; color: #f8f9fa; } +.site-container { display: flex; min-height: 100vh; position: relative; z-index: 1; } +.side-menu { width: 280px; background-color: #2f3640; color: #f5f6fa; } +.project-name { font-weight: bold; color: white; } +.nav-link-header { font-weight: bold; color: #888; padding: .5rem 1rem; font-size: 0.8rem; text-transform: uppercase; } +.site-logo { width: 80%; height: auto; margin-bottom: 10px; } +.main-content { flex-grow: 1; padding: 30px; } +.header-card { background-color: #343a40 !important; color: #f8f9fa; border-radius: 0.75rem !important; } +.modal-content { background-color: rgba(233, 236, 239, 0.95) !important; } +.main-content h3.text-white, .main-content h4.text-white { text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.8); } + +/* Login/Register Seiten */ +.auth-container { min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 20px; } +.auth-form-wrapper { max-width: 450px; width: 100%; padding: 30px; border-radius: 0.75rem; background-color: rgba(248, 249, 250, 0.95); box-shadow: 0 5px 15px rgba(0,0,0,0.2); border: 1px solid rgba(255, 255, 255, 0.2); } + +/* Button Farben & Icons */ +.btn-cazubu { --bs-btn-color: #fff; --bs-btn-bg: #556B2F; --bs-btn-border-color: #556B2F; --bs-btn-hover-color: #fff; --bs-btn-hover-bg: #6B8E23; --bs-btn-hover-border-color: #6B8E23; } +.popover { max-width: 220px; } .popover-body { padding: 0.5rem; } +.info-link, .info-link:hover { text-decoration: none; color: inherit; } +.notes-icon { cursor: help; color: #6c757d; margin-left: 5px; } + +/* Das einheitliche, rahmenlose Cazubu Tabellen-Design */ +.cazubu-table-frameless { width: 100%; border-collapse: separate; border-spacing: 0; border-radius: 0.75rem; overflow: hidden; } +.cazubu-table-frameless thead th { background-color: #2f3640; color: white; border: none; } +.cazubu-table-frameless tbody td { background-color: rgba(233, 236, 239, 0.92) !important; color: #212529; border-top: 1px solid rgba(47, 54, 64, 0.15); } +.cazubu-table-frameless tbody tr:first-child td { border-top: none; } +.cazubu-table-frameless.table-hover tbody tr:hover td { background-color: rgba(220, 223, 226, 0.95) !important; } + +/* Styling für die Registerkarten auf der plants.php */ +.plant-tabs { margin-bottom: 1.5rem; } +.plant-tabs .nav-link { color: #fff; font-weight: 500; background-color: rgba(47, 54, 64, 0.7); margin-right: 5px; border-radius: .375rem; } +.plant-tabs .nav-link.active { background-color: #556B2F; color: #fff; font-weight: bold; } + +/* Styling für die Detailseite */ +.plant-image-frame { background-color: #fff; padding: 0.75rem; border-radius: 0.75rem; box-shadow: 0 4px 15px rgba(0,0,0,0.1); height: 300px; display: flex; align-items: center; justify-content: center; } +.plant-image-frame img { border-radius: 0.5rem; max-width: 100%; max-height: 100%; } +.cazubu-table-frameless .nav-tabs { border-bottom: none; } +.cazubu-table-frameless .nav-tabs .nav-link { border: 1px solid transparent; color: #a9a9a9; padding: 0.75rem 1rem; } +.cazubu-table-frameless .nav-tabs .nav-link:hover { border-color: transparent; color: #fff; } +.cazubu-table-frameless .nav-tabs .nav-link.active { background-color: rgba(233, 236, 239, 0.92); color: #212529; font-weight: bold; border-color: transparent; border-top-left-radius: 0.5rem; border-top-right-radius: 0.5rem; } +.image-gallery { display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 1rem; } +.gallery-item { position: relative; } +.gallery-item .gallery-image { width: 100%; height: 120px; object-fit: cover; border-radius: 0.5rem; border: 1px solid #dee2e6; } +.gallery-item .gallery-date { font-size: 0.75rem; color: #6c757d; text-align: left; margin-top: 0.25rem; padding-left: 0.25rem; } +.gallery-item .delete-image-btn { position: absolute; bottom: 5px; right: 5px; opacity: 0.6; transition: opacity 0.2s ease-in-out; width: 30px; height: 30px; padding: 0; display: inline-flex; align-items: center; justify-content: center; } +.gallery-item:hover .delete-image-btn { opacity: 1; } + +/* NEU: Stile für die Startseite */ +.zoom-effect-container { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; overflow: hidden; z-index: -1; background-color: #212529; } +.zoom-effect-image { position: absolute; left: 50%; top: 50%; width: 100%; height: 100%; background-image: url('../wallpaper.png'); background-size: cover; background-position: center; opacity: 0.3; transform: translate(-50%, -50%) scale(1); animation: zoomEffect 80s infinite alternate ease-in-out; } +@keyframes zoomEffect { from { transform: translate(-50%, -50%) scale(1); } to { transform: translate(-50%, -50%) scale(1.2); } } +.dashboard-card { background-color: rgba(255, 255, 255, 0.05); border: 1px solid rgba(255, 255, 255, 0.1); backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); transition: all 0.3s ease; } +.dashboard-card:hover { background-color: rgba(255, 255, 255, 0.1); border-color: rgba(255, 255, 255, 0.2); } +.dashboard-card .stat-number { font-size: 3rem; font-weight: 700; color: #556B2F; } diff --git a/includes/auth_check.php b/includes/auth_check.php new file mode 100644 index 0000000..dab6331 --- /dev/null +++ b/includes/auth_check.php @@ -0,0 +1,10 @@ + diff --git a/includes/db_connect.php b/includes/db_connect.php new file mode 100644 index 0000000..985e58b --- /dev/null +++ b/includes/db_connect.php @@ -0,0 +1,18 @@ +connect_error); +} + +// Zeichensatz auf UTF-8 setzen +$mysqli->set_charset("utf8mb4"); +?> diff --git a/includes/footer.php b/includes/footer.php new file mode 100644 index 0000000..048c794 --- /dev/null +++ b/includes/footer.php @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/includes/footer.php.1345Uhr b/includes/footer.php.1345Uhr new file mode 100644 index 0000000..fcd2287 --- /dev/null +++ b/includes/footer.php.1345Uhr @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/includes/header.php b/includes/header.php new file mode 100644 index 0000000..63ac9df --- /dev/null +++ b/includes/header.php @@ -0,0 +1,53 @@ + + + + + + + Cazubu - Cannabis Zucht Buddy + + + + +
+
+
+
+ +
diff --git a/includes/header.php.1343Uhr b/includes/header.php.1343Uhr new file mode 100644 index 0000000..817b3da --- /dev/null +++ b/includes/header.php.1343Uhr @@ -0,0 +1,48 @@ + + + + + + + Cazubu - Cannabis Zucht Buddy + + + + +
+ +
diff --git a/includes/header.php.1357Uhr b/includes/header.php.1357Uhr new file mode 100644 index 0000000..d219afb --- /dev/null +++ b/includes/header.php.1357Uhr @@ -0,0 +1,50 @@ + + + + + + + Cazubu - Cannabis Zucht Buddy + + + + + +
+ +
diff --git a/index.php b/index.php new file mode 100644 index 0000000..5bb3113 --- /dev/null +++ b/index.php @@ -0,0 +1,120 @@ + 0, + 'seeds' => 0, + 'plants' => 0 +]; + +// Zonen zählen +$result_zones = $mysqli->query("SELECT COUNT(*) as count FROM zones WHERE user_id = $user_id"); +if($result_zones) { + $stats['zones'] = $result_zones->fetch_assoc()['count']; +} + +// Samen zählen +$result_seeds = $mysqli->query("SELECT SUM(stock_count) as count FROM seeds WHERE user_id = $user_id"); +if($result_seeds) { + $seed_count_result = $result_seeds->fetch_assoc()['count']; + $stats['seeds'] = $seed_count_result ?? 0; +} + +// Aktive Pflanzen zählen +$result_plants = $mysqli->query("SELECT COUNT(*) as count FROM plants WHERE user_id = $user_id AND status = 'Eingepflanzt'"); +if($result_plants) { + $stats['plants'] = $result_plants->fetch_assoc()['count']; +} + + +require_once 'includes/header.php'; +?> + +
+
+

Willkommen, !

+

Was möchtest du als Nächstes tun?

+
+
+ +
+
+ + + + + + + +
Schritt 1: Inventar
+

Lege deine Anbau-Zonen und Pflanzgefäße an.

+ 0): ?> +
+
+
Zone(n) angelegt
+
+ Zum Inventar + + + +
+
+ +
+ + + + + + + +
Schritt 2: Samenbank
+

Erfasse alle Samen, die du auf Lager hast.

+ 0): ?> +
+
+
Samen auf Lager
+
+ Zur Samenbank + + + +
+
+ +
+ + + + + + + +
Schritt 3: Pflanzen
+

Lege deine Pflanzen an und verfolge ihren Fortschritt.

+ 0): ?> +
+
+
Pflanze(n) wachsen
+
+ Zu den Pflanzen + + + +
+
+
+ + diff --git a/inventory.php b/inventory.php new file mode 100644 index 0000000..471ed6c --- /dev/null +++ b/inventory.php @@ -0,0 +1,90 @@ +prepare($sql_zones)) { + $stmt_zones->bind_param("i", $user_id); + $stmt_zones->execute(); + $result_zones = $stmt_zones->get_result(); + while ($row = $result_zones->fetch_assoc()) { $zones[] = $row; } + $stmt_zones->close(); +} +$sql_containers = "SELECT c.id, c.name, z.name AS zone_name, c.zone_id FROM containers c JOIN zones z ON c.zone_id = z.id WHERE c.user_id = ? ORDER BY c.name ASC"; +if ($stmt_containers = $mysqli->prepare($sql_containers)) { + $stmt_containers->bind_param("i", $user_id); + $stmt_containers->execute(); + $result_containers = $stmt_containers->get_result(); + while ($row = $result_containers->fetch_assoc()) { $containers[] = $row; } + $stmt_containers->close(); +} +require_once 'includes/header.php'; +?> +
+
+

Inventar-Verwaltung

+

Verwalten Sie hier Ihre Zonen und Pflanzgefäße.

+
+
+
+
+
+

Zonen

+ +
+ + + + + + + + + '; ?> + + + + + + + +
NameAktionen
Keine Zonen angelegt.
+ + +
+
+
+
+

Pflanzgefäße

+ +
+ + + + + + + + + + '; ?> + + + + + + + + +
NameZoneAktionen
Keine Pflanzgefäße angelegt.
+ + +
+
+
+ + + + diff --git a/js/main.js b/js/main.js new file mode 100644 index 0000000..28cf1fa --- /dev/null +++ b/js/main.js @@ -0,0 +1,97 @@ +$(document).ready(function() { + // Bootstrap Initialisierungen + const popoverTriggerList = document.querySelectorAll('[data-bs-toggle="popover"]'); + const popoverList = [...popoverTriggerList].map(popoverTriggerEl => new bootstrap.Popover(popoverTriggerEl)); + const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]'); + const tooltipList = [...tooltipTriggerList].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl)); + + // DataTables Initialisierungen + if ($('#seeds-table').length) { + $('#seeds-table').DataTable({ "dom": "t", "paging": false, "info": false, "language": { "zeroRecords": "Keine passenden Samen gefunden" }, "columnDefs": [ { "orderable": false, "targets": 'no-sort' } ] }); + } + if ($('#plants-table').length) { + $('#plants-table').DataTable({ "dom": "t", "paging": false, "info": false, "order": [[ 4, "desc" ]], "language": { "zeroRecords": "Keine passenden Pflanzen gefunden" }, "columnDefs": [ { "orderable": false, "targets": 'no-sort' }, { "type": "num", "targets": 4 } ] }); + } + + // Universeller Formular-Handler + function handleFormSubmit(form) { + $.ajax({ + type: 'POST', url: 'ajax_handler.php', data: form.serialize(), dataType: 'json', + success: function(response) { + if (response.success) { + if (response.message) { alert(response.message); } + location.reload(); + } else { alert('Speichern fehlgeschlagen: ' + (response.message || 'Unbekannter Fehler')); } + }, + error: () => alert('Ein schwerwiegender Serverfehler ist aufgetreten.') + }); + } + + // Event-Listener für alle Standard-Formulare + $('form:not(#image-upload-form, #change-password-form)').on('submit', function(e) { + e.preventDefault(); + if ($(this).attr('id') === 'generate-api-key-form' && !confirm('Bist du sicher? Ein neuer Key macht einen eventuell bestehenden ungültig.')) { return; } + handleFormSubmit($(this)); + }); + + // Spezielle Handler für Datei-Upload und Passwort-Änderung + $('#image-upload-form').on('submit', function(e) { e.preventDefault(); const formData = new FormData(this); $.ajax({ type: 'POST', url: 'ajax_handler.php', data: formData, dataType: 'json', contentType: false, processData: false, success: function(response) { if (response.success) { location.reload(); } else { alert('Fehler beim Upload: ' + (response.message || 'Unbekannter Fehler')); } }, error: function() { alert('Ein schwerwiegender Serverfehler ist aufgetreten.'); } }); }); + $('#change-password-form').on('submit', function(e) { e.preventDefault(); $.ajax({ type: 'POST', url: 'ajax_handler.php', data: $(this).serialize(), dataType: 'json', success: function(response) { if (response.success) { $('#changePasswordModal').modal('hide'); alert(response.message); } else { alert('Fehler: ' + (response.message || 'Unbekannter Fehler')); } }, error: () => alert('Serverfehler!') }); }); + + // Modal-Logik (Befüllen von Formularen beim Öffnen) + $('#zoneModal').on('show.bs.modal', function(event) { const button = $(event.relatedTarget); const form = $('#zone-form'); form.trigger('reset'); if (button.data('id')) { $('#zoneModalLabel').text('Zone bearbeiten'); form.find('[name="name"]').val(button.data('name')); form.find('[name="id"]').val(button.data('id')); form.find('[name="action"]').val('edit_zone'); } else { $('#zoneModalLabel').text('Neue Zone hinzufügen'); form.find('[name="action"]').val('add_zone'); } }); + $('#containerModal').on('show.bs.modal', function(event) { const button = $(event.relatedTarget); const form = $('#container-form'); form.trigger('reset'); if (button.data('id')) { $('#containerModalLabel').text('Pflanzgefäß bearbeiten'); form.find('[name="name"]').val(button.data('name')); form.find('[name="id"]').val(button.data('id')); form.find('[name="zone_id"]').val(button.data('zoneid')); form.find('[name="action"]').val('edit_container'); } else { $('#containerModalLabel').text('Neues Pflanzgefäß hinzufügen'); form.find('[name="action"]').val('add_container'); } }); + $('#seedModal').on('show.bs.modal', function(event) { const button = $(event.relatedTarget); const form = $('#seed-form'); form.trigger('reset'); $('#ratio_sativa').val(50).trigger('input'); if (button.data('seed')) { $('#seedModalLabel').text('Samen bearbeiten'); const seedData = button.data('seed'); form.find('[name="id"]').val(seedData.id); form.find('[name="strain_name"]').val(seedData.strain_name); form.find('[name="internal_name"]').val(seedData.internal_name); form.find('[name="stock_count"]').val(seedData.stock_count); form.find('[name="info_url"]').val(seedData.info_url); form.find('[name="description"]').val(seedData.description); form.find('[name="ratio_sativa"]').val(seedData.ratio_sativa).trigger('input'); form.find('[name="is_autoflower"]').prop('checked', seedData.is_autoflower == 1); form.find('[name="action"]').val('edit_seed'); } else { $('#seedModalLabel').text('Neuen Samen hinzufügen'); form.find('[name="action"]').val('add_seed'); } }); + $('#editPlantModal').on('show.bs.modal', function(event) { const button = $(event.relatedTarget); const form = $('#edit-plant-form'); const plantData = button.data('plant-info'); form.find('[name="phase"]').val(plantData.phase); form.find('[name="plant_date"]').val(plantData.plant_date); form.find('[name="zone_id"]').val(plantData.zone_id).trigger('change', [plantData.container_id]); }); + $('#addActivityModal').on('show.bs.modal', function() { const now = new Date(); now.setMinutes(now.getMinutes() - now.getTimezoneOffset()); const localDateTime = now.toISOString().slice(0,16); $(this).find('input[type="datetime-local"]').val(localDateTime); }); + $('#addMeasurementModal').on('show.bs.modal', function() { const today = new Date().toISOString().slice(0, 10); $(this).find('input[type="date"]').val(today); }); + + // Logik für abhängige Dropdowns + $('#zone-select, #edit-zone-select').on('change', function(event, preselectContainerId) { const zoneId = $(this).val(); const isEditMode = $(this).attr('id') === 'edit-zone-select'; const containerSelect = isEditMode ? $('#edit-container-select') : $('#container-select'); containerSelect.prop('disabled', true).html(''); if (!zoneId) { containerSelect.html(''); return; } $.ajax({ type: 'GET', url: 'ajax_handler.php', data: { action: 'get_containers_by_zone', zone_id: zoneId }, dataType: 'json', success: function(response) { if (response.success) { containerSelect.empty().append(''); if (isEditMode && preselectContainerId) { const plantData = $('.edit-plant-btn').data('plant-info'); containerSelect.append($(''); } }, error: function() { containerSelect.html(''); } }); }); + + // Logik für Status-Änderungen und Löschen + $('#confirmHarvestBtn').on('click', function() { const plantId = $('#edit-plant-form [name="plant_id"]').val(); if (plantId) { $.ajax({ type: 'POST', url: 'ajax_handler.php', data: { action: 'harvest_plant', plant_id: plantId }, dataType: 'json', success: function(response) { if (response.success) { location.reload(); } else { alert('Aktion fehlgeschlagen: ' + (response.message || 'Unbekannter Fehler')); } }, error: () => alert('Serverfehler!') }); } }); + $('#confirmFinishDryingBtn').on('click', function() { const plantId = $('#edit-plant-form [name="plant_id"]').val(); if (plantId) { $.ajax({ type: 'POST', url: 'ajax_handler.php', data: { action: 'finish_drying', plant_id: plantId }, dataType: 'json', success: function(response) { if (response.success) { location.reload(); } else { alert('Aktion fehlgeschlagen: ' + (response.message || 'Unbekannter Fehler')); } }, error: () => alert('Serverfehler!') }); } }); + let itemToDelete = { id: null, type: null }; + $('#deleteConfirmModal').on('show.bs.modal', function(event) { const button = $(event.relatedTarget); itemToDelete.id = button.data('id'); itemToDelete.type = button.data('type'); $('#item-name-to-delete').text(button.data('name')); let typeText = 'dieses Element'; if (itemToDelete.type === 'zone') { typeText = 'die Zone'; } else if (itemToDelete.type === 'container') { typeText = 'das Pflanzgefäß'; } else if (itemToDelete.type === 'seed') { typeText = 'diesen Samen'; } else if (itemToDelete.type === 'plant') { typeText = 'diese Pflanze'; } else if (itemToDelete.type === 'plant_image') { typeText = 'dieses Bild'; } $('#item-type-to-delete').text(typeText); $('#delete-warning').toggle(itemToDelete.type === 'zone'); }); + $('#confirmDeleteBtn').on('click', function() { if (!itemToDelete.id || !itemToDelete.type) return; $.ajax({ type: 'POST', url: 'ajax_handler.php', data: { action: 'delete_' + itemToDelete.type, id: itemToDelete.id }, dataType: 'json', success: function(response) { if (response.success) { if (itemToDelete.type === 'plant') { window.location.href = 'plants.php'; } else { location.reload(); } } else { alert('Löschen fehlgeschlagen: ' + (response.message || 'Unbekannter Fehler')); } }, error: () => alert('Serverfehler!') }); }); + + // Globale Aktivitäten Logik + const globalActivityModal = document.getElementById('globalActivityModal'); + if (globalActivityModal) { globalActivityModal.addEventListener('show.bs.modal', function () { const zoneFilterSelect = $('#zone-filter-select'); if(zoneFilterSelect.children().length <= 1) { $.ajax({ type: 'GET', url: 'ajax_handler.php', data: { action: 'get_all_zones' }, dataType: 'json', success: function(response) { if (response.success && response.data.length > 0) { response.data.forEach(function(zone) { zoneFilterSelect.append($('