Feature Complete: Modern Glass UI, Sensor History, Seed Genetics & Cleanup
All checks were successful
Docker Build & Push / build-and-push (push) Successful in 28s
All checks were successful
Docker Build & Push / build-and-push (push) Successful in 28s
This commit is contained in:
@@ -314,13 +314,33 @@ try {
|
||||
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'];
|
||||
$range = $_GET['range'] ?? '24h';
|
||||
|
||||
$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";
|
||||
|
||||
// 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->bind_param("i", $plant_id);
|
||||
$stmt->execute();
|
||||
@@ -328,17 +348,28 @@ try {
|
||||
$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']; }
|
||||
// Formatierung des Timestamps für Chart (kürzer bei langen Zeiträumen)
|
||||
$ts = $row['timestamp'];
|
||||
$date_key = $ts; // Als Key nutzen wir den Timestamp direkt zum Sortieren
|
||||
|
||||
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);
|
||||
|
||||
$chart_data = ['labels' => [], 'temperature' => [], 'humidity' => []];
|
||||
foreach ($data_by_timestamp as $timestamp => $values) {
|
||||
$chart_data['labels'][] = date('d.m H:i', strtotime($timestamp));
|
||||
foreach ($data_by_timestamp as $ts => $values) {
|
||||
// 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['humidity'][] = $values['feuchtigkeit'];
|
||||
}
|
||||
|
||||
@@ -1,20 +1,110 @@
|
||||
/*
|
||||
* CAZUBU Custom Stylesheet
|
||||
* Version 9.2 - Finaler Feinschliff
|
||||
* Version 9.2 - Finaler Feinschliff (Refactored for Clean HTML)
|
||||
*/
|
||||
|
||||
/* Globale Stile & Layout */
|
||||
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; }
|
||||
.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; }
|
||||
|
||||
/* Sidebar */
|
||||
.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; }
|
||||
.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); }
|
||||
|
||||
/* Main Content */
|
||||
.main-content { flex-grow: 1; padding: 30px 10%; color: #fff; }
|
||||
|
||||
/* 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 */
|
||||
.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 */
|
||||
.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 thead th { background-color: #2f3640; color: white; border: none; text-align: left !important; vertical-align: middle; padding: 1rem; }
|
||||
/* 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.table-hover tbody tr:hover td { background-color: rgba(220, 223, 226, 0.95) !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_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;
|
||||
|
||||
/* Dashboard Summary Cards alignment fix */
|
||||
.col-lg-4.d-flex .cazubu-table-frameless {
|
||||
height: 100%;
|
||||
}
|
||||
.col-lg-4.d-flex .cazubu-table-frameless tbody,
|
||||
.col-lg-4.d-flex .cazubu-table-frameless tr,
|
||||
.col-lg-4.d-flex .cazubu-table-frameless td {
|
||||
height: 100%;
|
||||
}
|
||||
@@ -9,6 +9,13 @@ define('APP_VERSION', '18.3.0');
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<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 rel="stylesheet" href="css/style.css?v=<?php echo APP_VERSION; ?>">
|
||||
</head>
|
||||
@@ -18,27 +25,33 @@ define('APP_VERSION', '18.3.0');
|
||||
</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">
|
||||
<div class="text-center mb-4">
|
||||
<a href="index.php"><img src="logo.png" alt="Cazubu Logo" class="site-logo"></a>
|
||||
<h2 class="project-name">Cazubu</h2>
|
||||
</div>
|
||||
|
||||
<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): ?>
|
||||
<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>
|
||||
<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 <?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="nav-link-header">Aktionen</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><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 class="nav-link-header">Verwaltung</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="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: ?>
|
||||
<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>
|
||||
<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 <?php echo ($current_page == 'register.php') ? 'active' : ''; ?>">Registrieren</a></li>
|
||||
<?php endif; ?>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -47,7 +47,7 @@ require_once 'includes/header.php';
|
||||
<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">
|
||||
<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>
|
||||
<?php if ($stats['zones'] > 0): ?>
|
||||
<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>
|
||||
<tbody>
|
||||
<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>
|
||||
<?php if ($stats['seeds'] > 0): ?>
|
||||
<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>
|
||||
<tbody>
|
||||
<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>
|
||||
<?php if ($stats['plants'] > 0): ?>
|
||||
<div class="my-auto">
|
||||
|
||||
@@ -47,8 +47,8 @@ require_once 'includes/header.php';
|
||||
<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>
|
||||
<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"><i class="bi bi-trash-fill"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
|
||||
@@ -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>'); } } }); });
|
||||
$(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-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');
|
||||
let tempChart = null;
|
||||
let humidityChart = null;
|
||||
|
||||
function loadSensorData(plantId, range) {
|
||||
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',
|
||||
type: 'GET', url: 'ajax_handler.php', data: { action: 'get_sensor_data', plant_id: plantId, range: range }, 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' } } } } });
|
||||
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>'); }
|
||||
|
||||
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' } } } } });
|
||||
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 { 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>'); }
|
||||
});
|
||||
}
|
||||
|
||||
$('#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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 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 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>
|
||||
</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>
|
||||
|
||||
@@ -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(); }
|
||||
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><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>
|
||||
</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>
|
||||
</ul>
|
||||
<table id="plants-table" class="table table-hover cazubu-table-frameless">
|
||||
<thead class="table-dark">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 8%;" class="no-sort">Foto</th>
|
||||
<th style="width: 25%;">Sorte / Interne Bez.</th>
|
||||
@@ -56,9 +56,6 @@ require_once 'includes/header.php';
|
||||
</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>
|
||||
@@ -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 $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>
|
||||
<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>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
|
||||
@@ -17,48 +17,62 @@ require_once 'includes/header.php';
|
||||
<table id="seeds-table" class="table table-hover cazubu-table-frameless">
|
||||
<thead class="table-dark">
|
||||
<tr>
|
||||
<th>Sortenname</th>
|
||||
<th>Sorte</th>
|
||||
<th>Interne Bez.</th>
|
||||
<th style="width: 25%;">Genetik</th>
|
||||
<th>Typ</th>
|
||||
<th>Genetik</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>
|
||||
</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'; ?>
|
||||
<?php foreach ($seeds as $seed): ?>
|
||||
<tr>
|
||||
<td>
|
||||
<strong><?php echo htmlspecialchars($seed['strain_name']); ?></strong>
|
||||
</td>
|
||||
<td><?php echo htmlspecialchars($seed['internal_name']); ?></td>
|
||||
<td>
|
||||
<?php
|
||||
$sativa = intval($seed['ratio_sativa'] ?? 50);
|
||||
$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>
|
||||
<small class="text-muted" style="font-size: 0.75em;"><?php echo $sativa; ?>% S / <?php echo $indica; ?>% I</small>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge <?php echo $seed['stock_count'] > 0 ? 'text-bg-success' : 'text-bg-danger'; ?>">
|
||||
<?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>
|
||||
</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-cazubu" 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'; ?>
|
||||
|
||||
175
src/sensors.php
175
src/sensors.php
@@ -32,44 +32,50 @@ if ($stmt = $mysqli->prepare($sql)) {
|
||||
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">
|
||||
<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 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">
|
||||
<?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 class="alert alert-light text-center border shadow-sm">
|
||||
<h5>Keine Sensordaten verfügbar</h5>
|
||||
<p class="text-muted">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 class="card h-100 border-0 shadow-sm" style="background-color: rgba(255, 255, 255, 0.9); backdrop-filter: blur(5px);">
|
||||
<div class="card-header bg-dark text-white py-3">
|
||||
<h5 class="mb-0 fs-6 text-white fw-bold text-uppercase ls-1">
|
||||
<a href="plant_detail.php?id=<?php echo $plant['id']; ?>" class="text-white text-decoration-none">
|
||||
<?php echo htmlspecialchars($plant['container_name']); ?>
|
||||
<span class="text-white-50 fw-normal mx-1">|</span>
|
||||
<?php echo htmlspecialchars($plant['strain_name']); ?>
|
||||
</a>
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row sensor-chart-group" data-plant-id="<?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-secondary 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-secondary spinner-border-sm" role="status"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
@@ -78,62 +84,83 @@ require_once 'includes/header.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>
|
||||
$(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);
|
||||
// Store chart instances to destroy them before re-creating
|
||||
const charts = {};
|
||||
|
||||
$.ajax({
|
||||
type: 'GET',
|
||||
url: 'ajax_handler.php',
|
||||
data: { action: 'get_sensor_data', plant_id: plantId },
|
||||
dataType: 'json',
|
||||
success: function(response) {
|
||||
tempWrapper.empty();
|
||||
humidityWrapper.empty();
|
||||
function loadAllCharts(range) {
|
||||
$('.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);
|
||||
|
||||
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' } } } } });
|
||||
// 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({
|
||||
type: 'GET',
|
||||
url: 'ajax_handler.php',
|
||||
data: { action: 'get_sensor_data', plant_id: plantId, range: range },
|
||||
dataType: 'json',
|
||||
success: function(response) {
|
||||
tempWrapper.empty();
|
||||
humidityWrapper.empty();
|
||||
|
||||
if (response.success && response.data.labels.length > 0) {
|
||||
// Temp Chart
|
||||
if (response.data.temperature.some(v => v !== null)) {
|
||||
tempWrapper.append('<h6 class="text-center fw-bold text-dark mb-2">Temperatur</h6>');
|
||||
let canvasId = 'temp-canvas-' + plantId;
|
||||
tempWrapper.append($('<div style="height: 180px;">').append($('<canvas id="' + canvasId + '">')));
|
||||
|
||||
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 {
|
||||
tempWrapper.html('<div class="d-flex align-items-center justify-content-center h-100 text-muted small">Keine Temperaturdaten</div>');
|
||||
}
|
||||
|
||||
// Humidity Chart
|
||||
if (response.data.humidity.some(v => v !== null)) {
|
||||
humidityWrapper.append('<h6 class="text-center fw-bold text-dark mb-2">Feuchtigkeit</h6>');
|
||||
let canvasId = 'humidity-canvas-' + plantId;
|
||||
humidityWrapper.append($('<div style="height: 180px;">').append($('<canvas id="' + canvasId + '">')));
|
||||
|
||||
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 {
|
||||
humidityWrapper.html('<div class="d-flex align-items-center justify-content-center h-100 text-muted small">Keine Feuchtigkeitsdaten</div>');
|
||||
}
|
||||
} 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>');
|
||||
const noDataHtml = '<div class="col-12 text-center py-4 text-muted small">Keine Sensordaten für diesen Zeitraum.</div>';
|
||||
tempWrapper.parent().html(noDataHtml);
|
||||
}
|
||||
// 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 py-4 text-danger small">Fehler beim Laden.</div>';
|
||||
tempWrapper.parent().html(errorHtml);
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
const errorHtml = '<div class="col-12 text-center"><p class="text-danger">Fehler beim Laden.</p></div>';
|
||||
chartsContainer.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>
|
||||
Reference in New Issue
Block a user