Initial commit: Dockerize Cazubu
All checks were successful
Docker Build & Push / build-and-push (push) Successful in 1m36s
All checks were successful
Docker Build & Push / build-and-push (push) Successful in 1m36s
This commit is contained in:
7
.dockerignore
Normal file
7
.dockerignore
Normal file
@@ -0,0 +1,7 @@
|
||||
.git
|
||||
.gitignore
|
||||
.gitea
|
||||
Dockerfile
|
||||
README.md
|
||||
*.swp
|
||||
.DS_Store
|
||||
59
.gitea/workflows/build-push.yaml
Normal file
59
.gitea/workflows/build-push.yaml
Normal file
@@ -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 <NODE-RED-IP> 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
|
||||
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
vendor/
|
||||
.idea/
|
||||
.vscode/
|
||||
*.log
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
28
Dockerfile
Normal file
28
Dockerfile
Normal file
@@ -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
|
||||
56
README.md
Normal file
56
README.md
Normal file
@@ -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.
|
||||
357
ajax_handler.php
Normal file
357
ajax_handler.php
Normal file
@@ -0,0 +1,357 @@
|
||||
<?php
|
||||
require_once 'includes/auth_check.php';
|
||||
require_once 'includes/db_connect.php';
|
||||
header('Content-Type: application/json');
|
||||
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
|
||||
|
||||
$action = $_POST['action'] ?? $_GET['action'] ?? '';
|
||||
$user_id = $_SESSION['user_id'];
|
||||
|
||||
try {
|
||||
switch ($action) {
|
||||
case 'add_zone':
|
||||
if (empty(trim($_POST['name']))) { echo json_encode(['success' => 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();
|
||||
?>
|
||||
81
api.php
Normal file
81
api.php
Normal file
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
require_once 'includes/db_connect.php';
|
||||
|
||||
// --- Parameter validieren ---
|
||||
if (!isset($_GET['apikey'], $_GET['pflanze'], $_GET['sensor'], $_GET['wert'])) {
|
||||
http_response_code(400); // Bad Request
|
||||
die('Error: Missing parameters.');
|
||||
}
|
||||
|
||||
$api_key = $_GET['apikey'];
|
||||
$plant_id = (int)$_GET['pflanze'];
|
||||
$sensor_key = strtolower($_GET['sensor']);
|
||||
$value = $_GET['wert'];
|
||||
|
||||
if (empty($api_key) || empty($plant_id) || empty($sensor_key) || !is_numeric($value)) {
|
||||
http_response_code(400);
|
||||
die('Error: Invalid parameters.');
|
||||
}
|
||||
|
||||
// --- API-Key validieren und User-ID holen ---
|
||||
$user_id = null;
|
||||
$sql_user = "SELECT id FROM users WHERE api_key = ?";
|
||||
if ($stmt_user = $mysqli->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();
|
||||
?>
|
||||
BIN
assets/dummy_plant.png
Normal file
BIN
assets/dummy_plant.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.0 MiB |
72
css/style.css
Normal file
72
css/style.css
Normal file
@@ -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;
|
||||
}
|
||||
135
css/style.css.0629Uhr
Normal file
135
css/style.css.0629Uhr
Normal file
@@ -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;
|
||||
}
|
||||
58
css/style.css.1413Uhr
Normal file
58
css/style.css.1413Uhr
Normal file
@@ -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; }
|
||||
61
css/style.css.1908Uhr
Normal file
61
css/style.css.1908Uhr
Normal file
@@ -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; }
|
||||
10
includes/auth_check.php
Normal file
10
includes/auth_check.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
if(!isset($_SESSION["loggedin"]) || $_SESSION["loggedin"] !== true){
|
||||
header("location: login.php");
|
||||
exit;
|
||||
}
|
||||
?>
|
||||
18
includes/db_connect.php
Normal file
18
includes/db_connect.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
// Datenbank-Verbindungsdaten
|
||||
define('DB_SERVER', getenv('DB_SERVER') ?: '172.30.242.130');
|
||||
define('DB_USERNAME', getenv('DB_USERNAME') ?: 'cazubu');
|
||||
define('DB_PASSWORD', getenv('DB_PASSWORD') ?: '1nisZvCfl(vpL_[z');
|
||||
define('DB_NAME', getenv('DB_NAME') ?: 'cazubu');
|
||||
|
||||
// Verbindung herstellen mit der mysqli-Bibliothek
|
||||
$mysqli = new mysqli(DB_SERVER, DB_USERNAME, DB_PASSWORD, DB_NAME);
|
||||
|
||||
// Verbindung prüfen
|
||||
if($mysqli === false){
|
||||
die("FEHLER: Konnte keine Verbindung zur Datenbank herstellen. " . $mysqli->connect_error);
|
||||
}
|
||||
|
||||
// Zeichensatz auf UTF-8 setzen
|
||||
$mysqli->set_charset("utf8mb4");
|
||||
?>
|
||||
11
includes/footer.php
Normal file
11
includes/footer.php
Normal file
@@ -0,0 +1,11 @@
|
||||
</main>
|
||||
</div>
|
||||
<div class="modal fade" id="globalActivityModal" tabindex="-1"><div class="modal-dialog modal-lg"><div class="modal-content"><div class="modal-header"><h5 class="modal-title">Aktivität für mehrere Pflanzen durchführen</h5><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body"><form id="global-activity-form"><input type="hidden" name="action" value="add_global_activity"><div class="row"><div class="col-md-6 mb-3"><label class="form-label">Aktivität</label><select class="form-select" name="activity_type" required><option value="Wässern">Wässern</option><option value="Düngen">Düngen</option><option value="Schnitt">Schnitt</option><option value="Sonstiges">Sonstiges</option></select></div><div class="col-md-6 mb-3"><label class="form-label">Datum & Uhrzeit</label><input class="form-control" type="datetime-local" name="activity_date" required></div></div><div class="mb-3"><label class="form-label">Notiz (optional)</label><textarea class="form-control" name="note" rows="2"></textarea></div><hr><div class="mb-3"><label class="form-label">Für welche Pflanzen?</label><div id="plant-target-selection"><p class="text-muted">Bitte zuerst eine Zone auswählen.</p></div></div></form></div><div class="modal-footer justify-content-between"><div><label for="zone-filter-select" class="form-label-sm">Nach Zone filtern:</label><select class="form-select form-select-sm" id="zone-filter-select"><option value="all">Alle aktiven Pflanzen</option></select></div><button type="submit" class="btn btn-primary" form="global-activity-form">Aktion für ausgewählte Pflanzen speichern</button></div></div></div></div>
|
||||
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="https://cdn.datatables.net/2.0.8/js/dataTables.min.js"></script>
|
||||
<script src="https://cdn.datatables.net/2.0.8/js/dataTables.bootstrap5.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<script src="js/main.js?v=<?php echo APP_VERSION; ?>"></script>
|
||||
</body>
|
||||
</html>
|
||||
9
includes/footer.php.1345Uhr
Normal file
9
includes/footer.php.1345Uhr
Normal file
@@ -0,0 +1,9 @@
|
||||
</main>
|
||||
</div>
|
||||
<div class="modal fade" id="globalActivityModal" tabindex="-1"><div class="modal-dialog modal-lg"><div class="modal-content"><div class="modal-header"><h5 class="modal-title">Aktivität für mehrere Pflanzen durchführen</h5><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body"><form id="global-activity-form"><input type="hidden" name="action" value="add_global_activity"><div class="row"><div class="col-md-6 mb-3"><label class="form-label">Aktivität</label><select class="form-select" name="activity_type" required><option value="Wässern">Wässern</option><option value="Düngen">Düngen</option><option value="Schnitt">Schnitt</option><option value="Sonstiges">Sonstiges</option></select></div><div class="col-md-6 mb-3"><label class="form-label">Datum & Uhrzeit</label><input class="form-control" type="datetime-local" name="activity_date" required></div></div><div class="mb-3"><label class="form-label">Notiz (optional)</label><textarea class="form-control" name="note" rows="2"></textarea></div><hr><div class="mb-3"><label class="form-label">Für welche Pflanzen?</label><div id="plant-target-selection"><p class="text-muted">Bitte zuerst eine Zone auswählen.</p></div></div></form></div><div class="modal-footer justify-content-between"><div><label for="zone-filter-select" class="form-label-sm">Nach Zone filtern:</label><select class="form-select form-select-sm" id="zone-filter-select"><option value="all">Alle aktiven Pflanzen</option></select></div><button type="submit" class="btn btn-primary" form="global-activity-form">Aktion für ausgewählte Pflanzen speichern</button></div></div></div></div>
|
||||
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<script src="js/main.js?v=<?php echo APP_VERSION; ?>"></script>
|
||||
</body>
|
||||
</html>
|
||||
53
includes/header.php
Normal file
53
includes/header.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
if (session_status() === PHP_SESSION_NONE) { session_start(); }
|
||||
// Finale Version für Sensor-Seiten-Fix
|
||||
define('APP_VERSION', '18.3.0');
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Cazubu - Cannabis Zucht Buddy</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="css/style.css?v=<?php echo APP_VERSION; ?>">
|
||||
</head>
|
||||
<body>
|
||||
<div class="zoom-effect-container">
|
||||
<div class="zoom-effect-image"></div>
|
||||
</div>
|
||||
<div class="site-container">
|
||||
<nav class="side-menu d-flex flex-column p-3">
|
||||
<div>
|
||||
<div class="text-center">
|
||||
<a href="index.php"><img src="logo.png" alt="Cazubu Logo" class="site-logo"></a>
|
||||
<h2 class="project-name">Cazubu</h2>
|
||||
</div>
|
||||
<hr>
|
||||
<ul class="nav nav-pills flex-column">
|
||||
<?php if(isset($_SESSION["loggedin"]) && $_SESSION["loggedin"] === true): ?>
|
||||
<li><a href="index.php" class="nav-link text-white">Startseite</a></li>
|
||||
<li><a href="plants.php" class="nav-link text-white">Pflanzen</a></li>
|
||||
<li class="mt-2"><span class="nav-link-header">AKTIONEN</span></li>
|
||||
<li><a href="sensors.php" class="nav-link text-white">Sensoren</a></li>
|
||||
<li><a href="#" class="nav-link text-white" data-bs-toggle="modal" data-bs-target="#globalActivityModal">Aktivität durchführen</a></li>
|
||||
<li class="mt-2"><span class="nav-link-header">VERWALTUNG</span></li>
|
||||
<li><a href="inventory.php" class="nav-link text-white">Inventar</a></li>
|
||||
<li><a href="seeds.php" class="nav-link text-white">Samen</a></li>
|
||||
<li class="mt-2"><span class="nav-link-header">SYSTEM</span></li>
|
||||
<li><a href="profile.php" class="nav-link text-white">Profil</a></li>
|
||||
<?php else: ?>
|
||||
<li><a href="login.php" class="nav-link text-white">Login</a></li>
|
||||
<li><a href="register.php" class="nav-link text-white">Registrieren</a></li>
|
||||
<?php endif; ?>
|
||||
</ul>
|
||||
</div>
|
||||
<?php if(isset($_SESSION["loggedin"]) && $_SESSION["loggedin"] === true): ?>
|
||||
<div class="mt-auto">
|
||||
<hr>
|
||||
<div class="text-center text-white-50 small"><p class="mb-1">Angemeldet als:</p><strong class="text-white d-block"><?php echo htmlspecialchars($_SESSION['username']); ?></strong></div>
|
||||
<a href="logout.php" class="btn btn-outline-light w-100 mt-2">Abmelden</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</nav>
|
||||
<main class="main-content">
|
||||
48
includes/header.php.1343Uhr
Normal file
48
includes/header.php.1343Uhr
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
if (session_status() === PHP_SESSION_NONE) { session_start(); }
|
||||
define('APP_VERSION', '12.3.0');
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Cazubu - Cannabis Zucht Buddy</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="css/style.css?v=<?php echo APP_VERSION; ?>">
|
||||
</head>
|
||||
<body>
|
||||
<div class="site-container">
|
||||
<nav class="side-menu d-flex flex-column p-3">
|
||||
<div>
|
||||
<div class="text-center">
|
||||
<a href="index.php"><img src="logo.png" alt="Cazubu Logo" class="site-logo"></a>
|
||||
<h2 class="project-name">Cazubu</h2>
|
||||
</div>
|
||||
<hr>
|
||||
<ul class="nav nav-pills flex-column">
|
||||
<?php if(isset($_SESSION["loggedin"]) && $_SESSION["loggedin"] === true): ?>
|
||||
<li><a href="index.php" class="nav-link text-white">Startseite</a></li>
|
||||
<li><a href="plants.php" class="nav-link text-white">Pflanzen</a></li>
|
||||
<li class="mt-2"><span class="nav-link-header">AKTIONEN</span></li>
|
||||
<li><a href="#" class="nav-link text-white" data-bs-toggle="modal" data-bs-target="#globalActivityModal">Aktivität durchführen</a></li>
|
||||
<li class="mt-2"><span class="nav-link-header">VERWALTUNG</span></li>
|
||||
<li><a href="inventory.php" class="nav-link text-white">Inventar</a></li>
|
||||
<li><a href="seeds.php" class="nav-link text-white">Samen</a></li>
|
||||
<li class="mt-2"><span class="nav-link-header">SYSTEM</span></li>
|
||||
<li><a href="profile.php" class="nav-link text-white">Profil</a></li>
|
||||
<?php else: ?>
|
||||
<li><a href="login.php" class="nav-link text-white">Login</a></li>
|
||||
<li><a href="register.php" class="nav-link text-white">Registrieren</a></li>
|
||||
<?php endif; ?>
|
||||
</ul>
|
||||
</div>
|
||||
<?php if(isset($_SESSION["loggedin"]) && $_SESSION["loggedin"] === true): ?>
|
||||
<div class="mt-auto">
|
||||
<hr>
|
||||
<div class="text-center text-white-50 small"><p class="mb-1">Angemeldet als:</p><strong class="text-white d-block"><?php echo htmlspecialchars($_SESSION['username']); ?></strong></div>
|
||||
<a href="logout.php" class="btn btn-outline-light w-100 mt-2">Abmelden</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</nav>
|
||||
<main class="main-content">
|
||||
50
includes/header.php.1357Uhr
Normal file
50
includes/header.php.1357Uhr
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
if (session_status() === PHP_SESSION_NONE) { session_start(); }
|
||||
// Neue Version für sortierbare Samentabelle
|
||||
define('APP_VERSION', '12.4.0');
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Cazubu - Cannabis Zucht Buddy</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.datatables.net/2.0.8/css/dataTables.bootstrap5.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="css/style.css?v=<?php echo APP_VERSION; ?>">
|
||||
</head>
|
||||
<body>
|
||||
<div class="site-container">
|
||||
<nav class="side-menu d-flex flex-column p-3">
|
||||
<div>
|
||||
<div class="text-center">
|
||||
<a href="index.php"><img src="logo.png" alt="Cazubu Logo" class="site-logo"></a>
|
||||
<h2 class="project-name">Cazubu</h2>
|
||||
</div>
|
||||
<hr>
|
||||
<ul class="nav nav-pills flex-column">
|
||||
<?php if(isset($_SESSION["loggedin"]) && $_SESSION["loggedin"] === true): ?>
|
||||
<li><a href="index.php" class="nav-link text-white">Startseite</a></li>
|
||||
<li><a href="plants.php" class="nav-link text-white">Pflanzen</a></li>
|
||||
<li class="mt-2"><span class="nav-link-header">AKTIONEN</span></li>
|
||||
<li><a href="#" class="nav-link text-white" data-bs-toggle="modal" data-bs-target="#globalActivityModal">Aktivität durchführen</a></li>
|
||||
<li class="mt-2"><span class="nav-link-header">VERWALTUNG</span></li>
|
||||
<li><a href="inventory.php" class="nav-link text-white">Inventar</a></li>
|
||||
<li><a href="seeds.php" class="nav-link text-white">Samen</a></li>
|
||||
<li class="mt-2"><span class="nav-link-header">SYSTEM</span></li>
|
||||
<li><a href="profile.php" class="nav-link text-white">Profil</a></li>
|
||||
<?php else: ?>
|
||||
<li><a href="login.php" class="nav-link text-white">Login</a></li>
|
||||
<li><a href="register.php" class="nav-link text-white">Registrieren</a></li>
|
||||
<?php endif; ?>
|
||||
</ul>
|
||||
</div>
|
||||
<?php if(isset($_SESSION["loggedin"]) && $_SESSION["loggedin"] === true): ?>
|
||||
<div class="mt-auto">
|
||||
<hr>
|
||||
<div class="text-center text-white-50 small"><p class="mb-1">Angemeldet als:</p><strong class="text-white d-block"><?php echo htmlspecialchars($_SESSION['username']); ?></strong></div>
|
||||
<a href="logout.php" class="btn btn-outline-light w-100 mt-2">Abmelden</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</nav>
|
||||
<main class="main-content">
|
||||
120
index.php
Normal file
120
index.php
Normal file
@@ -0,0 +1,120 @@
|
||||
<?php
|
||||
require_once 'includes/auth_check.php';
|
||||
require_once 'includes/db_connect.php';
|
||||
|
||||
$user_id = $_SESSION['user_id'];
|
||||
|
||||
// Statistiken für das Dashboard laden
|
||||
$stats = [
|
||||
'zones' => 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';
|
||||
?>
|
||||
|
||||
<div class="card header-card mb-4">
|
||||
<div class="card-body text-center">
|
||||
<h1 class="display-4">Willkommen, <?php echo htmlspecialchars($_SESSION["username"]); ?>!</h1>
|
||||
<p class="lead text-white-50">Was möchtest du als Nächstes tun?</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-4 mb-4 d-flex">
|
||||
<table class="table cazubu-table-frameless">
|
||||
<thead class="table-dark"><tr><th class="text-center">Schritt 1: Inventar</th></tr></thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="d-flex flex-column text-center p-4">
|
||||
<p class="text-muted">Lege deine Anbau-Zonen und Pflanzgefäße an.</p>
|
||||
<?php if ($stats['zones'] > 0): ?>
|
||||
<div class="my-auto">
|
||||
<div class="display-4 fw-bold text-dark"><?php echo $stats['zones']; ?></div>
|
||||
<div class="h5 text-dark">Zone(n) angelegt</div>
|
||||
</div>
|
||||
<a href="inventory.php" class="btn btn-outline-dark mt-3">Zum Inventar</a>
|
||||
<?php else: ?>
|
||||
<div class="my-auto p-4">
|
||||
<a href="inventory.php" class="btn btn-cazubu btn-lg mt-2">Inventar anlegen</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4 mb-4 d-flex">
|
||||
<table class="table cazubu-table-frameless">
|
||||
<thead class="table-dark"><tr><th class="text-center">Schritt 2: Samenbank</th></tr></thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="d-flex flex-column text-center p-4">
|
||||
<p class="text-muted">Erfasse alle Samen, die du auf Lager hast.</p>
|
||||
<?php if ($stats['seeds'] > 0): ?>
|
||||
<div class="my-auto">
|
||||
<div class="display-4 fw-bold text-dark"><?php echo $stats['seeds']; ?></div>
|
||||
<div class="h5 text-dark">Samen auf Lager</div>
|
||||
</div>
|
||||
<a href="seeds.php" class="btn btn-outline-dark mt-3">Zur Samenbank</a>
|
||||
<?php else: ?>
|
||||
<div class="my-auto p-4">
|
||||
<a href="seeds.php" class="btn btn-cazubu btn-lg mt-2">Samen erfassen</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4 mb-4 d-flex">
|
||||
<table class="table cazubu-table-frameless">
|
||||
<thead class="table-dark"><tr><th class="text-center">Schritt 3: Pflanzen</th></tr></thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="d-flex flex-column text-center p-4">
|
||||
<p class="text-muted">Lege deine Pflanzen an und verfolge ihren Fortschritt.</p>
|
||||
<?php if ($stats['plants'] > 0): ?>
|
||||
<div class="my-auto">
|
||||
<div class="display-4 fw-bold text-dark"><?php echo $stats['plants']; ?></div>
|
||||
<div class="h5 text-dark">Pflanze(n) wachsen</div>
|
||||
</div>
|
||||
<a href="plants.php" class="btn btn-outline-dark mt-3">Zu den Pflanzen</a>
|
||||
<?php else: ?>
|
||||
<div class="my-auto p-4">
|
||||
<a href="plants.php" class="btn btn-cazubu btn-lg mt-2">+ Erste Pflanze anlegen</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
require_once 'includes/footer.php';
|
||||
?>
|
||||
90
inventory.php
Normal file
90
inventory.php
Normal file
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
require_once 'includes/auth_check.php';
|
||||
require_once 'includes/db_connect.php';
|
||||
$user_id = $_SESSION['user_id'];
|
||||
$zones = [];
|
||||
$containers = [];
|
||||
$sql_zones = "SELECT id, name FROM zones WHERE user_id = ? ORDER BY name ASC";
|
||||
if ($stmt_zones = $mysqli->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';
|
||||
?>
|
||||
<div class="card header-card mb-4">
|
||||
<div class="card-body">
|
||||
<h1 class="mb-0">Inventar-Verwaltung</h1>
|
||||
<p class="card-text text-white-50 mt-2">Verwalten Sie hier Ihre Zonen und Pflanzgefäße.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-lg-6 mb-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h3 class="text-white mb-0">Zonen</h3>
|
||||
<button class="btn btn-sm btn-cazubu" data-bs-toggle="modal" data-bs-target="#zoneModal">+ Neue Zone</button>
|
||||
</div>
|
||||
<table class="table table-hover cazubu-table-frameless">
|
||||
<thead class="table-dark">
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th style="width: 120px;">Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($zones)) echo '<tr><td colspan="2" class="text-center p-4"><i>Keine Zonen angelegt.</i></td></tr>'; ?>
|
||||
<?php foreach ($zones as $zone): ?>
|
||||
<tr>
|
||||
<td class="align-middle"><?php echo htmlspecialchars($zone['name']); ?></td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-outline-primary edit-zone-btn" data-bs-toggle="modal" data-bs-target="#zoneModal" data-id="<?php echo $zone['id']; ?>" data-name="<?php echo htmlspecialchars($zone['name']); ?>">✏️</button>
|
||||
<button class="btn btn-sm btn-outline-danger delete-btn" data-bs-toggle="modal" data-bs-target="#deleteConfirmModal" data-id="<?php echo $zone['id']; ?>" data-name="<?php echo htmlspecialchars($zone['name']); ?>" data-type="zone">🗑️</button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-lg-6 mb-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h3 class="text-white mb-0">Pflanzgefäße</h3>
|
||||
<button class="btn btn-sm btn-cazubu" data-bs-toggle="modal" data-bs-target="#containerModal">+ Neues Pflanzgefäß</button>
|
||||
</div>
|
||||
<table class="table table-hover cazubu-table-frameless">
|
||||
<thead class="table-dark">
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Zone</th>
|
||||
<th style="width: 120px;">Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($containers)) echo '<tr><td colspan="3" class="text-center p-4"><i>Keine Pflanzgefäße angelegt.</i></td></tr>'; ?>
|
||||
<?php foreach ($containers as $container): ?>
|
||||
<tr>
|
||||
<td class="align-middle"><?php echo htmlspecialchars($container['name']); ?></td>
|
||||
<td class="align-middle"><span class="badge text-bg-dark"><?php echo htmlspecialchars($container['zone_name']); ?></span></td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-outline-primary edit-container-btn" data-bs-toggle="modal" data-bs-target="#containerModal" data-id="<?php echo $container['id']; ?>" data-name="<?php echo htmlspecialchars($container['name']); ?>" data-zoneid="<?php echo $container['zone_id']; ?>">✏️</button>
|
||||
<button class="btn btn-sm btn-outline-danger delete-btn" data-bs-toggle="modal" data-bs-target="#deleteConfirmModal" data-id="<?php echo $container['id']; ?>" data-name="<?php echo htmlspecialchars($container['name']); ?>" data-type="container">🗑️</button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal fade" id="zoneModal" tabindex="-1"><div class="modal-dialog"><div class="modal-content"><div class="modal-header"><h5 class="modal-title" id="zoneModalLabel">Zone</h5><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body"><form id="zone-form"><input type="hidden" name="action" id="zone-form-action"><input type="hidden" name="id" id="zone-id"><div class="mb-3"><label for="zone-name" class="form-label">Name der Zone</label><input type="text" class="form-control" id="zone-name" name="name" required></div></form></div><div class="modal-footer"><button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Schließen</button><button type="submit" class="btn btn-primary" form="zone-form">Speichern</button></div></div></div></div>
|
||||
<div class="modal fade" id="containerModal" tabindex="-1"><div class="modal-dialog"><div class="modal-content"><div class="modal-header"><h5 class="modal-title" id="containerModalLabel">Pflanzgefäß</h5><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body"><form id="container-form"><input type="hidden" name="action" id="container-form-action"><input type="hidden" name="id" id="container-id"><div class="mb-3"><label class="form-label">Name des Pflanzgefäßes</label><input type="text" class="form-control" name="name" required></div><div class="mb-3"><label class="form-label">Zugehörige Zone</label><select class="form-select" name="zone_id" required><option value="">Bitte wählen...</option><?php foreach ($zones as $zone) { echo "<option value='{$zone['id']}'>" . htmlspecialchars($zone['name']) . "</option>"; } ?></select></div></form></div><div class="modal-footer"><button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Schließen</button><button type="submit" class="btn btn-primary" form="container-form">Speichern</button></div></div></div></div>
|
||||
<div class="modal fade" id="deleteConfirmModal" tabindex="-1"><div class="modal-dialog"><div class="modal-content"><div class="modal-header"><h5 class="modal-title">Löschen bestätigen</h5><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body"><p>Sind Sie sicher, dass Sie <strong id="item-type-to-delete"></strong> "<strong id="item-name-to-delete"></strong>" endgültig löschen möchten?</p><p class="text-danger" id="delete-warning"></p></div><div class="modal-footer"><button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button><button type="button" class="btn btn-danger" id="confirmDeleteBtn">Ja, endgültig löschen</button></div></div></div></div>
|
||||
<?php require_once 'includes/footer.php'; ?>
|
||||
97
js/main.js
Normal file
97
js/main.js
Normal file
@@ -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('<option value="">Lade Gefäße...</option>'); if (!zoneId) { containerSelect.html('<option value="">Zuerst Zone wählen</option>'); 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('<option value="">Pflanzgefäß wählen</option>'); if (isEditMode && preselectContainerId) { const plantData = $('.edit-plant-btn').data('plant-info'); containerSelect.append($('<option>', { value: plantData.container_id, text: plantData.container_name, selected: true })); } if (response.data.length > 0) { response.data.forEach(function(container) { if (!isEditMode || container.id != preselectContainerId) { containerSelect.append($('<option>', { value: container.id, text: container.name })); } }); } if(preselectContainerId) { containerSelect.val(preselectContainerId); } containerSelect.prop('disabled', false); } else { containerSelect.html('<option value="">Fehler</option>'); } }, error: function() { containerSelect.html('<option value="">Serverfehler!</option>'); } }); });
|
||||
|
||||
// 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($('<option>', { value: zone.id, text: zone.name })); }); } } }); } 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); zoneFilterSelect.val('all').trigger('change'); }); }
|
||||
$('#zone-filter-select').on('change', function() { const zoneId = $(this).val(); const selectionContainer = $('#plant-target-selection'); selectionContainer.html('<div class="spinner-border spinner-border-sm" role="status"><span class="visually-hidden">Lade...</span></div>'); $.ajax({ type: 'GET', url: 'ajax_handler.php', data: { action: 'get_plants_by_zone', zone_id: zoneId }, dataType: 'json', success: function(response) { selectionContainer.empty(); if (response.success && response.data.length > 0) { let plantCheckboxes = '<div class="form-check"><input class="form-check-input" type="checkbox" id="select-all-plants" checked><label class="form-check-label fw-bold" for="select-all-plants">Alle auswählen/abwählen</label></div><hr class="my-2">'; plantCheckboxes += '<div class="row" style="max-height: 200px; overflow-y: auto;">'; response.data.forEach(function(plant) { plantCheckboxes += `<div class="col-md-6"><div class="form-check"><input class="form-check-input plant-checkbox" type="checkbox" name="plant_ids[]" value="${plant.id}" id="plant_${plant.id}" checked><label class="form-check-label" for="plant_${plant.id}">${plant.strain_name} (${plant.container_name})</label></div></div>`; }); plantCheckboxes += '</div>'; selectionContainer.html(plantCheckboxes); } else { selectionContainer.html('<p class="text-muted">Keine aktiven Pflanzen in dieser Auswahl gefunden.</p>'); } } }); });
|
||||
$(document).on('change', '#select-all-plants', function() { $('#global-activity-form .plant-checkbox').prop('checked', $(this).prop('checked')); });
|
||||
|
||||
// Sensor-Graphen Logik
|
||||
$('#sensor-tab-btn').on('shown.bs.tab', function () {
|
||||
if ($(this).data('loaded')) { return; }
|
||||
$(this).data('loaded', true);
|
||||
const plantId = new URLSearchParams(window.location.search).get('id');
|
||||
const chartsContainer = $('#sensor-charts-container');
|
||||
chartsContainer.html('<div class="col-12 text-center p-5"><div class="spinner-border text-secondary" role="status"><span class="visually-hidden">Lade...</span></div></div>');
|
||||
$.ajax({
|
||||
type: 'GET', url: 'ajax_handler.php', data: { action: 'get_sensor_data', plant_id: plantId }, dataType: 'json',
|
||||
success: function(response) {
|
||||
chartsContainer.empty();
|
||||
if (response.success && response.data.labels.length > 0) {
|
||||
chartsContainer.html('<div class="col-lg-6 mb-4" id="temp-chart-wrapper"></div><div class="col-lg-6 mb-4" id="humidity-chart-wrapper"></div>');
|
||||
const tempWrapper = $('#temp-chart-wrapper');
|
||||
const humidityWrapper = $('#humidity-chart-wrapper');
|
||||
if (response.data.temperature.some(val => val !== null)) {
|
||||
tempWrapper.append('<h5>Temperaturverlauf</h5>');
|
||||
let tempCanvas = $('<canvas>').attr('height', '300');
|
||||
tempWrapper.append($('<div>').addClass('cazubu-table-frameless p-2').append(tempCanvas));
|
||||
new Chart(tempCanvas, { type: 'line', data: { labels: response.data.labels, datasets: [{ label: 'Temperatur (°C)', data: response.data.temperature, borderColor: 'rgba(255, 99, 132, 1)', backgroundColor: 'rgba(255, 99, 132, 0.2)', fill: true, tension: 0.2, spanGaps: true }] }, options: { maintainAspectRatio: false, scales: { x: { ticks: { color: '#212529' } }, y: { ticks: { color: '#212529' } } }, plugins: { legend: { labels: { color: '#212529' } } } } });
|
||||
} else { tempWrapper.append('<div class="cazubu-table-frameless p-3 text-center">Keine Temperaturdaten vorhanden.</div>'); }
|
||||
if (response.data.humidity.some(val => val !== null)) {
|
||||
humidityWrapper.append('<h5>Feuchtigkeitsverlauf</h5>');
|
||||
let humidityCanvas = $('<canvas>').attr('height', '300');
|
||||
humidityWrapper.append($('<div>').addClass('cazubu-table-frameless p-2').append(humidityCanvas));
|
||||
new Chart(humidityCanvas, { type: 'line', data: { labels: response.data.labels, datasets: [{ label: 'Feuchtigkeit (%)', data: response.data.humidity, borderColor: 'rgba(54, 162, 235, 1)', backgroundColor: 'rgba(54, 162, 235, 0.2)', fill: true, tension: 0.2, spanGaps: true }] }, options: { maintainAspectRatio: false, scales: { x: { ticks: { color: '#212529' } }, y: { ticks: { color: '#212529' } } }, plugins: { legend: { labels: { color: '#212529' } } } } });
|
||||
} else { humidityWrapper.append('<div class="cazubu-table-frameless p-3 text-center">Keine Feuchtigkeitsdaten vorhanden.</div>'); }
|
||||
} else { chartsContainer.html('<div class="col-12"><div class="cazubu-table-frameless p-3 text-center">Keine Sensordaten für diese Pflanze vorhanden.</div></div>'); }
|
||||
},
|
||||
error: function() { chartsContainer.html('<div class="col-12"><div class="cazubu-table-frameless p-3 text-center text-danger">Fehler beim Laden der Sensordaten.</div></div>'); }
|
||||
});
|
||||
});
|
||||
});
|
||||
90
js/main.js.0629Uhr
Normal file
90
js/main.js.0629Uhr
Normal file
@@ -0,0 +1,90 @@
|
||||
$(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 für die Samen-Tabelle initialisieren
|
||||
if ($('#seeds-table').length) {
|
||||
$('#seeds-table').DataTable({
|
||||
"dom": "t", // 't' bedeutet, dass NUR die Tabelle gerendert wird, ohne extra Controls
|
||||
"paging": false,
|
||||
"info": false,
|
||||
"language": {
|
||||
"zeroRecords": "Keine passenden Samen gefunden",
|
||||
},
|
||||
"columnDefs": [
|
||||
{ "orderable": false, "targets": 'no-sort' }
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
// Zonen Logik
|
||||
$('#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'); } });
|
||||
$('#zone-form').on('submit', function(e) { e.preventDefault(); handleFormSubmit($(this)); });
|
||||
|
||||
// Pflanzgefäß Logik
|
||||
$('#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'); } });
|
||||
$('#container-form').on('submit', function(e) { e.preventDefault(); handleFormSubmit($(this)); });
|
||||
|
||||
// Samen Logik
|
||||
$('#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'); } });
|
||||
$('#seed-form').on('submit', function(e) { e.preventDefault(); handleFormSubmit($(this)); });
|
||||
$('#ratio_sativa').on('input', function() { let sativa = $(this).val(); $('#sativa-value-label').text(sativa); $('#indica-value-label').text(100 - sativa); });
|
||||
|
||||
// Pflanzen Logik
|
||||
$('#plant-form, #edit-plant-form, #activity-form, #measurement-form, #global-activity-form').on('submit', function(e) { e.preventDefault(); handleFormSubmit($(this)); });
|
||||
$('#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.'); } }); });
|
||||
$('#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]); });
|
||||
$('#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('<option value="">Lade Gefäße...</option>'); if (!zoneId) { containerSelect.html('<option value="">Zuerst Zone wählen</option>'); 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('<option value="">Pflanzgefäß wählen</option>'); if (isEditMode && preselectContainerId) { const plantData = $('.edit-plant-btn').data('plant-info'); containerSelect.append($('<option>', { value: plantData.container_id, text: plantData.container_name })); } if (response.data.length > 0) { response.data.forEach(function(container) { if (!isEditMode || container.id != preselectContainerId) { containerSelect.append($('<option>', { value: container.id, text: container.name })); } }); } if(preselectContainerId) { containerSelect.val(preselectContainerId); } containerSelect.prop('disabled', false); } else { containerSelect.html('<option value="">Fehler</option>'); } }, error: function() { containerSelect.html('<option value="">Serverfehler!</option>'); } }); });
|
||||
$('#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!') }); } });
|
||||
$('#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); });
|
||||
|
||||
// Profil-Seiten Logik
|
||||
$('#generate-api-key-form').on('submit', function(e) { e.preventDefault(); if (confirm('Bist du sicher? Ein neuer Key macht einen eventuell bestehenden ungültig.')) { handleFormSubmit($(this)); } });
|
||||
$('#change-username-form').on('submit', function(e) { e.preventDefault(); handleFormSubmit($(this)); });
|
||||
$('#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!') }); });
|
||||
|
||||
// Sensor-Graphen Logik
|
||||
$('#sensor-tab-btn').on('shown.bs.tab', function () {
|
||||
if ($(this).data('loaded')) { return; }
|
||||
$(this).data('loaded', true);
|
||||
const plantId = new URLSearchParams(window.location.search).get('id');
|
||||
const chartsContainer = $('#sensor-charts-container');
|
||||
chartsContainer.html('<div class="col-12 text-center p-5"><div class="spinner-border text-secondary" role="status"><span class="visually-hidden">Lade...</span></div></div>');
|
||||
$.ajax({
|
||||
type: 'GET', url: 'ajax_handler.php', data: { action: 'get_sensor_data', plant_id: plantId }, dataType: 'json',
|
||||
success: function(response) {
|
||||
chartsContainer.empty();
|
||||
if (response.success && response.data.labels.length > 0) {
|
||||
chartsContainer.html('<div class="col-lg-6" id="temp-chart-wrapper"></div><div class="col-lg-6" id="humidity-chart-wrapper"></div>');
|
||||
const tempWrapper = $('#temp-chart-wrapper'); const humidityWrapper = $('#humidity-chart-wrapper');
|
||||
if (response.data.temperature.some(val => val !== null)) {
|
||||
tempWrapper.append('<h5>Temperaturverlauf</h5>'); let tempCanvas = $('<canvas>').attr('height', '300'); tempWrapper.append($('<div>').addClass('mb-4').append(tempCanvas));
|
||||
new Chart(tempCanvas, { type: 'line', data: { labels: response.data.labels, datasets: [{ label: 'Temperatur (°C)', data: response.data.temperature, borderColor: 'rgba(255, 99, 132, 1)', backgroundColor: 'rgba(255, 99, 132, 0.2)', fill: true, tension: 0.2, spanGaps: true }] }, options: { maintainAspectRatio: false } });
|
||||
} else { tempWrapper.append('<p class="text-muted">Keine Temperaturdaten vorhanden.</p>'); }
|
||||
if (response.data.humidity.some(val => val !== null)) {
|
||||
humidityWrapper.append('<h5>Feuchtigkeitsverlauf</h5>'); let humidityCanvas = $('<canvas>').attr('height', '300'); humidityWrapper.append($('<div>').addClass('mb-4').append(humidityCanvas));
|
||||
new Chart(humidityCanvas, { type: 'line', data: { labels: response.data.labels, datasets: [{ label: 'Feuchtigkeit (%)', data: response.data.humidity, borderColor: 'rgba(54, 162, 235, 1)', backgroundColor: 'rgba(54, 162, 235, 0.2)', fill: true, tension: 0.2, spanGaps: true }] }, options: { maintainAspectRatio: false } });
|
||||
} else { humidityWrapper.append('<p class="text-muted">Keine Feuchtigkeitsdaten vorhanden.</p>'); }
|
||||
} else { chartsContainer.html('<div class="col-12"><p class="text-muted">Keine Sensordaten für diese Pflanze vorhanden.</p></div>'); }
|
||||
},
|
||||
error: function() { chartsContainer.html('<div class="col-12"><p class="text-danger">Fehler beim Laden der Sensordaten.</p></div>'); }
|
||||
});
|
||||
});
|
||||
|
||||
// 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($('<option>', { value: zone.id, text: zone.name })); }); } } }); } 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); zoneFilterSelect.val('all').trigger('change'); }); }
|
||||
$('#zone-filter-select').on('change', function() { const zoneId = $(this).val(); const selectionContainer = $('#plant-target-selection'); selectionContainer.html('<div class="spinner-border spinner-border-sm" role="status"><span class="visually-hidden">Lade...</span></div>'); $.ajax({ type: 'GET', url: 'ajax_handler.php', data: { action: 'get_plants_by_zone', zone_id: zoneId }, dataType: 'json', success: function(response) { selectionContainer.empty(); if (response.success && response.data.length > 0) { let plantCheckboxes = '<div class="form-check"><input class="form-check-input" type="checkbox" id="select-all-plants" checked><label class="form-check-label fw-bold" for="select-all-plants">Alle auswählen/abwählen</label></div><hr class="my-2">'; plantCheckboxes += '<div class="row" style="max-height: 200px; overflow-y: auto;">'; response.data.forEach(function(plant) { plantCheckboxes += `<div class="col-md-6"><div class="form-check"><input class="form-check-input plant-checkbox" type="checkbox" name="plant_ids[]" value="${plant.id}" id="plant_${plant.id}" checked><label class="form-check-label" for="plant_${plant.id}">${plant.strain_name} (${plant.container_name})</label></div></div>`; }); plantCheckboxes += '</div>'; selectionContainer.html(plantCheckboxes); } else { selectionContainer.html('<p class="text-muted">Keine aktiven Pflanzen in dieser Auswahl gefunden.</p>'); } } }); });
|
||||
$(document).on('change', '#select-all-plants', function() { $('#global-activity-form .plant-checkbox').prop('checked', $(this).prop('checked')); });
|
||||
|
||||
// Universelle Logik
|
||||
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!') }); });
|
||||
function handleFormSubmit(form) { $.ajax({ type: 'POST', url: 'ajax_handler.php', data: form.serialize(), dataType: 'json', success: function(response) { if (response.success) { location.reload(); } else { alert('Speichern fehlgeschlagen: ' + (response.message || 'Unbekannter Fehler')); } }, error: () => alert('Serverfehler!') }); }
|
||||
});
|
||||
78
js/main.js.1310Uhr
Normal file
78
js/main.js.1310Uhr
Normal file
@@ -0,0 +1,78 @@
|
||||
$(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));
|
||||
|
||||
// Zonen Logik
|
||||
$('#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'); } });
|
||||
$('#zone-form').on('submit', function(e) { e.preventDefault(); handleFormSubmit($(this)); });
|
||||
|
||||
// Pflanzgefäß Logik
|
||||
$('#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'); } });
|
||||
$('#container-form').on('submit', function(e) { e.preventDefault(); handleFormSubmit($(this)); });
|
||||
|
||||
// Samen Logik
|
||||
$('#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'); } });
|
||||
$('#seed-form').on('submit', function(e) { e.preventDefault(); handleFormSubmit($(this)); });
|
||||
$('#ratio_sativa').on('input', function() { let sativa = $(this).val(); $('#sativa-value-label').text(sativa); $('#indica-value-label').text(100 - sativa); });
|
||||
|
||||
// Pflanzen Logik
|
||||
$('#plant-form, #edit-plant-form, #activity-form, #measurement-form, #global-activity-form').on('submit', function(e) { e.preventDefault(); handleFormSubmit($(this)); });
|
||||
$('#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.'); } }); });
|
||||
$('#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]); });
|
||||
$('#zone-select, #edit-zone-select').on('change', function(event, preselectContainerId) { const zoneId = $(this).val(); const containerSelect = $(this).attr('id') === 'zone-select' ? $('#container-select') : $('#edit-container-select'); containerSelect.prop('disabled', true).html('<option value="">Lade Gefäße...</option>'); if (!zoneId) { containerSelect.html('<option value="">Zuerst Zone wählen</option>'); 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('<option value="">Pflanzgefäß wählen</option>'); if (isEditMode && preselectContainerId) { const plantData = $('.edit-plant-btn').data('plant-info'); containerSelect.append($('<option>', { value: plantData.container_id, text: plantData.container_name })); } if (response.data.length > 0) { response.data.forEach(function(container) { if (!isEditMode || container.id != preselectContainerId) { containerSelect.append($('<option>', { value: container.id, text: container.name })); } }); } if(preselectContainerId) { containerSelect.val(preselectContainerId); } containerSelect.prop('disabled', false); } else { containerSelect.html('<option value="">Fehler</option>'); } }, error: function() { containerSelect.html('<option value="">Serverfehler!</option>'); } }); });
|
||||
$('#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!') }); } });
|
||||
$('#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); });
|
||||
|
||||
// Profil-Seiten Logik
|
||||
$('#generate-api-key-form').on('submit', function(e) { e.preventDefault(); if (confirm('Bist du sicher? Ein neuer Key macht einen eventuell bestehenden ungültig.')) { handleFormSubmit($(this)); } });
|
||||
$('#change-username-form').on('submit', function(e) { e.preventDefault(); handleFormSubmit($(this)); });
|
||||
$('#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!') }); });
|
||||
|
||||
// Sensor-Graphen Logik
|
||||
const sensorTabBtn = document.querySelector('#sensor-tab-btn');
|
||||
if (sensorTabBtn) {
|
||||
sensorTabBtn.addEventListener('shown.bs.tab', function () {
|
||||
if ($(this).data('loaded')) { return; }
|
||||
$(this).data('loaded', true);
|
||||
const plantId = new URLSearchParams(window.location.search).get('id');
|
||||
const chartsContainer = $('#sensor-charts-container');
|
||||
chartsContainer.html('<div class="col-12 text-center p-5"><div class="spinner-border text-secondary" role="status"><span class="visually-hidden">Lade...</span></div></div>');
|
||||
$.ajax({
|
||||
type: 'GET', url: 'ajax_handler.php', data: { action: 'get_sensor_data', plant_id: plantId }, dataType: 'json',
|
||||
success: function(response) {
|
||||
chartsContainer.empty();
|
||||
if (response.success && response.data.labels.length > 0) {
|
||||
chartsContainer.html('<div class="col-md-6" id="temp-chart-wrapper"></div><div class="col-md-6" id="humidity-chart-wrapper"></div>');
|
||||
const tempWrapper = $('#temp-chart-wrapper'); const humidityWrapper = $('#humidity-chart-wrapper');
|
||||
if (response.data.temperature.some(val => val !== null)) {
|
||||
tempWrapper.append('<h5>Temperaturverlauf</h5>'); let tempCanvas = $('<canvas>').attr('height', '300'); tempWrapper.append($('<div>').addClass('mb-4').append(tempCanvas));
|
||||
new Chart(tempCanvas, { type: 'line', data: { labels: response.data.labels, datasets: [{ label: 'Temperatur (°C)', data: response.data.temperature, borderColor: 'rgba(255, 99, 132, 1)', backgroundColor: 'rgba(255, 99, 132, 0.2)', fill: true, tension: 0.2, spanGaps: true }] }, options: { maintainAspectRatio: false } });
|
||||
} else { tempWrapper.append('<p class="text-muted">Keine Temperaturdaten vorhanden.</p>'); }
|
||||
if (response.data.humidity.some(val => val !== null)) {
|
||||
humidityWrapper.append('<h5>Feuchtigkeitsverlauf</h5>'); let humidityCanvas = $('<canvas>').attr('height', '300'); humidityWrapper.append($('<div>').addClass('mb-4').append(humidityCanvas));
|
||||
new Chart(humidityCanvas, { type: 'line', data: { labels: response.data.labels, datasets: [{ label: 'Feuchtigkeit (%)', data: response.data.humidity, borderColor: 'rgba(54, 162, 235, 1)', backgroundColor: 'rgba(54, 162, 235, 0.2)', fill: true, tension: 0.2, spanGaps: true }] }, options: { maintainAspectRatio: false } });
|
||||
} else { humidityWrapper.append('<p class="text-muted">Keine Feuchtigkeitsdaten vorhanden.</p>'); }
|
||||
} else { chartsContainer.html('<div class="col-12"><p class="text-muted">Keine Sensordaten für diese Pflanze vorhanden.</p></div>'); }
|
||||
},
|
||||
error: function() { chartsContainer.html('<div class="col-12"><p class="text-danger">Fehler beim Laden der Sensordaten.</p></div>'); }
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 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($('<option>', { value: zone.id, text: zone.name })); }); } } }); } 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); zoneFilterSelect.val('all').trigger('change'); }); }
|
||||
$('#zone-filter-select').on('change', function() { const zoneId = $(this).val(); const selectionContainer = $('#plant-target-selection'); selectionContainer.html('<div class="spinner-border spinner-border-sm" role="status"><span class="visually-hidden">Lade...</span></div>'); $.ajax({ type: 'GET', url: 'ajax_handler.php', data: { action: 'get_plants_by_zone', zone_id: zoneId }, dataType: 'json', success: function(response) { selectionContainer.empty(); if (response.success && response.data.length > 0) { let plantCheckboxes = '<div class="form-check"><input class="form-check-input" type="checkbox" id="select-all-plants" checked><label class="form-check-label fw-bold" for="select-all-plants">Alle auswählen/abwählen</label></div><hr class="my-2">'; plantCheckboxes += '<div class="row" style="max-height: 200px; overflow-y: auto;">'; response.data.forEach(function(plant) { plantCheckboxes += `<div class="col-md-6"><div class="form-check"><input class="form-check-input plant-checkbox" type="checkbox" name="plant_ids[]" value="${plant.id}" id="plant_${plant.id}" checked><label class="form-check-label" for="plant_${plant.id}">${plant.strain_name} (${plant.container_name})</label></div></div>`; }); plantCheckboxes += '</div>'; selectionContainer.html(plantCheckboxes); } else { selectionContainer.html('<p class="text-muted">Keine aktiven Pflanzen in dieser Auswahl gefunden.</p>'); } } }); });
|
||||
$(document).on('change', '#select-all-plants', function() { $('#global-activity-form .plant-checkbox').prop('checked', $(this).prop('checked')); });
|
||||
|
||||
// Universelle Logik
|
||||
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!') }); });
|
||||
function handleFormSubmit(form) { $.ajax({ type: 'POST', url: 'ajax_handler.php', data: form.serialize(), dataType: 'json', success: function(response) { if (response.success) { location.reload(); } else { alert('Speichern fehlgeschlagen: ' + (response.message || 'Unbekannter Fehler')); } }, error: () => alert('Serverfehler!') }); }
|
||||
});
|
||||
293
js/main.js.1346Uhr
Normal file
293
js/main.js.1346Uhr
Normal file
@@ -0,0 +1,293 @@
|
||||
$(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));
|
||||
|
||||
// Zonen Logik
|
||||
$('#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');
|
||||
}
|
||||
});
|
||||
$('#zone-form').on('submit', function(e) { e.preventDefault(); handleFormSubmit($(this)); });
|
||||
|
||||
// Pflanzgefäß Logik
|
||||
$('#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');
|
||||
}
|
||||
});
|
||||
$('#container-form').on('submit', function(e) { e.preventDefault(); handleFormSubmit($(this)); });
|
||||
|
||||
// Samen Logik
|
||||
$('#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');
|
||||
}
|
||||
});
|
||||
$('#seed-form').on('submit', function(e) { e.preventDefault(); handleFormSubmit($(this)); });
|
||||
$('#ratio_sativa').on('input', function() { let sativa = $(this).val(); $('#sativa-value-label').text(sativa); $('#indica-value-label').text(100 - sativa); });
|
||||
|
||||
// Pflanzen Logik
|
||||
$('#plant-form, #edit-plant-form, #activity-form, #measurement-form, #global-activity-form').on('submit', function(e) { e.preventDefault(); handleFormSubmit($(this)); });
|
||||
|
||||
$('#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.'); }
|
||||
});
|
||||
});
|
||||
|
||||
$('#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]);
|
||||
});
|
||||
|
||||
$('#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('<option value="">Lade Gefäße...</option>');
|
||||
if (!zoneId) {
|
||||
containerSelect.html('<option value="">Zuerst Zone wählen</option>');
|
||||
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('<option value="">Pflanzgefäß wählen</option>');
|
||||
if (isEditMode && preselectContainerId) {
|
||||
const plantData = $('.edit-plant-btn').data('plant-info');
|
||||
containerSelect.append($('<option>', { value: plantData.container_id, text: plantData.container_name }));
|
||||
}
|
||||
if (response.data.length > 0) {
|
||||
response.data.forEach(function(container) {
|
||||
if (!isEditMode || container.id != preselectContainerId) {
|
||||
containerSelect.append($('<option>', { value: container.id, text: container.name }));
|
||||
}
|
||||
});
|
||||
}
|
||||
if(preselectContainerId) { containerSelect.val(preselectContainerId); }
|
||||
containerSelect.prop('disabled', false);
|
||||
} else { containerSelect.html('<option value="">Fehler</option>'); }
|
||||
},
|
||||
error: function() { containerSelect.html('<option value="">Serverfehler!</option>'); }
|
||||
});
|
||||
});
|
||||
|
||||
$('#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!') }); } });
|
||||
$('#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); });
|
||||
|
||||
// Profil-Seiten Logik
|
||||
$('#generate-api-key-form').on('submit', function(e) { e.preventDefault(); if (confirm('Bist du sicher? Ein neuer Key macht einen eventuell bestehenden ungültig.')) { handleFormSubmit($(this)); } });
|
||||
$('#change-username-form').on('submit', function(e) { e.preventDefault(); handleFormSubmit($(this)); });
|
||||
$('#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!') }); });
|
||||
|
||||
// Sensor-Graphen Logik
|
||||
const sensorTabBtn = document.querySelector('#sensor-tab-btn');
|
||||
if (sensorTabBtn) {
|
||||
sensorTabBtn.addEventListener('shown.bs.tab', function () {
|
||||
if ($(this).data('loaded')) { return; }
|
||||
$(this).data('loaded', true);
|
||||
const plantId = new URLSearchParams(window.location.search).get('id');
|
||||
const chartsContainer = $('#sensor-charts-container');
|
||||
chartsContainer.html('<div class="col-12 text-center p-5"><div class="spinner-border text-secondary" role="status"><span class="visually-hidden">Lade...</span></div></div>');
|
||||
$.ajax({
|
||||
type: 'GET', url: 'ajax_handler.php', data: { action: 'get_sensor_data', plant_id: plantId }, dataType: 'json',
|
||||
success: function(response) {
|
||||
chartsContainer.empty();
|
||||
if (response.success && response.data.labels.length > 0) {
|
||||
chartsContainer.html('<div class="col-lg-6" id="temp-chart-wrapper"></div><div class="col-lg-6" id="humidity-chart-wrapper"></div>');
|
||||
const tempWrapper = $('#temp-chart-wrapper');
|
||||
const humidityWrapper = $('#humidity-chart-wrapper');
|
||||
|
||||
if (response.data.temperature.some(val => val !== null)) {
|
||||
tempWrapper.append('<h5>Temperaturverlauf</h5>');
|
||||
let tempCanvas = $('<canvas>').attr('height', '300');
|
||||
tempWrapper.append($('<div>').addClass('mb-4').append(tempCanvas));
|
||||
new Chart(tempCanvas, { type: 'line', data: { labels: response.data.labels, datasets: [{ label: 'Temperatur (°C)', data: response.data.temperature, borderColor: 'rgba(255, 99, 132, 1)', backgroundColor: 'rgba(255, 99, 132, 0.2)', fill: true, tension: 0.2, spanGaps: true }] }, options: { maintainAspectRatio: false } });
|
||||
} else {
|
||||
tempWrapper.append('<p class="text-muted">Keine Temperaturdaten vorhanden.</p>');
|
||||
}
|
||||
|
||||
if (response.data.humidity.some(val => val !== null)) {
|
||||
humidityWrapper.append('<h5>Feuchtigkeitsverlauf</h5>');
|
||||
let humidityCanvas = $('<canvas>').attr('height', '300');
|
||||
humidityWrapper.append($('<div>').addClass('mb-4').append(humidityCanvas));
|
||||
new Chart(humidityCanvas, { type: 'line', data: { labels: response.data.labels, datasets: [{ label: 'Feuchtigkeit (%)', data: response.data.humidity, borderColor: 'rgba(54, 162, 235, 1)', backgroundColor: 'rgba(54, 162, 235, 0.2)', fill: true, tension: 0.2, spanGaps: true }] }, options: { maintainAspectRatio: false } });
|
||||
} else {
|
||||
humidityWrapper.append('<p class="text-muted">Keine Feuchtigkeitsdaten vorhanden.</p>');
|
||||
}
|
||||
} else {
|
||||
chartsContainer.html('<div class="col-12"><p class="text-muted">Keine Sensordaten für diese Pflanze vorhanden.</p></div>');
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
chartsContainer.html('<div class="col-12"><p class="text-danger">Fehler beim Laden der Sensordaten.</p></div>');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 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($('<option>', { value: zone.id, text: zone.name }));
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
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);
|
||||
zoneFilterSelect.val('all').trigger('change');
|
||||
});
|
||||
}
|
||||
$('#zone-filter-select').on('change', function() {
|
||||
const zoneId = $(this).val();
|
||||
const selectionContainer = $('#plant-target-selection');
|
||||
selectionContainer.html('<div class="spinner-border spinner-border-sm" role="status"><span class="visually-hidden">Lade...</span></div>');
|
||||
$.ajax({
|
||||
type: 'GET', url: 'ajax_handler.php', data: { action: 'get_plants_by_zone', zone_id: zoneId }, dataType: 'json',
|
||||
success: function(response) {
|
||||
selectionContainer.empty();
|
||||
if (response.success && response.data.length > 0) {
|
||||
let plantCheckboxes = '<div class="form-check"><input class="form-check-input" type="checkbox" id="select-all-plants" checked><label class="form-check-label fw-bold" for="select-all-plants">Alle auswählen/abwählen</label></div><hr class="my-2">';
|
||||
plantCheckboxes += '<div class="row" style="max-height: 200px; overflow-y: auto;">';
|
||||
response.data.forEach(function(plant) {
|
||||
plantCheckboxes += `<div class="col-md-6"><div class="form-check"><input class="form-check-input plant-checkbox" type="checkbox" name="plant_ids[]" value="${plant.id}" id="plant_${plant.id}" checked><label class="form-check-label" for="plant_${plant.id}">${plant.strain_name} (${plant.container_name})</label></div></div>`;
|
||||
});
|
||||
plantCheckboxes += '</div>';
|
||||
selectionContainer.html(plantCheckboxes);
|
||||
} else {
|
||||
selectionContainer.html('<p class="text-muted">Keine aktiven Pflanzen in dieser Auswahl gefunden.</p>');
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
$(document).on('change', '#select-all-plants', function() {
|
||||
$('#global-activity-form .plant-checkbox').prop('checked', $(this).prop('checked'));
|
||||
});
|
||||
|
||||
// Universelle Logik
|
||||
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!')
|
||||
});
|
||||
});
|
||||
|
||||
function handleFormSubmit(form) {
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: 'ajax_handler.php',
|
||||
data: form.serialize(),
|
||||
dataType: 'json',
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Speichern fehlgeschlagen: ' + (response.message || 'Unbekannter Fehler'));
|
||||
}
|
||||
},
|
||||
error: () => alert('Serverfehler!')
|
||||
});
|
||||
}
|
||||
});
|
||||
58
js/main.js.1357Uhr
Normal file
58
js/main.js.1357Uhr
Normal file
@@ -0,0 +1,58 @@
|
||||
$(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));
|
||||
|
||||
// NEU: DataTables für die Samen-Tabelle initialisieren
|
||||
if ($('#seeds-table').length) {
|
||||
$('#seeds-table').DataTable({
|
||||
"dom": "t", // 't' bedeutet, dass NUR die Tabelle gerendert wird, ohne zusätzliche Controls
|
||||
"paging": false, // Keine Paginierung
|
||||
"info": false, // Keine "Zeige x von y Einträgen"-Info
|
||||
"language": {
|
||||
"zeroRecords": "Keine passenden Samen gefunden",
|
||||
},
|
||||
"columnDefs": [
|
||||
{ "orderable": false, "targets": 'no-sort' }
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
// Zonen Logik
|
||||
$('#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'); } });
|
||||
$('#zone-form').on('submit', function(e) { e.preventDefault(); handleFormSubmit($(this)); });
|
||||
|
||||
// Pflanzgefäß Logik
|
||||
$('#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'); } });
|
||||
$('#container-form').on('submit', function(e) { e.preventDefault(); handleFormSubmit($(this)); });
|
||||
|
||||
// Samen Logik
|
||||
$('#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'); } });
|
||||
$('#seed-form').on('submit', function(e) { e.preventDefault(); handleFormSubmit($(this)); });
|
||||
$('#ratio_sativa').on('input', function() { let sativa = $(this).val(); $('#sativa-value-label').text(sativa); $('#indica-value-label').text(100 - sativa); });
|
||||
|
||||
// Pflanzen Logik
|
||||
$('#plant-form, #edit-plant-form, #activity-form, #measurement-form, #global-activity-form').on('submit', function(e) { e.preventDefault(); handleFormSubmit($(this)); });
|
||||
$('#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.'); } }); });
|
||||
$('#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]); });
|
||||
$('#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('<option value="">Lade Gefäße...</option>'); if (!zoneId) { containerSelect.html('<option value="">Zuerst Zone wählen</option>'); 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('<option value="">Pflanzgefäß wählen</option>'); if (isEditMode && preselectContainerId) { const plantData = $('.edit-plant-btn').data('plant-info'); containerSelect.append($('<option>', { value: plantData.container_id, text: plantData.container_name })); } if (response.data.length > 0) { response.data.forEach(function(container) { if (!isEditMode || container.id != preselectContainerId) { containerSelect.append($('<option>', { value: container.id, text: container.name })); } }); } if(preselectContainerId) { containerSelect.val(preselectContainerId); } containerSelect.prop('disabled', false); } else { containerSelect.html('<option value="">Fehler</option>'); } }, error: function() { containerSelect.html('<option value="">Serverfehler!</option>'); } }); });
|
||||
$('#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!') }); } });
|
||||
$('#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); });
|
||||
$('#generate-api-key-form').on('submit', function(e) { e.preventDefault(); if (confirm('Bist du sicher? Ein neuer Key macht einen eventuell bestehenden ungültig.')) { handleFormSubmit($(this)); } });
|
||||
$('#change-username-form').on('submit', function(e) { e.preventDefault(); handleFormSubmit($(this)); });
|
||||
$('#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!') }); });
|
||||
const sensorTabBtn = document.querySelector('#sensor-tab-btn'); if (sensorTabBtn) { sensorTabBtn.addEventListener('shown.bs.tab', function () { if ($(this).data('loaded')) { return; } $(this).data('loaded', true); const plantId = new URLSearchParams(window.location.search).get('id'); const chartsContainer = $('#sensor-charts-container'); chartsContainer.html('<div class="col-12 text-center p-5"><div class="spinner-border text-secondary" role="status"><span class="visually-hidden">Lade...</span></div></div>'); $.ajax({ type: 'GET', url: 'ajax_handler.php', data: { action: 'get_sensor_data', plant_id: plantId }, dataType: 'json', success: function(response) { chartsContainer.empty(); if (response.success && response.data.labels.length > 0) { chartsContainer.html('<div class="col-lg-6" id="temp-chart-wrapper"></div><div class="col-lg-6" id="humidity-chart-wrapper"></div>'); const tempWrapper = $('#temp-chart-wrapper'); const humidityWrapper = $('#humidity-chart-wrapper'); if (response.data.temperature.some(val => val !== null)) { tempWrapper.append('<h5>Temperaturverlauf</h5>'); let tempCanvas = $('<canvas>').attr('height', '300'); tempWrapper.append($('<div>').addClass('mb-4').append(tempCanvas)); new Chart(tempCanvas, { type: 'line', data: { labels: response.data.labels, datasets: [{ label: 'Temperatur (°C)', data: response.data.temperature, borderColor: 'rgba(255, 99, 132, 1)', backgroundColor: 'rgba(255, 99, 132, 0.2)', fill: true, tension: 0.2, spanGaps: true }] }, options: { maintainAspectRatio: false } }); } else { tempWrapper.append('<p class="text-muted">Keine Temperaturdaten vorhanden.</p>'); } if (response.data.humidity.some(val => val !== null)) { humidityWrapper.append('<h5>Feuchtigkeitsverlauf</h5>'); let humidityCanvas = $('<canvas>').attr('height', '300'); humidityWrapper.append($('<div>').addClass('mb-4').append(humidityCanvas)); new Chart(humidityCanvas, { type: 'line', data: { labels: response.data.labels, datasets: [{ label: 'Feuchtigkeit (%)', data: response.data.humidity, borderColor: 'rgba(54, 162, 235, 1)', backgroundColor: 'rgba(54, 162, 235, 0.2)', fill: true, tension: 0.2, spanGaps: true }] }, options: { maintainAspectRatio: false } }); } else { humidityWrapper.append('<p class="text-muted">Keine Feuchtigkeitsdaten vorhanden.</p>'); } } else { chartsContainer.html('<div class="col-12"><p class="text-muted">Keine Sensordaten für diese Pflanze vorhanden.</p></div>'); } }, error: function() { chartsContainer.html('<div class="col-12"><p class="text-danger">Fehler beim Laden der Sensordaten.</p></div>'); } }); }); }
|
||||
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($('<option>', { value: zone.id, text: zone.name })); }); } } }); } 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); zoneFilterSelect.val('all').trigger('change'); }); }
|
||||
$('#zone-filter-select').on('change', function() { const zoneId = $(this).val(); const selectionContainer = $('#plant-target-selection'); selectionContainer.html('<div class="spinner-border spinner-border-sm" role="status"><span class="visually-hidden">Lade...</span></div>'); $.ajax({ type: 'GET', url: 'ajax_handler.php', data: { action: 'get_plants_by_zone', zone_id: zoneId }, dataType: 'json', success: function(response) { selectionContainer.empty(); if (response.success && response.data.length > 0) { let plantCheckboxes = '<div class="form-check"><input class="form-check-input" type="checkbox" id="select-all-plants" checked><label class="form-check-label fw-bold" for="select-all-plants">Alle auswählen/abwählen</label></div><hr class="my-2">'; plantCheckboxes += '<div class="row" style="max-height: 200px; overflow-y: auto;">'; response.data.forEach(function(plant) { plantCheckboxes += `<div class="col-md-6"><div class="form-check"><input class="form-check-input plant-checkbox" type="checkbox" name="plant_ids[]" value="${plant.id}" id="plant_${plant.id}" checked><label class="form-check-label" for="plant_${plant.id}">${plant.strain_name} (${plant.container_name})</label></div></div>`; }); plantCheckboxes += '</div>'; selectionContainer.html(plantCheckboxes); } else { selectionContainer.html('<p class="text-muted">Keine aktiven Pflanzen in dieser Auswahl gefunden.</p>'); } } }); });
|
||||
$(document).on('change', '#select-all-plants', function() { $('#global-activity-form .plant-checkbox').prop('checked', $(this).prop('checked')); });
|
||||
|
||||
// Universelle Logik
|
||||
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!') }); });
|
||||
function handleFormSubmit(form) { $.ajax({ type: 'POST', url: 'ajax_handler.php', data: form.serialize(), dataType: 'json', success: function(response) { if (response.success) { location.reload(); } else { alert('Speichern fehlgeschlagen: ' + (response.message || 'Unbekannter Fehler')); } }, error: () => alert('Serverfehler!') }); }
|
||||
});
|
||||
60
login.php
Normal file
60
login.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
session_start();
|
||||
if(isset($_SESSION["loggedin"]) && $_SESSION["loggedin"] === true){ header("location: index.php"); exit; }
|
||||
require_once "includes/db_connect.php";
|
||||
$username = $password = ""; $login_err = "";
|
||||
if($_SERVER["REQUEST_METHOD"] == "POST"){
|
||||
if(empty(trim($_POST["username"]))){ $login_err = "Bitte geben Sie einen Benutzernamen ein."; } else{ $username = trim($_POST["username"]); }
|
||||
if(empty(trim($_POST["password"]))){ $login_err = "Bitte geben Sie ein Passwort ein."; } else{ $password = trim($_POST["password"]); }
|
||||
if(empty($login_err)){
|
||||
$sql = "SELECT id, username, password_hash FROM users WHERE username = ?";
|
||||
if($stmt = $mysqli->prepare($sql)){
|
||||
$stmt->bind_param("s", $param_username);
|
||||
$param_username = $username;
|
||||
if($stmt->execute()){
|
||||
$stmt->store_result();
|
||||
if($stmt->num_rows == 1){
|
||||
$stmt->bind_result($id, $username, $hashed_password);
|
||||
if($stmt->fetch()){
|
||||
if(password_verify($password, $hashed_password)){
|
||||
session_start();
|
||||
$_SESSION["loggedin"] = true;
|
||||
$_SESSION["user_id"] = $id;
|
||||
$_SESSION["username"] = $username;
|
||||
header("location: index.php");
|
||||
} else{ $login_err = "Ungültiger Benutzername oder Passwort."; }
|
||||
}
|
||||
} else{ $login_err = "Ungültiger Benutzername oder Passwort."; }
|
||||
} else{ $login_err = "Oops! Etwas ist schief gelaufen."; }
|
||||
$stmt->close();
|
||||
}
|
||||
}
|
||||
$mysqli->close();
|
||||
}
|
||||
define('APP_VERSION', '8.3.0');
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Login - Cazubu</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="css/style.css?v=<?php echo APP_VERSION; ?>">
|
||||
</head>
|
||||
<body>
|
||||
<div class="auth-container">
|
||||
<div class="auth-form-wrapper">
|
||||
<div class="text-center mb-4"><img src="logo.png" alt="Cazubu Logo" style="max-width: 120px;"><h2 class="mt-2">Cazubu Login</h2></div>
|
||||
<?php if(!empty($login_err)){ echo '<div class="alert alert-danger">' . htmlspecialchars($login_err) . '</div>'; } ?>
|
||||
<form action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]); ?>" method="post">
|
||||
<div class="mb-3"><label for="username" class="form-label">Benutzername</label><input type="text" name="username" id="username" class="form-control" value="<?php echo htmlspecialchars($username); ?>" required></div>
|
||||
<div class="mb-3"><label for="password" class="form-label">Passwort</label><input type="password" name="password" id="password" class="form-control" required></div>
|
||||
<div class="d-grid"><button type="submit" class="btn btn-primary">Anmelden</button></div>
|
||||
<p class="text-center mt-3">Noch kein Konto? <a href="register.php">Jetzt registrieren</a></p>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
7
logout.php
Normal file
7
logout.php
Normal file
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
session_start();
|
||||
$_SESSION = array();
|
||||
session_destroy();
|
||||
header("location: login.php");
|
||||
exit;
|
||||
?>
|
||||
79
plant_detail.php
Normal file
79
plant_detail.php
Normal file
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
require_once 'includes/auth_check.php';
|
||||
require_once 'includes/db_connect.php';
|
||||
|
||||
$plant_id = $_GET['id'] ?? null;
|
||||
if (!$plant_id || !is_numeric($plant_id)) { header("location: plants.php"); exit; }
|
||||
$plant_id = (int)$plant_id;
|
||||
$user_id = $_SESSION['user_id'];
|
||||
|
||||
// Lade Pflanzendaten
|
||||
$plant = null;
|
||||
$sql = "SELECT p.*, s.strain_name, s.internal_name, z.name AS zone_name, c.name AS container_name, (SELECT file_path FROM plant_images WHERE plant_id = p.id ORDER BY uploaded_at DESC LIMIT 1) as latest_image, (SELECT MAX(hm.height_cm) FROM plant_height_measurements hm WHERE hm.plant_id = p.id) AS current_height, (SELECT MAX(pa.activity_date) FROM plant_activities pa WHERE pa.plant_id = p.id AND pa.activity_type = 'Wässern') AS last_watering, (SELECT MAX(pa.activity_date) FROM plant_activities pa WHERE pa.plant_id = p.id AND pa.activity_type = 'Düngen') AS last_fertilizing FROM plants p JOIN seeds s ON p.seed_id = s.id JOIN zones z ON p.zone_id = z.id JOIN containers c ON p.container_id = c.id WHERE p.id = ? AND p.user_id = ?";
|
||||
if ($stmt = $mysqli->prepare($sql)) { $stmt->bind_param("ii", $plant_id, $user_id); $stmt->execute(); $result = $stmt->get_result(); if ($result->num_rows === 1) { $plant = $result->fetch_assoc(); } $stmt->close(); }
|
||||
if ($plant === null) { header("location: plants.php"); exit; }
|
||||
|
||||
// KORREKTUR: Fehlende Abfrage für Benutzerdaten (API-Key) hinzugefügt
|
||||
$user_data = null;
|
||||
$sql_user = "SELECT api_key FROM users WHERE id = ?";
|
||||
if ($stmt_user = $mysqli->prepare($sql_user)) {
|
||||
$stmt_user->bind_param("i", $user_id);
|
||||
$stmt_user->execute();
|
||||
$user_data = $stmt_user->get_result()->fetch_assoc();
|
||||
$stmt_user->close();
|
||||
}
|
||||
|
||||
// Lade alle weiteren Daten für die Tabs
|
||||
$gallery_images = []; $sql_gallery = "SELECT id, file_path, uploaded_at FROM plant_images WHERE plant_id = ? ORDER BY uploaded_at DESC";
|
||||
if($stmt_gallery = $mysqli->prepare($sql_gallery)){ $stmt_gallery->bind_param("i", $plant_id); $stmt_gallery->execute(); $result_gallery = $stmt_gallery->get_result(); while($row = $result_gallery->fetch_assoc()){ $gallery_images[] = $row; } $stmt_gallery->close(); }
|
||||
$plant_activities = []; $sql_activities = "SELECT activity_type, note, activity_date FROM plant_activities WHERE plant_id = ? ORDER BY activity_date DESC";
|
||||
if($stmt_activities = $mysqli->prepare($sql_activities)){ $stmt_activities->bind_param("i", $plant_id); $stmt_activities->execute(); $result_activities = $stmt_activities->get_result(); while($row = $result_activities->fetch_assoc()){ $plant_activities[] = $row; } $stmt_activities->close(); }
|
||||
$plant_measurements = []; $sql_measurements = "SELECT height_cm, measurement_date FROM plant_height_measurements WHERE plant_id = ? ORDER BY measurement_date DESC";
|
||||
if($stmt_measurements = $mysqli->prepare($sql_measurements)){ $stmt_measurements->bind_param("i", $plant_id); $stmt_measurements->execute(); $result_measurements = $stmt_measurements->get_result(); while($row = $result_measurements->fetch_assoc()){ $plant_measurements[] = $row; } $stmt_measurements->close(); }
|
||||
$zones = []; $sql_zones = "SELECT id, name FROM zones WHERE user_id = ? ORDER BY name ASC";
|
||||
if ($stmt_zones = $mysqli->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(); }
|
||||
|
||||
require_once 'includes/header.php';
|
||||
?>
|
||||
|
||||
<div class="card header-card mb-4"><div class="card-body"><h1 class="mb-0"><?php echo htmlspecialchars($plant['strain_name']); ?></h1><p class="card-text text-white-50 mt-2">Detailansicht deiner Pflanze</p></div></div>
|
||||
<div class="row">
|
||||
<div class="col-lg-4 mb-4"><div class="plant-image-frame"><img src="<?php echo !empty($plant['latest_image']) ? htmlspecialchars($plant['latest_image']) : 'assets/dummy_plant.png'; ?>" class="img-fluid w-100" style="object-fit: contain;"></div></div>
|
||||
<div class="col-lg-8 mb-4">
|
||||
<table class="table table-hover cazubu-table-frameless">
|
||||
<thead class="table-dark"><tr><th colspan="2">Stammdaten & Zustand</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td class="fw-bold" style="width: 40%;">Status</td><td><span class='badge <?php if ($plant['status'] == 'Eingepflanzt') echo 'text-bg-success'; elseif ($plant['status'] == 'Trocknend') echo 'text-bg-info'; else echo 'text-bg-dark'; ?>'><?php echo htmlspecialchars($plant['status']); ?></span></td></tr>
|
||||
<tr><td class="fw-bold">Phase</td><td><?php $phase = $plant['phase']; $phase_badge_class = 'text-bg-secondary'; if ($phase == 'Keimend') $phase_badge_class = 'text-bg-light'; if ($phase == 'Setzling') $phase_badge_class = 'text-bg-success'; if ($phase == 'Wachstum') $phase_badge_class = 'text-bg-primary'; if ($phase == 'Blütenphase') $phase_badge_class = 'text-bg-warning'; if ($phase == 'Ernte' || $phase == 'Getrocknet') $phase_badge_class = 'text-bg-dark'; echo "<span class='badge {$phase_badge_class}'>".htmlspecialchars($phase)."</span>"; ?></td></tr>
|
||||
<tr><td class="fw-bold">Sorte</td><td><?php echo htmlspecialchars($plant['strain_name']); ?></td></tr>
|
||||
<tr><td class="fw-bold">Zone / Gefäß</td><td><?php echo htmlspecialchars($plant['zone_name']); ?> / <?php echo htmlspecialchars($plant['container_name']); ?></td></tr>
|
||||
<tr><td class="fw-bold">Gepflanzt am</td><td><?php echo date('d.m.Y', strtotime($plant['plant_date'])); ?> (vor <?php echo (new DateTime())->diff(new DateTime($plant['plant_date']))->days; ?> Tagen)</td></tr>
|
||||
<tr><td class="fw-bold">Aktuelle Höhe</td><td><?php echo $plant['current_height'] ? htmlspecialchars($plant['current_height']) . ' cm' : '<i>Keine Messung</i>'; ?></td></tr>
|
||||
<tr><td class="fw-bold">Letzte Bewässerung</td><td><?php echo $plant['last_watering'] ? date('d.m.Y H:i', strtotime($plant['last_watering'])) . ' Uhr' : '<i>Nie</i>'; ?></td></tr>
|
||||
<tr><td class="fw-bold">Letzte Düngung</td><td><?php echo $plant['last_fertilizing'] ? date('d.m.Y H:i', strtotime($plant['last_fertilizing'])) . ' Uhr' : '<i>Nie</i>'; ?></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<table class="table cazubu-table-frameless mt-3">
|
||||
<thead class="table-dark"><tr><th>Aktionen</th></tr></thead>
|
||||
<tbody><tr><td class="p-3"><div class="d-flex flex-wrap gap-2"><button class="btn btn-sm btn-danger delete-btn" data-bs-toggle="modal" data-bs-target="#deleteConfirmModal" data-id="<?php echo $plant['id']; ?>" data-name="<?php echo htmlspecialchars($plant['strain_name']); ?>" data-type="plant">Pflanze löschen</button><button class="btn btn-sm btn-secondary edit-plant-btn" data-bs-toggle="modal" data-bs-target="#editPlantModal" data-plant-info='<?php echo json_encode($plant, JSON_HEX_APOS | JSON_HEX_QUOT); ?>'>Infos bearbeiten</button><?php if ($plant['status'] == 'Eingepflanzt' && $plant['phase'] == 'Blütenphase'): ?><button class="btn btn-sm btn-cazubu" data-bs-toggle="modal" data-bs-target="#harvestConfirmModal">Pflanze ernten</button><?php elseif ($plant['status'] == 'Eingepflanzt'): ?><span class="d-inline-block" tabindex="0" data-bs-toggle="tooltip" title="Ernten ist nur in der Blütenphase möglich."><button class="btn btn-sm btn-cazubu" type="button" disabled style="pointer-events: none;">Pflanze ernten</button></span><?php endif; ?><?php if ($plant['status'] == 'Trocknend'): ?><button class="btn btn-sm btn-success" data-bs-toggle="modal" data-bs-target="#finishDryingModal">Trocknung abschließen</button><?php endif; ?><button class="btn btn-sm btn-info text-white" data-bs-toggle="modal" data-bs-target="#apiUrlModal">API-URL</button></div></td></tr></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<table class="table cazubu-table-frameless">
|
||||
<thead class="table-dark"><tr><th><ul class="nav nav-tabs card-header-tabs" id="detailTabs"><li class="nav-item"><button class="nav-link active" data-bs-toggle="tab" data-bs-target="#tab-activities">Aktivitäten (<?php echo count($plant_activities); ?>)</button></li><li class="nav-item"><button class="nav-link" id="sensor-tab-btn" data-bs-toggle="tab" data-bs-target="#tab-sensors">Sensoren</button></li><li class="nav-item"><button class="nav-link" data-bs-toggle="tab" data-bs-target="#tab-measurements">Messungen (<?php echo count($plant_measurements); ?>)</button></li><li class="nav-item"><button class="nav-link" data-bs-toggle="tab" data-bs-target="#tab-gallery">Bildergalerie (<?php echo count($gallery_images); ?>)</button></li></ul></th></tr></thead>
|
||||
<tbody><tr><td class="p-3"><div class="tab-content">
|
||||
<div class="tab-pane fade" id="tab-gallery"><div class="d-flex justify-content-between align-items-center mb-3"><h6 class="mb-0">Alle Bilder</h6><button class="btn btn-sm btn-cazubu" data-bs-toggle="modal" data-bs-target="#uploadImageModal">+ Neues Bild hochladen</button></div><div class="image-gallery"><?php if(empty($gallery_images)): ?><p class="text-muted">Noch keine Bilder für diese Pflanze hochgeladen.</p><?php endif; ?><?php foreach($gallery_images as $image): ?><div class="gallery-item text-center"><a href="<?php echo htmlspecialchars($image['file_path']); ?>" target="_blank"><img src="<?php echo htmlspecialchars($image['file_path']); ?>" class="gallery-image shadow-sm"></a><button class="btn btn-danger btn-sm delete-image-btn delete-btn" data-bs-toggle="modal" data-bs-target="#deleteConfirmModal" data-id="<?php echo $image['id']; ?>" data-name="Bild vom <?php echo date('d.m.Y', strtotime($image['uploaded_at'])); ?>" data-type="plant_image"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-x-circle-fill" viewBox="0 0 16 16"><path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM5.354 4.646a.5.5 0 1 0-.708.708L7.293 8l-2.647 2.646a.5.5 0 0 0 .708.708L8 8.707l2.646 2.647a.5.5 0 0 0 .708-.708L8.707 8l2.647-2.646a.5.5 0 0 0-.708-.708L8 7.293 5.354 4.646z"/></svg></button><div class="gallery-date"><?php echo date('d.m.Y', strtotime($image['uploaded_at'])); ?></div></div><?php endforeach; ?></div></div>
|
||||
<div class="tab-pane fade show active" id="tab-activities"><div class="d-flex justify-content-between align-items-center mb-3"><h6 class="mb-0">Protokollierte Aktivitäten</h6><button class="btn btn-sm btn-cazubu" data-bs-toggle="modal" data-bs-target="#addActivityModal">+ Aktivität hinzufügen</button></div><table class="table table-sm table-striped border"><thead><tr><th style="width:160px;">Datum</th><th style="width:120px;">Aktion</th><th>Notiz</th></tr></thead><tbody><?php if(empty($plant_activities)): ?><tr><td colspan="3" class="text-center p-3"><i>Keine Aktivitäten protokolliert.</i></td></tr><?php endif; ?><?php foreach($plant_activities as $activity): ?><tr><td><?= date('d.m.Y H:i', strtotime($activity['activity_date'])) ?></td><td class="fw-bold"><?= htmlspecialchars($activity['activity_type']) ?></td><td><?= nl2br(htmlspecialchars($activity['note'])) ?></td></tr><?php endforeach; ?></tbody></table></div>
|
||||
<div class="tab-pane fade" id="tab-measurements"><div class="d-flex justify-content-between align-items-center mb-3"><h6 class="mb-0">Größen-Messungen</h6><button class="btn btn-sm btn-cazubu" data-bs-toggle="modal" data-bs-target="#addMeasurementModal">+ Höhe messen</button></div><table class="table table-sm table-striped border"><thead><tr><th style="width:160px;">Datum</th><th>Höhe</th></tr></thead><tbody><?php if(empty($plant_measurements)): ?><tr><td colspan="2" class="text-center p-3"><i>Keine Messungen protokolliert.</i></td></tr><?php endif; ?><?php foreach($plant_measurements as $measurement): ?><tr><td><?= date('d.m.Y', strtotime($measurement['measurement_date'])) ?></td><td><?= htmlspecialchars($measurement['height_cm']) ?> cm</td></tr><?php endforeach; ?></tbody></table></div>
|
||||
<div class="tab-pane fade" id="tab-sensors"><div id="sensor-charts-container" class="row"><div class="col-12 text-center p-5"><div class="spinner-border text-secondary" role="status"><span class="visually-hidden">Lade Graphen...</span></div><p class="text-muted mt-2">Lade Sensordaten...</p></div></div></div>
|
||||
</div></td></tr></tbody>
|
||||
</table>
|
||||
<div class="modal fade" id="deleteConfirmModal" tabindex="-1"><div class="modal-dialog"><div class="modal-content"><div class="modal-header"><h5 class="modal-title">Löschen bestätigen</h5><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body"><p>Sind Sie sicher, dass Sie <strong id="item-type-to-delete"></strong> "<strong id="item-name-to-delete"></strong>" endgültig löschen möchten?</p><p class="text-danger" id="delete-warning"></p></div><div class="modal-footer"><button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button><button type="button" class="btn btn-danger" id="confirmDeleteBtn">Ja, endgültig löschen</button></div></div></div></div>
|
||||
<div class="modal fade" id="uploadImageModal" tabindex="-1"><div class="modal-dialog"><div class="modal-content"><div class="modal-header"><h5 class="modal-title">Neues Bild hochladen</h5><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body"><form id="image-upload-form" enctype="multipart/form-data"><input type="hidden" name="action" value="upload_plant_image"><input type="hidden" name="plant_id" value="<?php echo $plant_id; ?>"><div class="mb-3"><label for="plant_image_file" class="form-label">Bilddatei auswählen (JPG, PNG, GIF, WebP)</label><input class="form-control" type="file" id="plant_image_file" name="plant_image" required></div></form></div><div class="modal-footer"><button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button><button type="submit" class="btn btn-primary" form="image-upload-form">Hochladen & Speichern</button></div></div></div></div>
|
||||
<div class="modal fade" id="editPlantModal" tabindex="-1"><div class="modal-dialog modal-lg"><div class="modal-content"><div class="modal-header"><h5 class="modal-title">Pflanzen-Infos bearbeiten</h5><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body"><form id="edit-plant-form"><input type="hidden" name="action" value="edit_plant"><input type="hidden" name="plant_id" value="<?php echo $plant_id; ?>"><div class="row"><div class="col-md-6 mb-3"><label class="form-label">Phase</label><select class="form-select" name="phase" required><option value="Keimend">Keimend</option><option value="Setzling">Setzling</option><option value="Wachstum">Wachstum</option><option value="Blütenphase">Blütenphase</option><option value="Ernte">Ernte</option><option value="Getrocknet">Getrocknet</option></select></div><div class="col-md-6 mb-3"><label class="form-label">Datum der Pflanzung</label><input type="date" class="form-control" name="plant_date" required></div></div><div class="row"><div class="col-md-6 mb-3"><label class="form-label">Zone</label><select class="form-select" name="zone_id" id="edit-zone-select" required><option value="">Bitte wählen...</option><?php foreach($zones as $zone) { echo "<option value='{$zone['id']}'>".htmlspecialchars($zone['name'])."</option>"; } ?></select></div><div class="col-md-6 mb-3"><label class="form-label">Pflanzgefäß</label><select class="form-select" name="container_id" id="edit-container-select" required disabled><option value="">Zuerst Zone wählen</option></select></div></div></form></div><div class="modal-footer"><button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button><button type="submit" class="btn btn-primary" form="edit-plant-form">Änderungen speichern</button></div></div></div></div>
|
||||
<div class="modal fade" id="harvestConfirmModal" tabindex="-1"><div class="modal-dialog"><div class="modal-content"><div class="modal-header"><h5 class="modal-title">Ernte bestätigen</h5><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body"><p>Möchtest du diese Pflanze wirklich ernten?</p><p class="text-muted">Der Status wird auf "Trocknend" gesetzt.</p></div><div class="modal-footer"><button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button><button type="button" class="btn btn-success" id="confirmHarvestBtn">Ja, ernten</button></div></div></div></div>
|
||||
<div class="modal fade" id="finishDryingModal" tabindex="-1"><div class="modal-dialog"><div class="modal-content"><div class="modal-header"><h5 class="modal-title">Trocknung abschließen</h5><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body"><p>Soll der Status dieser Pflanze auf "Geerntet" gesetzt werden?</p><p class="text-muted">Die Pflanze gilt damit als vollständig verarbeitet.</p></div><div class="modal-footer"><button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button><button type="button" class="btn btn-dark" id="confirmFinishDryingBtn">Ja, Prozess abschließen</button></div></div></div></div>
|
||||
<div class="modal fade" id="addActivityModal" tabindex="-1"><div class="modal-dialog"><div class="modal-content"><div class="modal-header"><h5 class="modal-title">Aktivität hinzufügen</h5><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body"><form id="activity-form"><input type="hidden" name="action" value="add_activity"><input type="hidden" name="plant_id" value="<?php echo $plant_id; ?>"><div class="mb-3"><label class="form-label">Aktivität</label><select class="form-select" name="activity_type" required><option value="Wässern">Wässern</option><option value="Düngen">Düngen</option><option value="Umtopfen">Umtopfen</option><option value="Schnitt">Schnitt</option><option value="Sonstiges">Sonstiges</option></select></div><div class="mb-3"><label class="form-label">Datum & Uhrzeit</label><input class="form-control" type="datetime-local" name="activity_date" required></div><div class="mb-3"><label class="form-label">Notiz (optional)</label><textarea class="form-control" name="note" rows="3"></textarea></div></form></div><div class="modal-footer"><button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button><button type="submit" class="btn btn-primary" form="activity-form">Speichern</button></div></div></div></div>
|
||||
<div class="modal fade" id="addMeasurementModal" tabindex="-1"><div class="modal-dialog"><div class="modal-content"><div class="modal-header"><h5 class="modal-title">Höhe messen</h5><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body"><form id="measurement-form"><input type="hidden" name="action" value="add_measurement"><input type="hidden" name="plant_id" value="<?php echo $plant_id; ?>"><div class="mb-3"><label class="form-label">Höhe in cm</label><input class="form-control" type="number" step="0.1" name="height_cm" required></div><div class="mb-3"><label class="form-label">Datum der Messung</label><input class="form-control" type="date" name="measurement_date" required></div></form></div><div class="modal-footer"><button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button><button type="submit" class="btn btn-primary" form="measurement-form">Speichern</button></div></div></div></div>
|
||||
<div class="modal fade" id="apiUrlModal" tabindex="-1"><div class="modal-dialog modal-lg"><div class="modal-content"><div class="modal-header"><h5 class="modal-title">Sensor API-URL</h5><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body"><p>Benutze die folgenden URLs, um Sensor-Daten für diese Pflanze (ID: <?php echo $plant_id; ?>) zu senden. Ersetze `DEIN_WERT` durch den gemessenen Wert.</p><?php if (empty($user_data['api_key'])): ?><div class="alert alert-warning">Du musst zuerst auf deiner Profil-Seite einen API-Key generieren.</div><?php else: $base_url = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? "https" : "http") . "://$_SERVER[HTTP_HOST]/cazubu/api.php"; $api_key_part = "apikey=" . urlencode($user_data['api_key']); $plant_part = "pflanze=" . $plant_id; ?><div class="mb-3"><label class="form-label fw-bold">URL für Temperatur:</label><input type="text" class="form-control" value="<?php echo "{$base_url}?{$api_key_part}&{$plant_part}&sensor=temp&wert=DEIN_WERT"; ?>" readonly></div><div class="mb-3"><label class="form-label fw-bold">URL für Feuchtigkeit:</label><input type="text" class="form-control" value="<?php echo "{$base_url}?{$api_key_part}&{$plant_part}&sensor=humidity&wert=DEIN_WERT"; ?>" readonly></div><?php endif; ?></div><div class="modal-footer"><button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Schließen</button></div></div></div></div>
|
||||
<?php require_once 'includes/footer.php'; ?>
|
||||
77
plants.php
Normal file
77
plants.php
Normal file
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
require_once 'includes/auth_check.php';
|
||||
require_once 'includes/db_connect.php';
|
||||
$user_id = $_SESSION['user_id'];
|
||||
$status_filter = $_GET['status'] ?? 'Eingepflanzt';
|
||||
$allowed_stati = ['Eingepflanzt', 'Trocknend', 'Geerntet'];
|
||||
if (!in_array($status_filter, $allowed_stati)) { $status_filter = 'Eingepflanzt'; }
|
||||
$counts = [];
|
||||
$count_sql = "SELECT status, COUNT(*) as count FROM plants WHERE user_id = ? GROUP BY status";
|
||||
if ($stmt_count = $mysqli->prepare($count_sql)) { $stmt_count->bind_param("i", $user_id); $stmt_count->execute(); $result_count = $stmt_count->get_result(); while($row = $result_count->fetch_assoc()) { $counts[$row['status']] = $row['count']; } $stmt_count->close(); }
|
||||
$plants = [];
|
||||
$sql = "SELECT
|
||||
p.id, p.plant_date, p.phase,
|
||||
(SELECT file_path FROM plant_images WHERE plant_id = p.id ORDER BY uploaded_at DESC LIMIT 1) AS latest_image_path,
|
||||
s.strain_name, s.internal_name,
|
||||
z.name AS zone_name,
|
||||
c.name AS container_name,
|
||||
(SELECT value FROM sensor_data WHERE plant_id = p.id AND sensor_type = 'Temperatur' ORDER BY timestamp DESC LIMIT 1) AS current_temp,
|
||||
(SELECT value FROM sensor_data WHERE plant_id = p.id AND sensor_type = 'Feuchtigkeit' ORDER BY timestamp DESC LIMIT 1) AS current_humidity
|
||||
FROM plants p
|
||||
LEFT JOIN seeds s ON p.seed_id = s.id
|
||||
LEFT JOIN zones z ON p.zone_id = z.id
|
||||
LEFT JOIN containers c ON p.container_id = c.id
|
||||
WHERE p.user_id = ? AND p.status = ?
|
||||
ORDER BY p.plant_date DESC";
|
||||
if ($stmt = $mysqli->prepare($sql)) { $stmt->bind_param("is", $user_id, $status_filter); $stmt->execute(); $result = $stmt->get_result(); while ($row = $result->fetch_assoc()) { $plants[] = $row; } $stmt->close(); }
|
||||
$zones = []; $seeds = [];
|
||||
$sql_zones = "SELECT id, name FROM zones WHERE user_id = ? ORDER BY name ASC";
|
||||
if ($stmt_zones = $mysqli->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_seeds = "SELECT id, strain_name, internal_name, stock_count FROM seeds WHERE user_id = ? AND stock_count > 0 ORDER BY strain_name ASC";
|
||||
if ($stmt_seeds = $mysqli->prepare($sql_seeds)) { $stmt_seeds->bind_param("i", $user_id); $stmt_seeds->execute(); $result_seeds = $stmt_seeds->get_result(); while ($row = $result_seeds->fetch_assoc()) { $seeds[] = $row; } $stmt_seeds->close(); }
|
||||
require_once 'includes/header.php';
|
||||
?>
|
||||
<div class="card header-card glass-effect mb-4">
|
||||
<div class="card-body d-flex justify-content-between align-items-center">
|
||||
<div><h1 class="mb-0">Pflanzen-Übersicht</h1><p class="card-text text-white-50 mt-2">Übersicht über alle deine Pflanzen.</p></div>
|
||||
<button class="btn btn-cazubu" data-bs-toggle="modal" data-bs-target="#plantModal">+ Neue Pflanze anlegen</button>
|
||||
</div>
|
||||
</div>
|
||||
<ul class="nav nav-pills plant-tabs">
|
||||
<li class="nav-item"><a class="nav-link <?php if($status_filter == 'Eingepflanzt') echo 'active'; ?>" href="?status=Eingepflanzt">Wachsend (<?php echo $counts['Eingepflanzt'] ?? 0; ?>)</a></li>
|
||||
<li class="nav-item"><a class="nav-link <?php if($status_filter == 'Trocknend') echo 'active'; ?>" href="?status=Trocknend">Trocknend (<?php echo $counts['Trocknend'] ?? 0; ?>)</a></li>
|
||||
<li class="nav-item"><a class="nav-link <?php if($status_filter == 'Geerntet') echo 'active'; ?>" href="?status=Geerntet">Geerntet (<?php echo $counts['Geerntet'] ?? 0; ?>)</a></li>
|
||||
</ul>
|
||||
<table id="plants-table" class="table table-hover cazubu-table-frameless">
|
||||
<thead class="table-dark">
|
||||
<tr>
|
||||
<th style="width: 8%;" class="no-sort">Foto</th>
|
||||
<th style="width: 25%;">Sorte / Interne Bez.</th>
|
||||
<th style="width: 20%;">Zone / Gefäß</th>
|
||||
<th style="width: 12%;">Phase</th>
|
||||
<th style="width: 8%;">Alter</th>
|
||||
<th style="width: 8%;">Temp.</th>
|
||||
<th style="width: 9%;">Feucht.</th>
|
||||
<th style="width: 10%;" class="no-sort">Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($plants)): ?>
|
||||
<tr><td colspan="8" class="text-center p-4"><i>Keine Pflanzen mit Status "<?php echo htmlspecialchars($status_filter); ?>" gefunden.</i></td></tr>
|
||||
<?php endif; ?>
|
||||
<?php foreach ($plants as $plant): ?>
|
||||
<tr>
|
||||
<td><img src="<?php echo !empty($plant['latest_image_path']) ? htmlspecialchars($plant['latest_image_path']) : 'assets/dummy_plant.png'; ?>" alt="Pflanzenfoto" class="img-fluid rounded" style="width: 60px; height: 60px; object-fit: cover;" data-bs-toggle="popover" data-bs-trigger="hover" data-bs-html="true" data-bs-placement="right" data-bs-content="<img src='<?php echo !empty($plant['latest_image_path']) ? htmlspecialchars($plant['latest_image_path']) : 'assets/dummy_plant.png'; ?>' class='img-fluid' style='max-width: 200px;'>"></td>
|
||||
<td class="align-middle"><strong><?php echo htmlspecialchars($plant['strain_name']); ?></strong><small class="d-block text-muted"><?php if (!empty($plant['internal_name'])) echo htmlspecialchars($plant['internal_name']); ?></small></td>
|
||||
<td class="align-middle"><strong class="d-block"><?php echo htmlspecialchars($plant['container_name'] ?? 'N/A'); ?></strong><small class="text-muted"><?php echo htmlspecialchars($plant['zone_name'] ?? 'N/A'); ?></small></td>
|
||||
<td class="align-middle"><?php $phase = $plant['phase']; $badge_class = 'text-bg-secondary'; if ($phase == 'Keimend') $badge_class = 'text-bg-light'; elseif ($phase == 'Setzling') $badge_class = 'text-bg-success'; elseif ($phase == 'Wachstum') $phase_badge_class = 'text-bg-primary'; elseif ($phase == 'Blütenphase') $badge_class = 'text-bg-warning'; elseif ($phase == 'Ernte' || $phase == 'Getrocknet') $badge_class = 'text-bg-dark'; echo "<span class='badge {$badge_class}'>".htmlspecialchars($phase)."</span>"; ?></td>
|
||||
<td class="align-middle"><?php echo (new DateTime())->diff(new DateTime($plant['plant_date']))->days; ?> Tage</td>
|
||||
<td class="align-middle"><?php echo $plant['current_temp'] ? number_format($plant['current_temp'], 1) . '°C' : '-'; ?></td>
|
||||
<td class="align-middle"><?php echo $plant['current_humidity'] ? number_format($plant['current_humidity'], 1) . '%' : '-'; ?></td>
|
||||
<td class="align-middle text-center"><a href="plant_detail.php?id=<?php echo $plant['id']; ?>" class="btn btn-sm btn-outline-dark">🔍 Details</a></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="modal fade" id="plantModal" tabindex="-1"><div class="modal-dialog modal-lg"><div class="modal-content"><div class="modal-header"><h5 class="modal-title">Neue Pflanze anlegen</h5><button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button></div><div class="modal-body"><form id="plant-form"><input type="hidden" name="action" value="add_plant"><div class="row"><div class="col-md-6 mb-3"><label class="form-label">Zone</label><select class="form-select" name="zone_id" id="zone-select" required><option value="">1. Zone auswählen</option><?php foreach ($zones as $zone) { echo "<option value='{$zone['id']}'>" . htmlspecialchars($zone['name']) . "</option>"; } ?></select></div><div class="col-md-6 mb-3"><label class="form-label">Pflanzgefäß</label><select class="form-select" name="container_id" id="container-select" required disabled><option value="">2. Zuerst Zone wählen</option></select></div></div><div class="mb-3"><label class="form-label">Samen</label><select class="form-select" name="seed_id" required><option value="">3. Samen auswählen</option><?php foreach ($seeds as $seed) { echo "<option value='{$seed['id']}'>" . htmlspecialchars($seed['strain_name']) . " (" . htmlspecialchars($seed['internal_name']) . ") [" . $seed['stock_count'] . " Stk.]</option>"; } ?></select></div><div class="form-check mb-3"><input class="form-check-input" type="checkbox" name="reduce_seed_stock" value="1" id="reduce_seed_stock" checked><label class="form-check-label" for="reduce_seed_stock">Samenanzahl im Inventar automatisch um 1 reduzieren</label></div><div class="mb-3"><label class="form-label">Datum der Pflanzung</label><input type="date" class="form-control" name="plant_date" value="<?php echo date('Y-m-d'); ?>" required></div></form></div><div class="modal-footer"><button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Schließen</button><button type="submit" class="btn btn-cazubu" form="plant-form">Speichern</button></div></div></div></div>
|
||||
<?php require_once 'includes/footer.php'; ?>
|
||||
98
profile.php
Normal file
98
profile.php
Normal file
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
require_once 'includes/auth_check.php';
|
||||
require_once 'includes/db_connect.php';
|
||||
|
||||
$user_id = $_SESSION['user_id'];
|
||||
$user_data = null;
|
||||
|
||||
$sql = "SELECT username, api_key FROM users WHERE id = ?";
|
||||
if ($stmt = $mysqli->prepare($sql)) {
|
||||
$stmt->bind_param("i", $user_id);
|
||||
$stmt->execute();
|
||||
$result = $stmt->get_result();
|
||||
$user_data = $result->fetch_assoc();
|
||||
$stmt->close();
|
||||
}
|
||||
|
||||
require_once 'includes/header.php';
|
||||
?>
|
||||
|
||||
<div class="card header-card mb-4">
|
||||
<div class="card-body">
|
||||
<h1 class="mb-0">Dein Profil</h1>
|
||||
<p class="card-text text-white-50 mt-2">Verwalte hier deine Kontoeinstellungen.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-6 mb-4">
|
||||
<table class="table cazubu-table-frameless">
|
||||
<thead class="table-dark"><tr><th>Benutzernamen ändern</th></tr></thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="p-3">
|
||||
<form id="change-username-form">
|
||||
<input type="hidden" name="action" value="change_username">
|
||||
<div class="mb-3">
|
||||
<label for="username" class="form-label">Neuer Benutzername</label>
|
||||
<input type="text" class="form-control" name="username" id="username" value="<?php echo htmlspecialchars($user_data['username']); ?>" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-cazubu">Namen speichern</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-lg-6 mb-4">
|
||||
<table class="table cazubu-table-frameless">
|
||||
<thead class="table-dark"><tr><th>Passwort ändern</th></tr></thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="p-3">
|
||||
<p>Ändere hier dein Passwort für den Login.</p>
|
||||
<button class="btn btn-cazubu" data-bs-toggle="modal" data-bs-target="#changePasswordModal">Passwort ändern</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<table class="table cazubu-table-frameless">
|
||||
<thead class="table-dark"><tr><th>API-Key Management</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td class="p-3"><p class="text-muted small mb-2">Dein API-Key wird benötigt, um Sensor-Daten von externen Geräten (z.B. einem ESP32) an Cazubu zu senden.</p><strong>Dein API-Key:</strong><br><input type="text" class="form-control mt-1" value="<?php echo htmlspecialchars($user_data['api_key'] ?? 'Noch kein Key generiert'); ?>" readonly></td></tr>
|
||||
<tr>
|
||||
<td class="p-3 text-end">
|
||||
<form id="generate-api-key-form">
|
||||
<input type="hidden" name="action" value="generate_api_key">
|
||||
<button type="submit" class="btn btn-cazubu">Neuen API-Key generieren</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="changePasswordModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header"><h5 class="modal-title">Passwort ändern</h5><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div>
|
||||
<div class="modal-body">
|
||||
<form id="change-password-form">
|
||||
<input type="hidden" name="action" value="change_password">
|
||||
<div class="mb-3"><label for="current_password" class="form-label">Aktuelles Passwort</label><input type="password" class="form-control" name="current_password" id="current_password" required></div>
|
||||
<div class="mb-3"><label for="new_password" class="form-label">Neues Passwort</label><input type="password" class="form-control" name="new_password" id="new_password" required></div>
|
||||
<div class="mb-3"><label for="confirm_new_password" class="form-label">Neues Passwort bestätigen</label><input type="password" class="form-control" name="confirm_new_password" id="confirm_new_password" required></div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer"><button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button><button type="submit" class="btn btn-cazubu" form="change-password-form">Neues Passwort speichern</button></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php require_once 'includes/footer.php'; ?>
|
||||
58
register.php
Normal file
58
register.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
require_once "includes/db_connect.php";
|
||||
$username = $password = $confirm_password = "";
|
||||
$username_err = $password_err = $confirm_password_err = "";
|
||||
if($_SERVER["REQUEST_METHOD"] == "POST"){
|
||||
if(empty(trim($_POST["username"]))){ $username_err = "Bitte geben Sie einen Benutzernamen ein.";
|
||||
} else{
|
||||
$sql = "SELECT id FROM users WHERE username = ?";
|
||||
if($stmt = $mysqli->prepare($sql)){
|
||||
$stmt->bind_param("s", $param_username); $param_username = trim($_POST["username"]);
|
||||
if($stmt->execute()){
|
||||
$stmt->store_result();
|
||||
if($stmt->num_rows == 1){ $username_err = "Dieser Benutzername ist bereits vergeben."; } else{ $username = trim($_POST["username"]); }
|
||||
} else{ echo "Oops! Etwas ist schief gelaufen."; }
|
||||
$stmt->close();
|
||||
}
|
||||
}
|
||||
if(empty(trim($_POST["password"]))){ $password_err = "Bitte geben Sie ein Passwort ein."; } elseif(strlen(trim($_POST["password"])) < 6){ $password_err = "Das Passwort muss mindestens 6 Zeichen lang sein."; } else{ $password = trim($_POST["password"]); }
|
||||
if(empty(trim($_POST["confirm_password"]))){ $confirm_password_err = "Bitte bestätigen Sie das Passwort."; } else{ $confirm_password = trim($_POST["confirm_password"]); if(empty($password_err) && ($password != $confirm_password)){ $confirm_password_err = "Die Passwörter stimmen nicht überein."; } }
|
||||
if(empty($username_err) && empty($password_err) && empty($confirm_password_err)){
|
||||
$sql = "INSERT INTO users (username, password_hash) VALUES (?, ?)";
|
||||
if($stmt = $mysqli->prepare($sql)){
|
||||
$stmt->bind_param("ss", $param_username, $param_password);
|
||||
$param_username = $username;
|
||||
$param_password = password_hash($password, PASSWORD_DEFAULT);
|
||||
if($stmt->execute()){ header("location: login.php"); } else{ echo "Oops! Etwas ist schief gelaufen."; }
|
||||
$stmt->close();
|
||||
}
|
||||
}
|
||||
$mysqli->close();
|
||||
}
|
||||
define('APP_VERSION', '8.3.0');
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Registrierung - Cazubu</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="css/style.css?v=<?php echo APP_VERSION; ?>">
|
||||
</head>
|
||||
<body>
|
||||
<div class="auth-container">
|
||||
<div class="auth-form-wrapper">
|
||||
<div class="text-center mb-4"><img src="logo.png" alt="Cazubu Logo" style="max-width: 120px;"><h2 class="mt-2">Neues Konto erstellen</h2></div>
|
||||
<form action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]); ?>" method="post" novalidate>
|
||||
<div class="mb-3"><label for="username" class="form-label">Benutzername</label><input type="text" name="username" id="username" class="form-control <?php echo (!empty($username_err)) ? 'is-invalid' : ''; ?>" value="<?php echo htmlspecialchars($username); ?>" required><div class="invalid-feedback"><?php echo $username_err; ?></div></div>
|
||||
<div class="mb-3"><label for="password" class="form-label">Passwort</label><input type="password" name="password" id="password" class="form-control <?php echo (!empty($password_err)) ? 'is-invalid' : ''; ?>" required><div class="invalid-feedback"><?php echo $password_err; ?></div></div>
|
||||
<div class="mb-3"><label for="confirm_password" class="form-label">Passwort bestätigen</label><input type="password" name="confirm_password" id="confirm_password" class="form-control <?php echo (!empty($confirm_password_err)) ? 'is-invalid' : ''; ?>" required><div class="invalid-feedback"><?php echo $confirm_password_err; ?></div></div>
|
||||
<div class="d-grid"><button type="submit" class="btn btn-primary">Konto erstellen</button></div>
|
||||
<p class="text-center mt-3">Haben Sie bereits ein Konto? <a href="login.php">Hier anmelden</a></p>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
64
seeds.php
Normal file
64
seeds.php
Normal file
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
require_once 'includes/auth_check.php';
|
||||
require_once 'includes/db_connect.php';
|
||||
$user_id = $_SESSION['user_id'];
|
||||
$seeds = [];
|
||||
$sql = "SELECT * FROM seeds WHERE user_id = ? ORDER BY strain_name ASC";
|
||||
if ($stmt = $mysqli->prepare($sql)) { $stmt->bind_param("i", $user_id); $stmt->execute(); $result = $stmt->get_result(); while ($row = $result->fetch_assoc()) { $seeds[] = $row; } $stmt->close(); }
|
||||
require_once 'includes/header.php';
|
||||
?>
|
||||
<div class="card header-card mb-4">
|
||||
<div class="card-body d-flex justify-content-between align-items-center">
|
||||
<div><h1 class="mb-0">Samen-Verwaltung</h1><p class="card-text text-white-50 mt-2">Verwalte hier deine gesamte Sammlung an Cannabis-Samen.</p></div>
|
||||
<button class="btn btn-cazubu" data-bs-toggle="modal" data-bs-target="#seedModal">+ Neuer Samen</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table id="seeds-table" class="table table-hover cazubu-table-frameless">
|
||||
<thead class="table-dark">
|
||||
<tr>
|
||||
<th>Sortenname</th>
|
||||
<th>Interne Bez.</th>
|
||||
<th style="width: 25%;">Genetik</th>
|
||||
<th>Typ</th>
|
||||
<th>Anzahl</th>
|
||||
<th style="width: 120px;" class="no-sort">Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($seeds)): ?><tr><td colspan="6" class="text-center p-4"><i>Keine Samen angelegt.</i></td></tr><?php endif; ?>
|
||||
<?php foreach ($seeds as $seed): ?>
|
||||
<tr>
|
||||
<td class="align-middle">
|
||||
<span class="fw-bold"><?php echo htmlspecialchars($seed['strain_name']); ?></span>
|
||||
<?php if (!empty($seed['description'])): ?><span class="notes-icon" data-bs-toggle="tooltip" data-bs-placement="right" title="<?php echo htmlspecialchars($seed['description']); ?>"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-chat-left-text-fill" viewBox="0 0 16 16"><path d="M0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H4.414a1 1 0 0 0-.707.293L.854 15.146A.5.5 0 0 1 0 14.793V2zm3.5 1a.5.5 0 0 0 0 1h9a.5.5 0 0 0 0-1h-9zm0 2.5a.5.5 0 0 0 0 1h9a.5.5 0 0 0 0-1h-9zm0 2.5a.5.5 0 0 0 0 1h5a.5.5 0 0 0 0-1h-5z"/></svg></span><?php endif; ?>
|
||||
<?php if (!empty($seed['info_url'])): ?><a href="<?php echo htmlspecialchars($seed['info_url']); ?>" target="_blank" title="Weitere Infos" class="info-link">🔗</a><?php endif; ?>
|
||||
</td>
|
||||
<td class="align-middle"><?php echo htmlspecialchars($seed['internal_name']); ?></td>
|
||||
<td class="align-middle">
|
||||
<div class="d-flex justify-content-between" style="font-size: 0.8em; color: #6c757d;">
|
||||
<span>Sativa</span>
|
||||
<span>Indica</span>
|
||||
</div>
|
||||
<div class="progress" role="progressbar" aria-label="Sativa/Indica ratio" style="height: 18px; font-size: .75rem;">
|
||||
<div class="progress-bar text-bg-warning" style="width: <?php echo $seed['ratio_sativa']; ?>%"><?php echo $seed['ratio_sativa']; ?>%</div>
|
||||
<div class="progress-bar text-bg-success" style="width: <?php echo 100 - $seed['ratio_sativa']; ?>%"><?php echo 100 - $seed['ratio_sativa']; ?>%</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="align-middle">
|
||||
<?php $badge_class = $seed['is_autoflower'] ? 'text-bg-info' : 'text-bg-dark'; $badge_text = $seed['is_autoflower'] ? 'Autoflower' : 'Photoperiodisch'; echo "<span class='badge {$badge_class}'>{$badge_text}</span>"; ?>
|
||||
</td>
|
||||
<td class="align-middle">
|
||||
<span class="badge text-bg-secondary fs-6"><?php echo $seed['stock_count']; ?></span>
|
||||
</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-outline-primary edit-seed-btn" data-bs-toggle="modal" data-bs-target="#seedModal" data-seed='<?php echo json_encode($seed, JSON_HEX_APOS | JSON_HEX_QUOT); ?>'>✏️</button>
|
||||
<button class="btn btn-sm btn-outline-danger delete-btn" data-bs-toggle="modal" data-bs-target="#deleteConfirmModal" data-id="<?php echo $seed['id']; ?>" data-name="<?php echo htmlspecialchars($seed['strain_name']); ?>" data-type="seed">🗑️</button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="modal fade" id="seedModal" tabindex="-1"><div class="modal-dialog modal-lg"><div class="modal-content"><div class="modal-header"><h5 class="modal-title" id="seedModalLabel">Samen</h5><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body"><form id="seed-form"><input type="hidden" name="action" id="seed-form-action"><input type="hidden" name="id" id="seed-id"><div class="row"><div class="col-md-6"><div class="mb-3"><label class="form-label">Sortenname</label><input type="text" class="form-control" name="strain_name" required></div></div><div class="col-md-6"><div class="mb-3"><label class="form-label">Interne Bezeichnung (optional)</label><input type="text" class="form-control" name="internal_name"></div></div></div><div class="row"><div class="col-md-8"><div class="mb-3"><label class="form-label">Info-URL</label><input type="url" class="form-control" name="info_url" placeholder="https://..."></div></div><div class="col-md-4"><div class="mb-3"><label class="form-label">Anzahl</label><input type="number" class="form-control" name="stock_count" min="0" value="0" required></div></div></div><div class="mb-3"><label for="ratio_sativa" class="form-label">Genetik: <span id="sativa-value-label">50</span>% Sativa / <span id="indica-value-label">50</span>% Indica</label><input type="range" class="form-range" id="ratio_sativa" name="ratio_sativa" min="0" max="100" step="5" value="50"></div><div class="form-check mb-3"><input class="form-check-input" type="checkbox" name="is_autoflower" value="1" id="is_autoflower"><label class="form-check-label" for="is_autoflower">Selbstblühend (Autoflower)</label></div><div class="mb-3"><label class="form-label">Eigene Kurzbeschreibung</label><textarea class="form-control" name="description" rows="3"></textarea></div></form></div><div class="modal-footer"><button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Schließen</button><button type="submit" class="btn btn-primary" form="seed-form">Speichern</button></div></div></div></div>
|
||||
<div class="modal fade" id="deleteConfirmModal" tabindex="-1"><div class="modal-dialog"><div class="modal-content"><div class="modal-header"><h5 class="modal-title">Löschen bestätigen</h5><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body"><p>Sind Sie sicher, dass Sie <strong id="item-type-to-delete"></strong> "<strong id="item-name-to-delete"></strong>" endgültig löschen möchten?</p><p class="text-danger" id="delete-warning"></p></div><div class="modal-footer"><button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button><button type="button" class="btn btn-danger" id="confirmDeleteBtn">Ja, endgültig löschen</button></div></div></div></div>
|
||||
<?php require_once 'includes/footer.php'; ?>
|
||||
139
sensors.php
Normal file
139
sensors.php
Normal file
@@ -0,0 +1,139 @@
|
||||
<?php
|
||||
require_once 'includes/auth_check.php';
|
||||
require_once 'includes/db_connect.php';
|
||||
|
||||
$user_id = $_SESSION['user_id'];
|
||||
|
||||
// Lade alle aktiven Pflanzen, die mindestens einen Sensoreintrag haben
|
||||
$plants_with_sensors = [];
|
||||
$sql = "SELECT DISTINCT p.id,
|
||||
s.strain_name,
|
||||
s.internal_name,
|
||||
z.name AS zone_name,
|
||||
c.name AS container_name
|
||||
FROM plants p
|
||||
JOIN seeds s ON p.seed_id = s.id
|
||||
JOIN zones z ON p.zone_id = z.id
|
||||
JOIN containers c ON p.container_id = c.id
|
||||
JOIN sensor_data sd ON p.id = sd.plant_id
|
||||
WHERE p.user_id = ? AND p.status = 'Eingepflanzt'
|
||||
ORDER BY c.name ASC";
|
||||
|
||||
if ($stmt = $mysqli->prepare($sql)) {
|
||||
$stmt->bind_param("i", $user_id);
|
||||
$stmt->execute();
|
||||
$result = $stmt->get_result();
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$plants_with_sensors[] = $row;
|
||||
}
|
||||
$stmt->close();
|
||||
}
|
||||
|
||||
require_once 'includes/header.php';
|
||||
?>
|
||||
|
||||
<div class="card header-card glass-effect mb-4">
|
||||
<div class="card-body">
|
||||
<h1 class="mb-0">Sensor-Übersicht</h1>
|
||||
<p class="card-text text-white-50 mt-2">Live-Daten aller deiner aktiven Pflanzen.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<?php if (empty($plants_with_sensors)): ?>
|
||||
<div class="col-12">
|
||||
<div class="cazubu-table-frameless glass-effect p-4 text-center">
|
||||
<h5 class="text-white">Keine Sensordaten verfügbar</h5>
|
||||
<p class="text-white-50">Für deine aktiven Pflanzen wurden noch keine Sensordaten über die API gesendet.</p>
|
||||
</div>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<?php foreach ($plants_with_sensors as $plant): ?>
|
||||
<div class="col-lg-6 mb-4">
|
||||
<div class="cazubu-table-frameless glass-effect">
|
||||
<table class="table mb-0">
|
||||
<thead class="table-dark">
|
||||
<tr><th><a href="plant_detail.php?id=<?php echo $plant['id']; ?>" class="text-white text-decoration-none h5 mb-0"><?php echo htmlspecialchars($plant['container_name']) . ' (' . htmlspecialchars($plant['zone_name']) . ') - ' . htmlspecialchars($plant['strain_name']) . ' (' . htmlspecialchars($plant['internal_name']) . ')'; ?></a></th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="p-3">
|
||||
<div class="row sensor-chart-group" data-plant-id="<?php echo $plant['id']; ?>">
|
||||
<div class="col-md-6" id="temp-chart-wrapper-<?php echo $plant['id']; ?>">
|
||||
<div class="text-center p-4"><div class="spinner-border text-light spinner-border-sm" role="status"></div></div>
|
||||
</div>
|
||||
<div class="col-md-6" id="humidity-chart-wrapper-<?php echo $plant['id']; ?>">
|
||||
<div class="text-center p-4"><div class="spinner-border text-light spinner-border-sm" role="status"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php require_once 'includes/footer.php'; ?>
|
||||
|
||||
<style>
|
||||
.chart-wrapper {
|
||||
position: relative;
|
||||
height: 200px; /* Höhe weiter reduziert für kompaktere Darstellung */
|
||||
width: 100%;
|
||||
background-color: rgba(255,255,255,0.7);
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
$('.sensor-chart-group').each(function() {
|
||||
const plantId = $(this).data('plant-id');
|
||||
const tempWrapper = $(this).find('#temp-chart-wrapper-' + plantId);
|
||||
const humidityWrapper = $(this).find('#humidity-chart-wrapper-' + plantId);
|
||||
|
||||
$.ajax({
|
||||
type: 'GET',
|
||||
url: 'ajax_handler.php',
|
||||
data: { action: 'get_sensor_data', plant_id: plantId },
|
||||
dataType: 'json',
|
||||
success: function(response) {
|
||||
tempWrapper.empty();
|
||||
humidityWrapper.empty();
|
||||
|
||||
if (response.success && response.data.labels.length > 0) {
|
||||
// Temperatur-Chart
|
||||
if (response.data.temperature.some(v => v !== null)) {
|
||||
tempWrapper.append('<h6 class="text-dark text-center small">Temperatur</h6>');
|
||||
let tempCanvas = $('<canvas>');
|
||||
tempWrapper.append($('<div>').addClass('chart-wrapper').append(tempCanvas));
|
||||
new Chart(tempCanvas, { type: 'line', data: { labels: response.data.labels, datasets: [{ label: '°C', data: response.data.temperature, borderColor: 'rgba(255, 99, 132, 1)', tension: 0.2, spanGaps: true }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { x: { ticks: { color: '#212529', display: false } }, y: { ticks: { color: '#212529' } } } } });
|
||||
} else {
|
||||
tempWrapper.html('<div class="chart-wrapper d-flex align-items-center justify-content-center"><p class="text-muted mb-0">Keine Temperaturdaten.</p></div>');
|
||||
}
|
||||
// Feuchtigkeits-Chart
|
||||
if (response.data.humidity.some(v => v !== null)) {
|
||||
humidityWrapper.append('<h6 class="text-dark text-center small">Feuchtigkeit</h6>');
|
||||
let humidityCanvas = $('<canvas>');
|
||||
humidityWrapper.append($('<div>').addClass('chart-wrapper').append(humidityCanvas));
|
||||
new Chart(humidityCanvas, { type: 'line', data: { labels: response.data.labels, datasets: [{ label: '%', data: response.data.humidity, borderColor: 'rgba(54, 162, 235, 1)', tension: 0.2, spanGaps: true }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { x: { ticks: { color: '#212529', display: false } }, y: { ticks: { color: '#212529' } } } } });
|
||||
} else {
|
||||
humidityWrapper.html('<div class="chart-wrapper d-flex align-items-center justify-content-center"><p class="text-muted mb-0">Keine Feuchtigkeitsdaten.</p></div>');
|
||||
}
|
||||
} else {
|
||||
const noDataHtml = '<div class="col-12 text-center"><p class="text-white-50">Keine Sensordaten.</p></div>';
|
||||
chartsContainer.html(noDataHtml);
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
const errorHtml = '<div class="col-12 text-center"><p class="text-danger">Fehler beim Laden.</p></div>';
|
||||
chartsContainer.html(errorHtml);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
BIN
uploads/user_1/plant_1_685d315173aef1.94747216.png
Normal file
BIN
uploads/user_1/plant_1_685d315173aef1.94747216.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 768 KiB |
BIN
wallpaper.png
Normal file
BIN
wallpaper.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.1 MiB |
Reference in New Issue
Block a user