Feature Complete: Modern Glass UI, Sensor History, Seed Genetics & Cleanup
All checks were successful
Docker Build & Push / build-and-push (push) Successful in 28s

This commit is contained in:
Gemini Bot
2025-12-07 19:07:29 +00:00
parent 0d0b57adc9
commit c13076a291
13 changed files with 23687 additions and 235 deletions

125
README.md
View File

@@ -1,73 +1,98 @@
# Cazubu - Pflanzenverwaltung # Cazubu - Dein Cannabis Zucht Buddy 🌱
Cazubu ist eine Webanwendung zur Verwaltung von Pflanzen, Inventar, Samen und Sensoren. Cazubu ist eine moderne Webanwendung zur Verwaltung und Überwachung deiner Cannabis-Zucht ("Homegrow"). Sie hilft dir, den Überblick über Pflanzen, Inventar, Samen und Umweltbedingungen zu behalten.
## Projektstruktur ![Cazubu Dashboard](assets/dummy_plant.png)
Das Projekt ist so aufgebaut, dass der Quellcode vom Deployment-Setup getrennt ist:
- **`src/`**: Enthält den gesamten PHP-Quellcode, Assets und Frontend-Ressourcen.
- **`Dockerfile`**: Definition des Docker-Images.
- **`.gitea/workflows`**: CI/CD-Konfiguration.
## Installation & Betrieb ## Funktionen 🚀
### 1. Pflanzenverwaltung 🌿
* **Lebenszyklus:** Dokumentiere jeden Schritt vom Keimen bis zur Ernte.
* **Details:** Erfasse Sorte, Standort (Zone/Zelt), Phase (Wachstum/Blüte) und Alter.
* **Bildergalerie:** Lade Fotos deiner Pflanzen hoch, um die Entwicklung festzuhalten.
* **Aktivitäten-Log:** Protokolliere Gießen, Düngen, Umtopfen oder Beschneiden.
* **Messungen:** Erfasse regelmäßig die Höhe deiner Pflanzen.
### 2. Sensor-Überwachung 🌡️
* **Live-Daten:** Visualisierung von Temperatur und Luftfeuchtigkeit.
* **Historie:** Interaktive Graphen für 24h, 7 Tage oder 30 Tage.
* **API-Schnittstelle:** Einfache Integration von ESP32/Arduino Sensoren.
### 3. Inventar & Samenbank 📦
* **Zonen:** Definiere deine Anbaubereiche (z.B. "Zelt 1", "Outdoor").
* **Gefäße:** Verwalte deine Töpfe und deren Größen.
* **Samen:** Behalte den Überblick über deine Genetik-Sammlung (Sativa/Indica Anteil, Autoflower, Anzahl).
### 4. Dashboard 📊
* Schneller Überblick über offene Aufgaben und Bestände.
* Direkteinstieg in die wichtigsten Bereiche.
---
## Installation 🛠️
### Voraussetzungen ### Voraussetzungen
- Docker Engine * Docker & Docker Compose
- Eine laufende MySQL/MariaDB Datenbank * Alternativ: Webserver mit PHP 8.2+ und MySQL/MariaDB
### Docker Build & Run ### Docker Quickstart
1. **Image bauen:** 1. **Repository klonen:**
```bash ```bash
docker build -t cazubu . git clone https://git.klenzel.net/admin/cazubu.git
cd cazubu
``` ```
2. **Container starten (Produktiv):** 2. **Container starten:**
Für den produktiven Einsatz sollten Uploads (Bilder) persistent gespeichert werden. Dafür binden wir ein Volume an `/var/www/html/uploads`.
```bash ```bash
docker run -d \ docker-compose up -d
-p 8080:80 \
-e DB_SERVER="deine-db-ip" \
-e DB_USERNAME="cazubu" \
-e DB_PASSWORD="dein-sicheres-passwort" \
-e DB_NAME="cazubu" \
-v cazubu_uploads:/var/www/html/uploads \
--name cazubu-app \
--restart unless-stopped \
cazubu
``` ```
*Die App ist nun unter `http://localhost:8090` erreichbar.*
### Umgebungsvariablen 3. **Standard-Login:**
* Benutzer: `testnutzer`
* Passwort: `Start123!`
Die Anwendung wird über Umgebungsvariablen konfiguriert. Wenn diese nicht gesetzt sind, werden interne Fallbacks (für Entwicklung) genutzt. ### Manuelle Installation (ohne Docker)
1. Kopiere den Inhalt von `src/` in dein Webroot.
2. Importiere `src/database/install.sql` in deine Datenbank.
3. Passe `src/includes/db_connect.php` oder die Umgebungsvariablen an.
| Variable | Beschreibung | Standard (Dev) | ---
|----------|--------------|----------------|
| `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` |
### Persistente Daten ## Bedienungsanleitung 📖
User-Uploads (Pflanzenbilder) werden im Container unter `/var/www/html/uploads` gespeichert.
Um Datenverlust beim Neustart/Update des Containers zu vermeiden, **muss** dieses Verzeichnis als Volume gemountet werden (siehe oben).
## Entwicklung ### Erste Schritte
1. **Inventar anlegen:** Gehe zu "Inventar" und erstelle mindestens eine **Zone** (z.B. "Growbox") und ein **Pflanzgefäß** (z.B. "11L Topf").
2. **Samen erfassen:** Trage deine Samen unter "Samen" ein.
3. **Pflanze starten:** Klicke auf dem Dashboard oder unter "Pflanzen" auf "+ Neue Pflanze". Wähle Samen, Zone und Topf aus.
Um lokal zu entwickeln, ohne jedes Mal das Image neu zu bauen, kann der `src`-Ordner direkt in den Container gemountet werden: ### Sensoren verbinden
Jeder Benutzer hat einen eigenen API-Key (siehe "Profil").
Sende Sensordaten per HTTP GET Request an:
```bash
docker run -d \
-p 8080:80 \
-v $(pwd)/src:/var/www/html \
--name cazubu-dev \
cazubu
``` ```
http://DEINE-IP/api.php?apikey=DEIN_KEY&pflanze=PFLANZEN_ID&sensor=temp&wert=24.5
http://DEINE-IP/api.php?apikey=DEIN_KEY&pflanze=PFLANZEN_ID&sensor=humidity&wert=60
```
*Die `PFLANZEN_ID` findest du in der Detailansicht der Pflanze.*
---
## Tech Stack 💻
* **Frontend:** Bootstrap 5.3, Chart.js, jQuery
* **Backend:** PHP 8.2 (Native)
* **Datenbank:** MySQL / MariaDB
* **Design:** Custom "Organic Light Glass" Theme (CSS Variables, Backdrop Filter)
---
## Changelog ## Changelog
**v18.3.0** (Aktuell)
* Redesign: Modernes "Light Glass" UI.
* Feature: Sensor-Historie mit Zeitfilter (24h/7d/30d).
* Verbesserung: Erweiterte Samen-Datenbank mit Genetik-Anzeige.
* Fix: Diverse Bugfixes in Tabellen und DataTables.
### 2025-12-07 - Refactoring & Dockerisierung **v1.0.0**
- **Struktur:** Quellcode nach `src/` verschoben für saubereres Root-Verzeichnis. * Initiale Dockerisierung und Grundfunktionen.
- **Docker:** `Dockerfile` angepasst (kopiert nun `src/`).
- **Doku:** `README.md` erweitert um detaillierte Deployment-Infos und Volume-Handling.
- **CI/CD:** Pipeline für automatischen Build & Push eingerichtet.

23141
db-init/init.sql Normal file

File diff suppressed because it is too large Load Diff

55
docker-compose.yml Normal file
View File

@@ -0,0 +1,55 @@
version: '3.8'
services:
app:
build: .
container_name: cazubu-app
ports:
- "8090:80"
volumes:
- ./src:/var/www/html
environment:
- DB_SERVER=db
- DB_USERNAME=cazubu
- DB_PASSWORD=dev_secret
- DB_NAME=cazubu
depends_on:
- db
networks:
- cazubu-net
db:
image: mariadb:10.11
container_name: cazubu-db
environment:
- MYSQL_ROOT_PASSWORD=root_secret
- MYSQL_DATABASE=cazubu
- MYSQL_USER=cazubu
- MYSQL_PASSWORD=dev_secret
volumes:
- db_data:/var/lib/mysql
- ./db-init:/docker-entrypoint-initdb.d
ports:
- "3306:3306"
networks:
- cazubu-net
phpmyadmin:
image: phpmyadmin/phpmyadmin
container_name: cazubu-pma
environment:
- PMA_HOST=db
- PMA_USER=cazubu
- PMA_PASSWORD=dev_secret
ports:
- "8081:80"
depends_on:
- db
networks:
- cazubu-net
volumes:
db_data:
networks:
cazubu-net:

View File

@@ -314,13 +314,33 @@ try {
case 'get_sensor_data': 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; } 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']; $plant_id = $_GET['plant_id'];
$range = $_GET['range'] ?? '24h';
$sql_check = "SELECT id FROM plants WHERE id = ? AND user_id = ?"; $sql_check = "SELECT id FROM plants WHERE id = ? AND user_id = ?";
$stmt_check = $mysqli->prepare($sql_check); $stmt_check = $mysqli->prepare($sql_check);
$stmt_check->bind_param("ii", $plant_id, $user_id); $stmt_check->bind_param("ii", $plant_id, $user_id);
$stmt_check->execute(); $stmt_check->execute();
if ($stmt_check->get_result()->num_rows === 0) { echo json_encode(['success' => false, 'message' => 'Keine Berechtigung.']); exit; } if ($stmt_check->get_result()->num_rows === 0) { echo json_encode(['success' => false, 'message' => 'Keine Berechtigung.']); exit; }
$stmt_check->close(); $stmt_check->close();
$sql = "SELECT sensor_type, value, timestamp FROM sensor_data WHERE plant_id = ? ORDER BY timestamp ASC";
// Filter & Grouping Logik
$interval = "INTERVAL 24 HOUR";
$group_by = "FLOOR(UNIX_TIMESTAMP(timestamp) / 3600)"; // Default: Stündlich
if ($range == '7d') {
$interval = "INTERVAL 7 DAY";
$group_by = "FLOOR(UNIX_TIMESTAMP(timestamp) / 3600)"; // Stündlich für 7 Tage (ca 168 Punkte)
} elseif ($range == '30d') {
$interval = "INTERVAL 30 DAY";
$group_by = "FLOOR(UNIX_TIMESTAMP(timestamp) / 14400)"; // Alle 4 Stunden für 30 Tage (ca 180 Punkte)
}
$sql = "SELECT sensor_type, AVG(value) as value, MIN(timestamp) as timestamp
FROM sensor_data
WHERE plant_id = ? AND timestamp >= NOW() - $interval
GROUP BY sensor_type, $group_by
ORDER BY timestamp ASC";
$stmt = $mysqli->prepare($sql); $stmt = $mysqli->prepare($sql);
$stmt->bind_param("i", $plant_id); $stmt->bind_param("i", $plant_id);
$stmt->execute(); $stmt->execute();
@@ -328,17 +348,28 @@ try {
$raw_data = []; $raw_data = [];
while ($row = $result->fetch_assoc()) { $raw_data[] = $row; } while ($row = $result->fetch_assoc()) { $raw_data[] = $row; }
$stmt->close(); $stmt->close();
$data_by_timestamp = []; $data_by_timestamp = [];
foreach ($raw_data as $row) { foreach ($raw_data as $row) {
$timestamp = $row['timestamp']; // Formatierung des Timestamps für Chart (kürzer bei langen Zeiträumen)
if (!isset($data_by_timestamp[$timestamp])) { $data_by_timestamp[$timestamp] = ['temperatur' => null, 'feuchtigkeit' => null]; } $ts = $row['timestamp'];
if ($row['sensor_type'] == 'Temperatur') { $data_by_timestamp[$timestamp]['temperatur'] = (float)$row['value']; } $date_key = $ts; // Als Key nutzen wir den Timestamp direkt zum Sortieren
elseif ($row['sensor_type'] == 'Feuchtigkeit') { $data_by_timestamp[$timestamp]['feuchtigkeit'] = (float)$row['value']; }
if (!isset($data_by_timestamp[$date_key])) { $data_by_timestamp[$date_key] = ['temperatur' => null, 'feuchtigkeit' => null]; }
if ($row['sensor_type'] == 'Temperatur') { $data_by_timestamp[$date_key]['temperatur'] = (float)$row['value']; }
elseif ($row['sensor_type'] == 'Feuchtigkeit') { $data_by_timestamp[$date_key]['feuchtigkeit'] = (float)$row['value']; }
} }
ksort($data_by_timestamp); ksort($data_by_timestamp);
$chart_data = ['labels' => [], 'temperature' => [], 'humidity' => []]; $chart_data = ['labels' => [], 'temperature' => [], 'humidity' => []];
foreach ($data_by_timestamp as $timestamp => $values) { foreach ($data_by_timestamp as $ts => $values) {
$chart_data['labels'][] = date('d.m H:i', strtotime($timestamp)); // Label Formatierung basierend auf Range
$label = date('d.m H:i', strtotime($ts));
if ($range == '30d') { $label = date('d.m.', strtotime($ts)); }
$chart_data['labels'][] = $label;
$chart_data['temperature'][] = $values['temperatur']; $chart_data['temperature'][] = $values['temperatur'];
$chart_data['humidity'][] = $values['feuchtigkeit']; $chart_data['humidity'][] = $values['feuchtigkeit'];
} }

View File

@@ -1,20 +1,110 @@
/* /*
* CAZUBU Custom Stylesheet * CAZUBU Custom Stylesheet
* Version 9.2 - Finaler Feinschliff * Version 9.2 - Finaler Feinschliff (Refactored for Clean HTML)
*/ */
/* Globale Stile & Layout */ /* Globale Stile & Layout */
body, html { height: 100%; } body, html { height: 100%; }
body { background-image: url('../wallpaper.png'); background-size: cover; background-position: center; background-attachment: fixed; } body {
background-image: url('../wallpaper.png');
background-size: cover;
background-position: center;
background-attachment: fixed;
font-family: 'Inter', system-ui, -apple-system, sans-serif;
font-weight: 400;
}
.site-container { display: flex; min-height: 100vh; } .site-container { display: flex; min-height: 100vh; }
.side-menu { width: 280px; background-color: #2f3640; color: #f5f6fa; }
.project-name { font-weight: bold; color: white; } /* Sidebar */
.nav-link-header { font-weight: bold; color: #888; padding: .5rem 1rem; font-size: 0.8rem; text-transform: uppercase; } .side-menu {
width: 280px;
flex-shrink: 0;
background-color: rgba(47, 54, 64, 0.85);
color: #f5f6fa;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
}
.project-name { font-weight: 700; color: white; font-family: 'Poppins', sans-serif; letter-spacing: 0.5px; }
/* Modern Sidebar Navigation */
.nav-link-header {
font-family: 'Poppins', sans-serif;
font-weight: 600;
color: #a4b0be;
padding: 1rem 1rem 0.5rem 1rem;
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 1.5px;
}
.side-menu .nav-pills { padding: 0 0.5rem; }
.side-menu .nav-link {
color: #dfe6e9;
padding: 0.75rem 1rem;
margin-bottom: 0.25rem;
border-radius: 0.5rem;
transition: all 0.2s ease;
font-weight: 500;
font-size: 0.95rem;
display: flex;
align-items: center;
}
.side-menu .nav-link i {
margin-right: 12px;
font-size: 1.1em;
opacity: 0.8;
}
.side-menu .nav-link:hover {
background-color: rgba(255, 255, 255, 0.1);
color: #fff;
transform: translateX(4px);
}
.side-menu .nav-link.active {
background-color: #556B2F; /* Primary Green */
color: #fff;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
font-weight: 600;
}
.side-menu .nav-link.active i { opacity: 1; }
.site-logo { width: 80%; max-width: 180px; height: auto; margin-bottom: 10px; } .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; } /* Main Content */
.modal-content { background-color: rgba(233, 236, 239, 0.95) !important; color: #212529; } .main-content { flex-grow: 1; padding: 30px 10%; color: #fff; }
.main-content h3.text-white, .main-content h4.text-white { text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.8); }
/* Headers & Typography */
.main-content h1, .main-content h2, .main-content h3, .main-content h4, .main-content h5, .main-content h6 {
color: #fff;
font-family: 'Poppins', sans-serif;
font-weight: 600;
text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.8);
letter-spacing: -0.5px;
}
.card-text { color: rgba(255,255,255,0.7); }
.text-muted { color: #a4b0be !important; }
/* Cards */
.header-card { background-color: #343a40 !important; color: #f8f9fa; border-radius: 0.75rem !important; margin-bottom: 1.5rem; }
/* Modals */
.modal-content {
background-color: #f8f9fa;
color: #212529;
border: none;
overflow: hidden;
}
.modal-header {
background-color: #2f3640;
color: #fff;
border-bottom: 1px solid #4a5568;
}
.modal-title { color: #fff !important; font-family: 'Poppins', sans-serif; }
.btn-close { filter: invert(1); }
/* Login/Register Seiten */ /* Login/Register Seiten */
.auth-container { min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 20px; } .auth-container { min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 20px; }
@@ -22,15 +112,38 @@ body { background-image: url('../wallpaper.png'); background-size: cover; backgr
/* Buttons & Icons */ /* 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; } .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; } .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 */ /* 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 { 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 thead th { background-color: #2f3640; color: white; border: none; text-align: left !important; vertical-align: middle; padding: 1rem; }
.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; } /* Override for centered headers */
.cazubu-table-frameless thead th.text-center { text-align: center !important; }
.cazubu-table-frameless tbody td {
background-color: rgba(233, 236, 239, 0.85) !important; /* Mehr Transparenz */
color: #212529;
border-top: 1px solid rgba(47, 54, 64, 0.15);
vertical-align: middle;
padding: 1rem;
backdrop-filter: blur(5px); /* Glas-Effekt */
-webkit-backdrop-filter: blur(5px);
}
/* Fix Active Button Contrast */
.btn-outline-dark.active, .btn-outline-dark:active {
color: #fff !important;
background-color: #212529;
border-color: #212529;
}
/* Fix Tab Content Headings (Readable on Light BG) */
.tab-content h5, .tab-content h6 {
color: #212529 !important;
text-shadow: none !important;
font-weight: 600;
}
.cazubu-table-frameless tbody tr:first-child td { border-top: none; } .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.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 .text-muted { color: #495057 !important; }
@@ -62,11 +175,13 @@ body { background-image: url('../wallpaper.png'); background-size: cover; backgr
.dataTables_wrapper .dataTables_info, .dataTables_wrapper .dataTables_info,
.dataTables_wrapper .dataTables_paginate { display: none !important; } .dataTables_wrapper .dataTables_paginate { display: none !important; }
table.dataTable.no-footer { border-bottom: none !important; } table.dataTable.no-footer { border-bottom: none !important; }
table.dataTable thead > tr > th.sorting:before,
table.dataTable thead > tr > th.sorting:after, /* Dashboard Summary Cards alignment fix */
table.dataTable thead > tr > th.sorting_asc:before, .col-lg-4.d-flex .cazubu-table-frameless {
table.dataTable thead > tr > th.sorting_asc:after, height: 100%;
table.dataTable thead > tr > th.sorting_desc:before, }
table.dataTable thead > tr > th.sorting_desc:after { .col-lg-4.d-flex .cazubu-table-frameless tbody,
opacity: 0.5; .col-lg-4.d-flex .cazubu-table-frameless tr,
.col-lg-4.d-flex .cazubu-table-frameless td {
height: 100%;
} }

View File

@@ -9,6 +9,13 @@ define('APP_VERSION', '18.3.0');
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cazubu - Cannabis Zucht Buddy</title> <title>Cazubu - Cannabis Zucht Buddy</title>
<!-- Fonts & Icons -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&family=Poppins:wght@500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet"> <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; ?>"> <link rel="stylesheet" href="css/style.css?v=<?php echo APP_VERSION; ?>">
</head> </head>
@@ -18,27 +25,33 @@ define('APP_VERSION', '18.3.0');
</div> </div>
<div class="site-container"> <div class="site-container">
<nav class="side-menu d-flex flex-column p-3"> <nav class="side-menu d-flex flex-column p-3">
<div> <div class="text-center mb-4">
<div class="text-center">
<a href="index.php"><img src="logo.png" alt="Cazubu Logo" class="site-logo"></a> <a href="index.php"><img src="logo.png" alt="Cazubu Logo" class="site-logo"></a>
<h2 class="project-name">Cazubu</h2> <h2 class="project-name">Cazubu</h2>
</div> </div>
<hr>
<ul class="nav nav-pills flex-column"> <div class="flex-grow-1 overflow-auto">
<ul class="nav nav-pills flex-column gap-1">
<?php
$current_page = basename($_SERVER['PHP_SELF']);
?>
<?php if(isset($_SESSION["loggedin"]) && $_SESSION["loggedin"] === true): ?> <?php if(isset($_SESSION["loggedin"]) && $_SESSION["loggedin"] === true): ?>
<li><a href="index.php" class="nav-link text-white">Startseite</a></li> <li><a href="index.php" class="nav-link <?php echo ($current_page == 'index.php') ? 'active' : ''; ?>"><i class="bi bi-grid-1x2-fill me-2"></i> Startseite</a></li>
<li><a href="plants.php" class="nav-link text-white">Pflanzen</a></li> <li><a href="plants.php" class="nav-link <?php echo ($current_page == 'plants.php' || $current_page == 'plant_detail.php') ? 'active' : ''; ?>"><i class="bi bi-flower2 me-2"></i> 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 class="nav-link-header">Aktionen</li>
<li><a href="#" class="nav-link text-white" data-bs-toggle="modal" data-bs-target="#globalActivityModal">Aktivität durchführen</a></li> <li><a href="sensors.php" class="nav-link <?php echo ($current_page == 'sensors.php') ? 'active' : ''; ?>"><i class="bi bi-activity me-2"></i> Sensoren</a></li>
<li class="mt-2"><span class="nav-link-header">VERWALTUNG</span></li> <li><a href="#" class="nav-link" data-bs-toggle="modal" data-bs-target="#globalActivityModal"><i class="bi bi-lightning-charge-fill me-2"></i> Aktivität erfassen</a></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="nav-link-header">Verwaltung</li>
<li class="mt-2"><span class="nav-link-header">SYSTEM</span></li> <li><a href="inventory.php" class="nav-link <?php echo ($current_page == 'inventory.php') ? 'active' : ''; ?>"><i class="bi bi-box2-fill me-2"></i> Inventar</a></li>
<li><a href="profile.php" class="nav-link text-white">Profil</a></li> <li><a href="seeds.php" class="nav-link <?php echo ($current_page == 'seeds.php') ? 'active' : ''; ?>"><i class="bi bi-vinyl-fill me-2"></i> Samen</a></li>
<li class="nav-link-header">System</li>
<li><a href="profile.php" class="nav-link <?php echo ($current_page == 'profile.php') ? 'active' : ''; ?>"><i class="bi bi-person-circle me-2"></i> Profil</a></li>
<?php else: ?> <?php else: ?>
<li><a href="login.php" class="nav-link text-white">Login</a></li> <li><a href="login.php" class="nav-link <?php echo ($current_page == 'login.php') ? 'active' : ''; ?>">Login</a></li>
<li><a href="register.php" class="nav-link text-white">Registrieren</a></li> <li><a href="register.php" class="nav-link <?php echo ($current_page == 'register.php') ? 'active' : ''; ?>">Registrieren</a></li>
<?php endif; ?> <?php endif; ?>
</ul> </ul>
</div> </div>

View File

@@ -47,7 +47,7 @@ require_once 'includes/header.php';
<thead class="table-dark"><tr><th class="text-center">Schritt 1: Inventar</th></tr></thead> <thead class="table-dark"><tr><th class="text-center">Schritt 1: Inventar</th></tr></thead>
<tbody> <tbody>
<tr> <tr>
<td class="d-flex flex-column text-center p-4"> <td class="d-flex flex-column align-items-center text-center p-4">
<p class="text-muted">Lege deine Anbau-Zonen und Pflanzgefäße an.</p> <p class="text-muted">Lege deine Anbau-Zonen und Pflanzgefäße an.</p>
<?php if ($stats['zones'] > 0): ?> <?php if ($stats['zones'] > 0): ?>
<div class="my-auto"> <div class="my-auto">
@@ -71,7 +71,7 @@ require_once 'includes/header.php';
<thead class="table-dark"><tr><th class="text-center">Schritt 2: Samenbank</th></tr></thead> <thead class="table-dark"><tr><th class="text-center">Schritt 2: Samenbank</th></tr></thead>
<tbody> <tbody>
<tr> <tr>
<td class="d-flex flex-column text-center p-4"> <td class="d-flex flex-column align-items-center text-center p-4">
<p class="text-muted">Erfasse alle Samen, die du auf Lager hast.</p> <p class="text-muted">Erfasse alle Samen, die du auf Lager hast.</p>
<?php if ($stats['seeds'] > 0): ?> <?php if ($stats['seeds'] > 0): ?>
<div class="my-auto"> <div class="my-auto">
@@ -95,7 +95,7 @@ require_once 'includes/header.php';
<thead class="table-dark"><tr><th class="text-center">Schritt 3: Pflanzen</th></tr></thead> <thead class="table-dark"><tr><th class="text-center">Schritt 3: Pflanzen</th></tr></thead>
<tbody> <tbody>
<tr> <tr>
<td class="d-flex flex-column text-center p-4"> <td class="d-flex flex-column align-items-center text-center p-4">
<p class="text-muted">Lege deine Pflanzen an und verfolge ihren Fortschritt.</p> <p class="text-muted">Lege deine Pflanzen an und verfolge ihren Fortschritt.</p>
<?php if ($stats['plants'] > 0): ?> <?php if ($stats['plants'] > 0): ?>
<div class="my-auto"> <div class="my-auto">

View File

@@ -47,8 +47,8 @@ require_once 'includes/header.php';
<tr> <tr>
<td class="align-middle"><?php echo htmlspecialchars($zone['name']); ?></td> <td class="align-middle"><?php echo htmlspecialchars($zone['name']); ?></td>
<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-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']); ?>"><i class="bi bi-pencil-fill"></i></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> <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"><i class="bi bi-trash-fill"></i></button>
</td> </td>
</tr> </tr>
<?php endforeach; ?> <?php endforeach; ?>

View File

@@ -62,36 +62,63 @@ $(document).ready(function() {
$('#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>'); } } }); }); $('#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')); }); $(document).on('change', '#select-all-plants', function() { $('#global-activity-form .plant-checkbox').prop('checked', $(this).prop('checked')); });
// Slider Logik für Samen
$('#ratio_sativa').on('input', function() {
const sativaVal = $(this).val();
$('#sativa-value-label').text(sativaVal);
$('#indica-value-label').text(100 - sativaVal);
});
// Sensor-Graphen Logik // Sensor-Graphen Logik
$('#sensor-tab-btn').on('shown.bs.tab', function () { let tempChart = null;
if ($(this).data('loaded')) { return; } let humidityChart = null;
$(this).data('loaded', true);
const plantId = new URLSearchParams(window.location.search).get('id'); function loadSensorData(plantId, range) {
const chartsContainer = $('#sensor-charts-container'); 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>'); 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({ $.ajax({
type: 'GET', url: 'ajax_handler.php', data: { action: 'get_sensor_data', plant_id: plantId }, dataType: 'json', type: 'GET', url: 'ajax_handler.php', data: { action: 'get_sensor_data', plant_id: plantId, range: range }, dataType: 'json',
success: function(response) { success: function(response) {
chartsContainer.empty(); chartsContainer.empty();
if (response.success && response.data.labels.length > 0) { 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>'); 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 tempWrapper = $('#temp-chart-wrapper');
const humidityWrapper = $('#humidity-chart-wrapper'); const humidityWrapper = $('#humidity-chart-wrapper');
if (response.data.temperature.some(val => val !== null)) { if (response.data.temperature.some(val => val !== null)) {
tempWrapper.append('<h5>Temperaturverlauf</h5>'); tempWrapper.append('<h5>Temperaturverlauf</h5>');
let tempCanvas = $('<canvas>').attr('height', '300'); let tempCanvas = $('<canvas>').attr('height', '300');
tempWrapper.append($('<div>').addClass('cazubu-table-frameless p-2').append(tempCanvas)); 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' } } } } }); if(tempChart) { tempChart.destroy(); }
tempChart = 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, pointRadius: 3, pointHoverRadius: 5, borderWidth: 2 }] }, options: { maintainAspectRatio: false, scales: { x: { ticks: { color: '#212529', font: { size: 11 } } }, y: { ticks: { color: '#212529', font: { size: 11 } } } }, plugins: { legend: { labels: { color: '#212529', font: { size: 12 } } } } } });
} else { tempWrapper.append('<div class="cazubu-table-frameless p-3 text-center">Keine Temperaturdaten vorhanden.</div>'); } } else { tempWrapper.append('<div class="cazubu-table-frameless p-3 text-center">Keine Temperaturdaten vorhanden.</div>'); }
if (response.data.humidity.some(val => val !== null)) { if (response.data.humidity.some(val => val !== null)) {
humidityWrapper.append('<h5>Feuchtigkeitsverlauf</h5>'); humidityWrapper.append('<h5>Feuchtigkeitsverlauf</h5>');
let humidityCanvas = $('<canvas>').attr('height', '300'); let humidityCanvas = $('<canvas>').attr('height', '300');
humidityWrapper.append($('<div>').addClass('cazubu-table-frameless p-2').append(humidityCanvas)); 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' } } } } }); if(humidityChart) { humidityChart.destroy(); }
humidityChart = 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, pointRadius: 3, pointHoverRadius: 5, borderWidth: 2 }] }, options: { maintainAspectRatio: false, scales: { x: { ticks: { color: '#212529', font: { size: 11 } } }, y: { ticks: { color: '#212529', font: { size: 11 } } } }, plugins: { legend: { labels: { color: '#212529', font: { size: 12 } } } } } });
} else { humidityWrapper.append('<div class="cazubu-table-frameless p-3 text-center">Keine Feuchtigkeitsdaten vorhanden.</div>'); } } 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>'); } } else { chartsContainer.html('<div class="col-12"><div class="cazubu-table-frameless p-3 text-center">Keine Sensordaten für diesen Zeitraum.</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>'); } 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>'); }
}); });
}
$('#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');
loadSensorData(plantId, '24h');
});
$(document).on('click', '.sensor-range-btn', function() {
$('.sensor-range-btn').removeClass('active');
$(this).addClass('active');
const range = $(this).data('range');
const plantId = new URLSearchParams(window.location.search).get('id');
loadSensorData(plantId, range);
}); });
}); });

View File

@@ -65,7 +65,14 @@ require_once 'includes/header.php';
<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" 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 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-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 class="tab-pane fade" id="tab-sensors">
<div class="d-flex justify-content-center gap-2 mb-4">
<button class="btn btn-sm btn-outline-dark sensor-range-btn active" data-range="24h">24 Stunden</button>
<button class="btn btn-sm btn-outline-dark sensor-range-btn" data-range="7d">7 Tage</button>
<button class="btn btn-sm btn-outline-dark sensor-range-btn" data-range="30d">30 Tage</button>
</div>
<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> </div></td></tr></tbody>
</table> </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="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>

View File

@@ -31,9 +31,9 @@ $sql_seeds = "SELECT id, strain_name, internal_name, stock_count FROM seeds WHER
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(); } 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'; require_once 'includes/header.php';
?> ?>
<div class="card header-card glass-effect mb-4"> <div class="card header-card mb-4">
<div class="card-body d-flex justify-content-between align-items-center"> <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> <div><h1 class="mb-0">Pflanzen-Übersicht</h1><p class="card-text 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> <button class="btn btn-cazubu" data-bs-toggle="modal" data-bs-target="#plantModal">+ Neue Pflanze anlegen</button>
</div> </div>
</div> </div>
@@ -43,7 +43,7 @@ require_once 'includes/header.php';
<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> <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> </ul>
<table id="plants-table" class="table table-hover cazubu-table-frameless"> <table id="plants-table" class="table table-hover cazubu-table-frameless">
<thead class="table-dark"> <thead>
<tr> <tr>
<th style="width: 8%;" class="no-sort">Foto</th> <th style="width: 8%;" class="no-sort">Foto</th>
<th style="width: 25%;">Sorte / Interne Bez.</th> <th style="width: 25%;">Sorte / Interne Bez.</th>
@@ -56,9 +56,6 @@ require_once 'includes/header.php';
</tr> </tr>
</thead> </thead>
<tbody> <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): ?> <?php foreach ($plants as $plant): ?>
<tr> <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><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>
@@ -68,7 +65,7 @@ require_once 'includes/header.php';
<td class="align-middle"><?php echo (new DateTime())->diff(new DateTime($plant['plant_date']))->days; ?> Tage</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_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"><?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> <td class="align-middle text-center"><a href="plant_detail.php?id=<?php echo $plant['id']; ?>" class="btn btn-sm btn-outline-dark"><i class="bi bi-search"></i> Details</a></td>
</tr> </tr>
<?php endforeach; ?> <?php endforeach; ?>
</tbody> </tbody>

View File

@@ -17,48 +17,62 @@ require_once 'includes/header.php';
<table id="seeds-table" class="table table-hover cazubu-table-frameless"> <table id="seeds-table" class="table table-hover cazubu-table-frameless">
<thead class="table-dark"> <thead class="table-dark">
<tr> <tr>
<th>Sortenname</th> <th>Sorte</th>
<th>Interne Bez.</th> <th>Interne Bez.</th>
<th style="width: 25%;">Genetik</th> <th>Genetik</th>
<th>Typ</th>
<th>Anzahl</th> <th>Anzahl</th>
<th style="width: 120px;" class="no-sort">Aktionen</th> <th>Info</th>
<th style="width: 120px;" class="no-sort text-center">Aktionen</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<?php if (empty($seeds)): ?><tr><td colspan="6" class="text-center p-4"><i>Keine Samen angelegt.</i></td></tr><?php endif; ?> <?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): ?> <?php foreach ($seeds as $seed): ?>
<tr> <tr>
<td class="align-middle"> <td>
<span class="fw-bold"><?php echo htmlspecialchars($seed['strain_name']); ?></span> <strong><?php echo htmlspecialchars($seed['strain_name']); ?></strong>
<?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>
<td class="align-middle"><?php echo htmlspecialchars($seed['internal_name']); ?></td> <td><?php echo htmlspecialchars($seed['internal_name']); ?></td>
<td class="align-middle"> <td>
<div class="d-flex justify-content-between" style="font-size: 0.8em; color: #6c757d;"> <?php
<span>Sativa</span> $sativa = intval($seed['ratio_sativa'] ?? 50);
<span>Indica</span> $indica = 100 - $sativa;
?>
<div class="progress" style="height: 6px; min-width: 80px;" title="<?php echo $sativa; ?>% Sativa / <?php echo $indica; ?>% Indica">
<div class="progress-bar bg-success" role="progressbar" style="width: <?php echo $sativa; ?>%"></div>
<div class="progress-bar bg-warning" role="progressbar" style="width: <?php echo $indica; ?>%"></div>
</div> </div>
<div class="progress" role="progressbar" aria-label="Sativa/Indica ratio" style="height: 18px; font-size: .75rem;"> <small class="text-muted" style="font-size: 0.75em;"><?php echo $sativa; ?>% S / <?php echo $indica; ?>% I</small>
<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>
<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> <span class="badge <?php echo $seed['stock_count'] > 0 ? 'text-bg-success' : 'text-bg-danger'; ?>">
<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> <?php echo $seed['stock_count']; ?> Stk.
</span>
</td>
<td>
<?php if(!empty($seed['info_url'])): ?>
<a href="<?php echo htmlspecialchars($seed['info_url']); ?>" target="_blank" class="btn btn-sm btn-outline-dark" title="Link zum Breeder"><i class="bi bi-link-45deg"></i></a>
<?php else: ?>
<span class="text-muted">-</span>
<?php endif; ?>
</td>
<td class="text-center">
<button class="btn btn-sm btn-outline-secondary edit-seed-btn"
data-bs-toggle="modal" data-bs-target="#seedModal"
data-seed='<?php echo json_encode($seed, JSON_HEX_APOS | JSON_HEX_QUOT); ?>'>
<i class="bi bi-pencil-fill"></i>
</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">
<i class="bi bi-trash-fill"></i>
</button>
</td> </td>
</tr> </tr>
<?php endforeach; ?> <?php endforeach; ?> </tbody>
</tbody> </table>
</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-cazubu" form="seed-form">Speichern</button></div></div></div></div>
<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>
<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'; ?>
<?php require_once 'includes/footer.php'; ?>

View File

@@ -32,44 +32,50 @@ if ($stmt = $mysqli->prepare($sql)) {
require_once 'includes/header.php'; require_once 'includes/header.php';
?> ?>
<div class="card header-card glass-effect mb-4"> <div class="card header-card mb-4">
<div class="card-body"> <div class="card-body">
<h1 class="mb-0">Sensor-Übersicht</h1> <h1 class="mb-0">Sensor-Übersicht</h1>
<p class="card-text text-white-50 mt-2">Live-Daten aller deiner aktiven Pflanzen.</p> <p class="card-text mt-2">Live-Daten aller deiner aktiven Pflanzen.</p>
</div> </div>
</div> </div>
<div class="d-flex justify-content-center gap-2 mb-4">
<button class="btn btn-sm btn-outline-dark sensor-range-btn active" data-range="24h">24 Stunden</button>
<button class="btn btn-sm btn-outline-dark sensor-range-btn" data-range="7d">7 Tage</button>
<button class="btn btn-sm btn-outline-dark sensor-range-btn" data-range="30d">30 Tage</button>
</div>
<div class="row"> <div class="row">
<?php if (empty($plants_with_sensors)): ?> <?php if (empty($plants_with_sensors)): ?>
<div class="col-12"> <div class="col-12">
<div class="cazubu-table-frameless glass-effect p-4 text-center"> <div class="alert alert-light text-center border shadow-sm">
<h5 class="text-white">Keine Sensordaten verfügbar</h5> <h5>Keine Sensordaten verfügbar</h5>
<p class="text-white-50">Für deine aktiven Pflanzen wurden noch keine Sensordaten über die API gesendet.</p> <p class="text-muted">Für deine aktiven Pflanzen wurden noch keine Sensordaten über die API gesendet.</p>
</div> </div>
</div> </div>
<?php else: ?> <?php else: ?>
<?php foreach ($plants_with_sensors as $plant): ?> <?php foreach ($plants_with_sensors as $plant): ?>
<div class="col-lg-6 mb-4"> <div class="col-lg-6 mb-4">
<div class="cazubu-table-frameless glass-effect"> <div class="card h-100 border-0 shadow-sm" style="background-color: rgba(255, 255, 255, 0.9); backdrop-filter: blur(5px);">
<table class="table mb-0"> <div class="card-header bg-dark text-white py-3">
<thead class="table-dark"> <h5 class="mb-0 fs-6 text-white fw-bold text-uppercase ls-1">
<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> <a href="plant_detail.php?id=<?php echo $plant['id']; ?>" class="text-white text-decoration-none">
</thead> <?php echo htmlspecialchars($plant['container_name']); ?>
<tbody> <span class="text-white-50 fw-normal mx-1">|</span>
<tr> <?php echo htmlspecialchars($plant['strain_name']); ?>
<td class="p-3"> </a>
</h5>
</div>
<div class="card-body">
<div class="row sensor-chart-group" data-plant-id="<?php echo $plant['id']; ?>"> <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="col-md-6 mb-3 mb-md-0" 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 class="text-center p-4"><div class="spinner-border text-secondary spinner-border-sm" role="status"></div></div>
</div> </div>
<div class="col-md-6" id="humidity-chart-wrapper-<?php echo $plant['id']; ?>"> <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 class="text-center p-4"><div class="spinner-border text-secondary spinner-border-sm" role="status"></div></div>
</div>
</div> </div>
</div> </div>
</td>
</tr>
</tbody>
</table>
</div> </div>
</div> </div>
<?php endforeach; ?> <?php endforeach; ?>
@@ -78,62 +84,83 @@ require_once 'includes/header.php';
<?php require_once 'includes/footer.php'; ?> <?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> <script>
$(document).ready(function() { $(document).ready(function() {
// Store chart instances to destroy them before re-creating
const charts = {};
function loadAllCharts(range) {
$('.sensor-chart-group').each(function() { $('.sensor-chart-group').each(function() {
const plantId = $(this).data('plant-id'); const plantId = $(this).data('plant-id');
const tempWrapper = $(this).find('#temp-chart-wrapper-' + plantId); const tempWrapper = $(this).find('#temp-chart-wrapper-' + plantId);
const humidityWrapper = $(this).find('#humidity-chart-wrapper-' + plantId); const humidityWrapper = $(this).find('#humidity-chart-wrapper-' + plantId);
// Show spinners only if empty (optional UX choice, here we force reload)
// tempWrapper.html('<div class="text-center p-4"><div class="spinner-border text-secondary spinner-border-sm"></div></div>');
// humidityWrapper.html('<div class="text-center p-4"><div class="spinner-border text-secondary spinner-border-sm"></div></div>');
$.ajax({ $.ajax({
type: 'GET', type: 'GET',
url: 'ajax_handler.php', url: 'ajax_handler.php',
data: { action: 'get_sensor_data', plant_id: plantId }, data: { action: 'get_sensor_data', plant_id: plantId, range: range },
dataType: 'json', dataType: 'json',
success: function(response) { success: function(response) {
tempWrapper.empty(); tempWrapper.empty();
humidityWrapper.empty(); humidityWrapper.empty();
if (response.success && response.data.labels.length > 0) { if (response.success && response.data.labels.length > 0) {
// Temperatur-Chart // Temp Chart
if (response.data.temperature.some(v => v !== null)) { if (response.data.temperature.some(v => v !== null)) {
tempWrapper.append('<h6 class="text-dark text-center small">Temperatur</h6>'); tempWrapper.append('<h6 class="text-center fw-bold text-dark mb-2">Temperatur</h6>');
let tempCanvas = $('<canvas>'); let canvasId = 'temp-canvas-' + plantId;
tempWrapper.append($('<div>').addClass('chart-wrapper').append(tempCanvas)); tempWrapper.append($('<div style="height: 180px;">').append($('<canvas id="' + canvasId + '">')));
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' } } } } });
if(charts[canvasId]) charts[canvasId].destroy();
charts[canvasId] = new Chart(document.getElementById(canvasId), {
type: 'line',
data: { labels: response.data.labels, datasets: [{ label: '°C', data: response.data.temperature, borderColor: '#ff6b6b', backgroundColor: 'rgba(255, 107, 107, 0.1)', fill: true, tension: 0.3, pointRadius: 3, pointHoverRadius: 5 }] },
options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { x: { display: false }, y: { ticks: { color: '#212529', font: { size: 12 } }, grid: { color: 'rgba(0,0,0,0.05)' } } } }
});
} else { } 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>'); tempWrapper.html('<div class="d-flex align-items-center justify-content-center h-100 text-muted small">Keine Temperaturdaten</div>');
} }
// Feuchtigkeits-Chart
// Humidity Chart
if (response.data.humidity.some(v => v !== null)) { if (response.data.humidity.some(v => v !== null)) {
humidityWrapper.append('<h6 class="text-dark text-center small">Feuchtigkeit</h6>'); humidityWrapper.append('<h6 class="text-center fw-bold text-dark mb-2">Feuchtigkeit</h6>');
let humidityCanvas = $('<canvas>'); let canvasId = 'humidity-canvas-' + plantId;
humidityWrapper.append($('<div>').addClass('chart-wrapper').append(humidityCanvas)); humidityWrapper.append($('<div style="height: 180px;">').append($('<canvas id="' + canvasId + '">')));
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' } } } } });
if(charts[canvasId]) charts[canvasId].destroy();
charts[canvasId] = new Chart(document.getElementById(canvasId), {
type: 'line',
data: { labels: response.data.labels, datasets: [{ label: '%', data: response.data.humidity, borderColor: '#4dabf7', backgroundColor: 'rgba(77, 171, 247, 0.1)', fill: true, tension: 0.3, pointRadius: 3, pointHoverRadius: 5 }] },
options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { x: { display: false }, y: { ticks: { color: '#212529', font: { size: 12 } }, grid: { color: 'rgba(0,0,0,0.05)' } } } }
});
} else { } 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>'); humidityWrapper.html('<div class="d-flex align-items-center justify-content-center h-100 text-muted small">Keine Feuchtigkeitsdaten</div>');
} }
} else { } else {
const noDataHtml = '<div class="col-12 text-center"><p class="text-white-50">Keine Sensordaten.</p></div>'; const noDataHtml = '<div class="col-12 text-center py-4 text-muted small">Keine Sensordaten für diesen Zeitraum.</div>';
chartsContainer.html(noDataHtml); tempWrapper.parent().html(noDataHtml);
} }
}, },
error: function() { error: function() {
const errorHtml = '<div class="col-12 text-center"><p class="text-danger">Fehler beim Laden.</p></div>'; const errorHtml = '<div class="col-12 text-center py-4 text-danger small">Fehler beim Laden.</div>';
chartsContainer.html(errorHtml); tempWrapper.parent().html(errorHtml);
} }
}); });
}); });
}
// Initial load
loadAllCharts('24h');
// Filter click handler
$('.sensor-range-btn').click(function() {
$('.sensor-range-btn').removeClass('active');
$(this).addClass('active');
loadAllCharts($(this).data('range'));
});
}); });
</script> </script>