Compare commits

..

21 Commits

Author SHA1 Message Date
Hickmeister
8410a50f26 Many Tings 2025-01-16 23:13:31 +00:00
Hickmeister
053662c2e8 Update Filament Dryer 2025-01-15 17:50:57 +00:00
Hickmeister
cc4e915cf5 scraper update 2025-01-14 23:24:23 +00:00
Hickmeister
030d72c202 Price tracking Changes 2025-01-14 22:49:09 +00:00
Hickmeister
a08c4eba3b I cba with this commit message tbh 2025-01-14 20:28:59 +00:00
Hickmeister
a1aee4134c added ESP32 Code 2025-01-13 21:42:35 +00:00
Hickmeister
9424f1857f Filament dryer page 2025-01-13 21:35:48 +00:00
Hickmeister
bcb7d7ea7d Check for admin 2025-01-05 13:24:10 +00:00
Hickmeister
e9908bd65b System 2025-01-05 13:23:55 +00:00
Hickmeister
8c9cb042ff Updated Nav 2025-01-05 03:53:51 +00:00
Hickmeister
d96fa63298 Added price tracking cards 2025-01-05 03:39:59 +00:00
Hickmeister
5a11305caf Track Filament 2025-01-05 02:28:51 +00:00
Hickmeister
e31bd43eaa lazy commit 2025-01-04 21:06:25 +00:00
Hickmeister
ca0007515f Added printers to DB form 2025-01-04 15:21:32 +00:00
Hickmeister
54007325c3 Updated File Structure 2025-01-04 14:57:44 +00:00
Hickmeister
b6da8a5ad5 adding printer form 2025-01-04 14:28:53 +00:00
Hickmeister
3216e90c4f renamed to .php 2025-01-04 14:28:31 +00:00
Hickmeister
0fa05b6514 added env 2025-01-04 14:28:17 +00:00
Hickmeister
a8676a432f created page template 2025-01-04 14:27:59 +00:00
Hickmeister
155b1ff293 Added GitIgnore 2025-01-04 14:27:42 +00:00
Hickmeister
9dd13bc802 Added Etsy Auth 2025-01-04 14:27:31 +00:00
54 changed files with 5943 additions and 351 deletions

14
.gitignore vendored Normal file
View File

@@ -0,0 +1,14 @@
# Ignore .env file to keep sensitive data out of version control
.env
# Ignore compiled PHP files
*.php~
# Ignore token files
etsyTokens.json
# Ignore system and IDE files
.DS_Store
node_modules/
vendor/
logs/

57
.htaccess Normal file
View File

@@ -0,0 +1,57 @@
RewriteEngine On
# Fix for nginx proxy to avoid internal server errors
RewriteBase /
# Redirect all traffic to the public folder, but allow existing files/directories
RewriteCond %{REQUEST_URI} !^/public/
RewriteCond %{DOCUMENT_ROOT}/public%{REQUEST_URI} -f [OR]
RewriteCond %{DOCUMENT_ROOT}/public%{REQUEST_URI} -d
RewriteRule ^(.*)$ /public/$1 [L,QSA]
# Handle cases where the file doesn't exist
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /public/index.php [L]
# Ensure directory listing is disabled
Options -Indexes
# Enable compression
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css text/javascript application/javascript application/json
</IfModule>
# Leverage browser caching
<IfModule mod_expires.c>
ExpiresActive On
ExpiresByType text/html "access plus 1 month"
ExpiresByType image/gif "access plus 1 year"
ExpiresByType image/jpeg "access plus 1 year"
ExpiresByType image/png "access plus 1 year"
ExpiresByType text/css "access plus 1 month"
ExpiresByType text/javascript "access plus 1 month"
ExpiresByType application/javascript "access plus 1 month"
ExpiresByType application/x-shockwave-flash "access plus 1 month"
ExpiresByType application/pdf "access plus 1 month"
</IfModule>
# Basic security headers
<IfModule mod_headers.c>
Header set X-Content-Type-Options "nosniff"
Header set X-Frame-Options "SAMEORIGIN"
Header set X-XSS-Protection "1; mode=block"
</IfModule>
# Handle 404 errors
ErrorDocument 404 /public/404.html
# Handle PHP execution if needed
<FilesMatch "\.php$">
SetHandler application/x-httpd-php
</FilesMatch>
# Deny access to sensitive files
<FilesMatch "^\.(htaccess|htpasswd|env|ini|log|sh|sql|bak|config)$">
Require all denied
</FilesMatch>

View File

@@ -3780,7 +3780,7 @@ form select.error:focus {
.btn-inverse-success {
color: #15ca20;
color: #1aad24;
background-color: rgba(21, 202, 32, 0.18);
@@ -3792,7 +3792,7 @@ form select.error:focus {
.btn-inverse-success:hover {
color: #15ca20;
color: #1aad24;
background-color: rgba(21, 202, 32, 0.18);
@@ -3802,7 +3802,7 @@ form select.error:focus {
.btn-inverse-success:focus {
color: #15ca20;
color: #1aad24;
background-color: rgba(21, 202, 32, 0.18);

View File

@@ -23,33 +23,33 @@ a {
}
.valid-feedback {
color: #15ca20
color: #23ac2c
}
.form-control.is-valid, .was-validated .form-control:valid {
border-color: #15ca20;
border-color: #23ac2c;
}
.form-control.is-valid:focus, .was-validated .form-control:valid:focus {
border-color: #15ca20;
border-color: #23ac2c;
}
.form-select.is-valid, .was-validated .form-select:valid {
border-color: #15ca20;
border-color: #23ac2c;
}
.form-select.is-valid:focus, .was-validated .form-select:valid:focus {
border-color: #15ca20;
border-color: #23ac2c;
}
.form-check-input.is-valid, .was-validated .form-check-input:valid {
border-color: #15ca20
border-color: #23ac2c
}
.form-check-input.is-valid:checked, .was-validated .form-check-input:valid:checked {
background-color: #15ca20
background-color: #23ac2c
}
.form-check-input.is-valid~.form-check-label, .was-validated .form-check-input:valid~.form-check-label {
color: #15ca20
color: #23ac2c
}
.invalid-feedback {
@@ -120,8 +120,8 @@ a {
.btn-success {
color: #fff;
background-color: #15ca20;
border-color: #15ca20
background-color: #2375ac;
border-color: #23ac2c
}
.btn-success:hover {
color: #fff;
@@ -207,27 +207,27 @@ a {
.btn-outline-success {
color: #15ca20;
border-color: #15ca20
color: #23ac2c;
border-color: #23ac2c
}
.btn-outline-success:hover {
color: #fff;
background-color: #15ca20;
border-color: #15ca20
background-color: #23ac2c;
border-color: #23ac2c
}
.btn-check:focus+.btn-outline-success, .btn-outline-success:focus {
box-shadow: 0 0 0 .25rem rgb(23 160 14 / 52%)
}
.btn-check:active+.btn-outline-success, .btn-check:checked+.btn-outline-success, .btn-outline-success.active, .btn-outline-success.dropdown-toggle.show, .btn-outline-success:active {
color: #fff;
background-color: #15ca20;
border-color: #15ca20
background-color: #23ac2c;
border-color: #23ac2c
}
.btn-check:active+.btn-outline-success:focus, .btn-check:checked+.btn-outline-success:focus, .btn-outline-success.active:focus, .btn-outline-success.dropdown-toggle.show:focus, .btn-outline-success:active:focus {
box-shadow: 0 0 0 .25rem rgb(23 160 14 / 52%)
}
.btn-outline-success.disabled, .btn-outline-success:disabled {
color: #15ca20;
color: #23ac2c;
background-color: transparent
}
@@ -304,7 +304,7 @@ a {
}
.border-success {
border-color: #15ca20!important
border-color: #23ac2c!important
}
.border-danger {
@@ -324,7 +324,7 @@ a {
color: #6c757d!important
}
.text-success {
color: #15ca20!important
color: #23ac2c!important
}
.text-info {
color: #0dcaf0!important
@@ -364,7 +364,7 @@ a {
}
.bg-success {
background-color: #15ca20 !important;
background-color: #23ac2c !important;
}
.bg-danger {
@@ -373,19 +373,19 @@ a {
.form-check-success .form-check-input:checked {
background-color: #15ca20;
border-color: #15ca20
background-color: #23ac2c;
border-color: #23ac2c
}
.form-check-success .form-check-input[type=checkbox]:indeterminate {
background-color: #15ca20;
border-color: #15ca20;
background-color: #23ac2c;
border-color: #23ac2c;
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e")
}
.form-check-success .form-check-input:focus {
border-color: #15ca20;
border-color: #23ac2c;
outline: 0;
box-shadow: 0 0 0 .25rem rgba(21, 202, 33, 0.25)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

View File

@@ -1,161 +0,0 @@
<!--sidebar wrapper -->
<div class="sidebar-wrapper" data-simplebar="true">
<div class="sidebar-header">
<div>
<img src="assets/images/logo-icon.png" class="logo-icon" alt="logo icon">
</div>
<div>
<h4 class="logo-text">TechOdyssey</h4>
</div>
<div class="mobile-toggle-icon ms-auto"><i class='bx bx-x'></i>
</div>
</div>
<!--navigation-->
<ul class="metismenu" id="menu">
<li>
<a href="javascript:;" class="has-arrow">
<div class="parent-icon"><i class='bx bx-home-alt'></i>
</div>
<div class="menu-title">Dashboard</div>
</a>
<ul>
<li> <a href="index.html"><i class='bx bx-radio-circle'></i>Infographic</a>
</li>
<li> <a href="index2.html"><i class='bx bx-radio-circle'></i>eCommerce</a>
</li>
<li> <a href="index3.html"><i class='bx bx-radio-circle'></i>Analytics</a>
</li>
</ul>
</li>
<li>
<a href="javascript:;" class="has-arrow">
<div class="parent-icon"><i class="bx bx-category"></i>
</div>
<div class="menu-title">Application</div>
</a>
<ul>
<li> <a href="app-emailbox.html"><i class='bx bx-radio-circle'></i>Email</a>
</li>
<li> <a href="app-chat-box.html"><i class='bx bx-radio-circle'></i>Chat Box</a>
</li>
<li> <a href="app-file-manager.html"><i class='bx bx-radio-circle'></i>File Manager</a>
</li>
<li> <a href="app-contact-list.html"><i class='bx bx-radio-circle'></i>Contatcs</a>
</li>
<li> <a href="app-to-do.html"><i class='bx bx-radio-circle'></i>Todo List</a>
</li>
<li> <a href="app-invoice.html"><i class='bx bx-radio-circle'></i>Invoice</a>
</li>
<li> <a href="app-fullcalender.html"><i class='bx bx-radio-circle'></i>Calendar</a>
</li>
</ul>
</li>
<li class="menu-label">UI Elements</li>
<li>
<a href="widgets.html">
<div class="parent-icon"><i class='bx bx-cookie'></i>
</div>
<div class="menu-title">Widgets</div>
</a>
</li>
<li>
<a href="javascript:;" class="has-arrow">
<div class="parent-icon"><i class='bx bx-cart'></i>
</div>
<div class="menu-title">eCommerce</div>
</a>
<ul>
<li> <a href="ecommerce-products.html"><i class='bx bx-radio-circle'></i>Products</a>
</li>
<li> <a href="ecommerce-products-details.html"><i class='bx bx-radio-circle'></i>Product Details</a>
</li>
<li> <a href="ecommerce-add-new-products.html"><i class='bx bx-radio-circle'></i>Add New Products</a>
</li>
<li> <a href="ecommerce-orders.html"><i class='bx bx-radio-circle'></i>Orders</a>
</li>
</ul>
</li>
<li>
<a class="has-arrow" href="javascript:;">
<div class="parent-icon"><i class='bx bx-bookmark-heart'></i>
</div>
<div class="menu-title">Components</div>
</a>
<ul>
<li> <a href="component-alerts.html"><i class='bx bx-radio-circle'></i>Alerts</a>
</li>
<li> <a href="component-accordions.html"><i class='bx bx-radio-circle'></i>Accordions</a>
</li>
<li> <a href="component-badges.html"><i class='bx bx-radio-circle'></i>Badges</a>
</li>
<li> <a href="component-buttons.html"><i class='bx bx-radio-circle'></i>Buttons</a>
</li>
<li> <a href="component-cards.html"><i class='bx bx-radio-circle'></i>Cards</a>
</li>
<li> <a href="component-carousels.html"><i class='bx bx-radio-circle'></i>Carousels</a>
</li>
<li> <a href="component-list-groups.html"><i class='bx bx-radio-circle'></i>List Groups</a>
</li>
<li> <a href="component-media-object.html"><i class='bx bx-radio-circle'></i>Media Objects</a>
</li>
<li> <a href="component-modals.html"><i class='bx bx-radio-circle'></i>Modals</a>
</li>
<li> <a href="component-navs-tabs.html"><i class='bx bx-radio-circle'></i>Navs & Tabs</a>
</li>
<li> <a href="component-navbar.html"><i class='bx bx-radio-circle'></i>Navbar</a>
</li>
<li> <a href="component-paginations.html"><i class='bx bx-radio-circle'></i>Pagination</a>
</li>
<li> <a href="component-popovers-tooltips.html"><i class='bx bx-radio-circle'></i>Popovers & Tooltips</a>
</li>
<li> <a href="component-progress-bars.html"><i class='bx bx-radio-circle'></i>Progress</a>
</li>
<li> <a href="component-spinners.html"><i class='bx bx-radio-circle'></i>Spinners</a>
</li>
<li> <a href="component-notifications.html"><i class='bx bx-radio-circle'></i>Notifications</a>
</li>
<li> <a href="component-avtars-chips.html"><i class='bx bx-radio-circle'></i>Avatrs & Chips</a>
</li>
</ul>
</li>
<li>
<a class="has-arrow" href="javascript:;">
<div class="parent-icon"><i class="bx bx-repeat"></i>
</div>
<div class="menu-title">Content</div>
</a>
<ul>
<li> <a href="content-grid-system.html"><i class='bx bx-radio-circle'></i>Grid System</a>
</li>
<li> <a href="content-typography.html"><i class='bx bx-radio-circle'></i>Typography</a>
</li>
<li> <a href="content-text-utilities.html"><i class='bx bx-radio-circle'></i>Text Utilities</a>
</li>
</ul>
</li>
<li>
<a class="has-arrow" href="javascript:;">
<div class="parent-icon"> <i class="bx bx-donate-blood"></i>
</div>
<div class="menu-title">Icons</div>
</a>
<ul>
<li> <a href="icons-line-icons.html"><i class='bx bx-radio-circle'></i>Line Icons</a>
</li>
<li> <a href="icons-boxicons.html"><i class='bx bx-radio-circle'></i>Boxicons</a>
</li>
<li> <a href="icons-feather-icons.html"><i class='bx bx-radio-circle'></i>Feather Icons</a>
</li>
</ul>
</li>
<li>
<a href="form-froala-editor.html">
<div class="parent-icon"><i class='bx bx-code-alt'></i>
</div>
<div class="menu-title">Froala Editor</div>
</a>
</li>
</ul>
<!--end navigation-->
</div>
<!--end sidebar wrapper -->

View File

@@ -1,7 +0,0 @@
<?php
session_start();
if (!isset($_SESSION['user_id'])) {
header("Location: /login.html");
exit();
}
?>

View File

@@ -72,11 +72,11 @@ $(function() {
label: 'Facebook',
data: [12, 30, 16, 23, 8, 14, 11],
backgroundColor: [
'#15ca20'
'#23ac2c'
],
tension: 0,
borderColor: [
'#15ca20'
'#23ac2c'
],
borderWidth: 3
}]
@@ -113,7 +113,7 @@ $(function() {
'#6f42c1',
'#d63384',
'#fd7e14',
'#15ca20',
'#23ac2c',
'#0dcaf0'
],
borderWidth: 1.5
@@ -146,7 +146,7 @@ $(function() {
'#6f42c1',
'#d63384',
'#fd7e14',
'#15ca20',
'#23ac2c',
'#0dcaf0'
],
borderWidth: 1
@@ -242,7 +242,7 @@ $(function() {
'#6f42c1',
'#d63384',
'#fd7e14',
'#15ca20',
'#23ac2c',
'#0dcaf0'
],
}]
@@ -372,7 +372,7 @@ var myChart = new Chart(ctx, {
label: 'Facebook',
data: [5, 30, 16, 23, 8, 14, 2],
backgroundColor: [
'#15ca20'
'#23ac2c'
],
fill: {
target: 'origin',
@@ -381,7 +381,7 @@ var myChart = new Chart(ctx, {
},
tension: 0.4,
borderColor: [
'#15ca20'
'#23ac2c'
],
borderWidth: 4
}]

View File

@@ -41,12 +41,12 @@ Morris.Area({
labels: ['iPhone', 'iPad'],
pointSize: 3,
fillOpacity: 0,
pointStrokeColors:['#008cff', '#15ca20'],
pointStrokeColors:['#008cff', '#23ac2c'],
behaveLikeLine: true,
gridLineColor: '#e0e0e0',
lineWidth: 3,
hideHover: 'auto',
lineColors: ['#008cff', '#15ca20'],
lineColors: ['#008cff', '#23ac2c'],
resize: true
});
@@ -67,7 +67,7 @@ Morris.Area({
value: 20
}],
resize: true,
colors:['#008cff', '#15ca20', '#fd3550']
colors:['#008cff', '#23ac2c', '#fd3550']
});
// Morris bar chart
@@ -112,7 +112,7 @@ Morris.Area({
xkey: 'y',
ykeys: ['a', 'b', 'c'],
labels: ['A', 'B', 'C'],
barColors:['#008cff', '#15ca20', '#75808a'],
barColors:['#008cff', '#23ac2c', '#75808a'],
hideHover: 'auto',
gridLineColor: '#eef0f2',
resize: true
@@ -153,7 +153,7 @@ Morris.Area({
],
lineColors: ['#008cff', '#15ca20'],
lineColors: ['#008cff', '#23ac2c'],
xkey: 'period',
ykeys: ['iphone', 'ipad'],
labels: ['Site A', 'Site B'],

View File

@@ -2130,17 +2130,17 @@ form select.error:focus {
}
.btn-inverse-success {
color: #15ca20;
color: #23ac2c;
background-color: rgba(21, 202, 32, 0.18);
border-color: rgb(212, 246, 214);
}
.btn-inverse-success:hover {
color: #15ca20;
color: #23ac2c;
background-color: rgba(21, 202, 32, 0.18);
border-color: rgba(21, 202, 32, 0.18);
}
.btn-inverse-success:focus {
color: #15ca20;
color: #23ac2c;
background-color: rgba(21, 202, 32, 0.18);
border-color: rgba(21, 202, 32, 0.18);
box-shadow: 0 0 0 0.25rem rgba(23, 160, 14, 0.32);

View File

@@ -2582,18 +2582,18 @@ form select.error:focus {
}
.btn-inverse-success {
color: #15ca20;
color: #23ac2c;
background-color: rgba(21, 202, 32, 0.18);
border-color: rgb(212, 246, 214);
&:hover {
color: #15ca20;
color: #23ac2c;
background-color: rgba(21, 202, 32, 0.18);
border-color: rgba(21, 202, 32, 0.18);
}
&:focus {
color: #15ca20;
color: #23ac2c;
background-color: rgba(21, 202, 32, 0.18);
border-color: rgba(21, 202, 32, 0.18);
box-shadow: 0 0 0 .25rem rgb(23 160 14 / 32%);

9
composer.json Normal file
View File

@@ -0,0 +1,9 @@
{
"require": {
"php-mqtt/client": "^2.2",
"fabpot/goutte": "^4.0",
"symfony/polyfill-ctype": "^1.31",
"phpmailer/phpmailer": "^6.9",
"twig/twig": "^3.18"
}
}

1532
composer.lock generated Normal file

File diff suppressed because it is too large Load Diff

226
esp32Code/dryer/main.ino Normal file
View File

@@ -0,0 +1,226 @@
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <Wire.h>
#include "Adafruit_AHTX0.h"
// WiFi Credentials
const char* ssid = "HickmanWiFi";
const char* password = "BlackBriar8787@@";
// Sensor and Server Instances
Adafruit_AHTX0 aht;
AsyncWebServer server(80);
// Pins for Relays
#define FAN_RELAY_PIN 26
#define HEATER_RELAY_PIN 25 // GPIO 25 for heater relay
// Dryer State Variables
bool dryerOn = false;
bool fanRelayState = false;
bool heaterRelayState = false;
float currentTemperature = 0.0;
float currentHumidity = 0.0;
float targetTemperature = 0.0;
int dryingTime = 0; // Total drying time in minutes
int remainingTime = 0; // Remaining drying time in minutes
unsigned long startTime = 0; // Time when the dryer was started (millis)
// Historical Data
const int maxHistory = 180;
std::vector<float> tempHistory;
std::vector<float> humidityHistory;
// Initialize Relays and Sensor
void setup() {
Serial.begin(115200);
// Connect to WiFi
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(1000);
Serial.println("Connecting to WiFi...");
}
Serial.println("Connected to WiFi");
// AHT10 Sensor Setup
if (!aht.begin()) {
Serial.println("Failed to find AHT10 sensor");
while (1) delay(10);
}
Serial.println("AHT10 Found");
// Setup Relay Pins
pinMode(FAN_RELAY_PIN, OUTPUT);
pinMode(HEATER_RELAY_PIN, OUTPUT);
digitalWrite(FAN_RELAY_PIN, LOW);
digitalWrite(HEATER_RELAY_PIN, LOW);
// Web Server Endpoints
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
request->send(200, "text/plain", "Filament Dryer Control");
});
server.on("/status", HTTP_GET, [](AsyncWebServerRequest *request) {
sensors_event_t humidity, temp;
aht.getEvent(&humidity, &temp);
currentTemperature = temp.temperature;
currentHumidity = humidity.relative_humidity;
// Update remaining drying time
if (dryerOn) {
unsigned long elapsedTime = (millis() - startTime) / 60000; // Convert to minutes
remainingTime = dryingTime > elapsedTime ? dryingTime - elapsedTime : 0;
// Automatically turn off dryer when time is up
if (remainingTime == 0) {
dryerOn = false;
heaterRelayState = false;
fanRelayState = false;
digitalWrite(HEATER_RELAY_PIN, LOW);
digitalWrite(FAN_RELAY_PIN, LOW);
Serial.println("Drying completed. Dryer turned off.");
}
} else {
remainingTime = 0;
}
// Store historical data
if (tempHistory.size() >= maxHistory) tempHistory.erase(tempHistory.begin());
if (humidityHistory.size() >= maxHistory) humidityHistory.erase(humidityHistory.begin());
tempHistory.push_back(currentTemperature);
humidityHistory.push_back(currentHumidity);
String response = "{";
response += "\"dryerOn\":" + String(dryerOn ? "true" : "false") + ",";
response += "\"fanRelayState\":" + String(fanRelayState ? "true" : "false") + ",";
response += "\"heaterRelayState\":" + String(heaterRelayState ? "true" : "false") + ",";
response += "\"temperature\":" + String(currentTemperature) + ",";
response += "\"humidity\":" + String(currentHumidity) + ",";
response += "\"targetTemperature\":" + String(targetTemperature) + ",";
response += "\"dryingTime\":" + String(dryingTime) + ","; // Total drying time
response += "\"remainingTime\":" + String(remainingTime) + ","; // Remaining time
// Serialize temperature and humidity history
response += "\"tempHistory\":[";
for (size_t i = 0; i < tempHistory.size(); i++) {
response += String(tempHistory[i]);
if (i < tempHistory.size() - 1) response += ",";
}
response += "],";
response += "\"humidityHistory\":[";
for (size_t i = 0; i < humidityHistory.size(); i++) {
response += String(humidityHistory[i]);
if (i < humidityHistory.size() - 1) response += ",";
}
response += "]";
response += "}";
request->send(200, "application/json", response);
});
server.on("/control", HTTP_POST, [](AsyncWebServerRequest *request) {
int params = request->params();
for (int i = 0; i < params; i++) {
AsyncWebParameter* p = request->getParam(i);
if (p->name() == "dryerOn") {
dryerOn = (p->value() == "true");
if (dryerOn) {
startTime = millis();
fanRelayState = true;
}
} else if (p->name() == "targetTemperature") {
targetTemperature = p->value().toFloat();
} else if (p->name() == "dryingTime") {
dryingTime = p->value().toInt();
}
}
request->send(200, "application/json", "{\"status\":\"success\"}");
});
// CORS Handling
DefaultHeaders::Instance().addHeader("Access-Control-Allow-Origin", "*");
DefaultHeaders::Instance().addHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
DefaultHeaders::Instance().addHeader("Access-Control-Allow-Headers", "*");
// Start Server
server.begin();
}
// Monitor and Update
void loop() {
sensors_event_t humidity, temp;
aht.getEvent(&humidity, &temp);
currentTemperature = temp.temperature;
currentHumidity = humidity.relative_humidity;
// Safety Controls
if (currentTemperature > 26.0) {
fanRelayState = true;
digitalWrite(FAN_RELAY_PIN, HIGH); // Force fan on
Serial.println("Safety: Fan forced ON due to high temperature.");
}
// Dryer Control Logic
if (dryerOn) {
if (currentTemperature < targetTemperature) {
heaterRelayState = true;
digitalWrite(HEATER_RELAY_PIN, HIGH);
} else {
heaterRelayState = false;
digitalWrite(HEATER_RELAY_PIN, LOW);
}
// Ensure the fan is always on when the heater is active
fanRelayState = true;
digitalWrite(FAN_RELAY_PIN, HIGH);
} else {
heaterRelayState = false;
digitalWrite(HEATER_RELAY_PIN, LOW);
if (currentTemperature <= 26.0) {
fanRelayState = false;
digitalWrite(FAN_RELAY_PIN, LOW);
}
}
// Manual Serial Command Handling
handleSerialCommands();
delay(5000); // Update sensor readings every 5 seconds
}
// Handle Serial Commands to Control Relays
void handleSerialCommands() {
if (Serial.available() > 0) {
String command = Serial.readStringUntil('\n');
command.trim();
if (command == "fan on") {
fanRelayState = true;
digitalWrite(FAN_RELAY_PIN, HIGH);
Serial.println("Fan relay activated.");
} else if (command == "fan off") {
fanRelayState = false;
digitalWrite(FAN_RELAY_PIN, LOW);
Serial.println("Fan relay deactivated.");
} else if (command == "heater on") {
heaterRelayState = true;
digitalWrite(HEATER_RELAY_PIN, HIGH);
fanRelayState = true;
digitalWrite(FAN_RELAY_PIN, HIGH);
Serial.println("Heater relay activated. Fan ON for safety.");
} else if (command == "heater off") {
heaterRelayState = false;
digitalWrite(HEATER_RELAY_PIN, LOW);
Serial.println("Heater relay deactivated.");
} else {
Serial.println("Unknown command. Use 'fan on', 'fan off', 'heater on', or 'heater off'.");
}
}
}

196
public/addFilament.php Normal file
View File

@@ -0,0 +1,196 @@
<?php include '../src/session_check.php';
checkUserRole(['admin']);
?>
<html lang="en" data-bs-theme="dark">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!--favicon-->
<link rel="icon" href="../assets/images/favicon-32x32.png" type="image/png">
<!--plugins-->
<link href="../assets/plugins/vectormap/jquery-jvectormap-2.0.2.css" rel="stylesheet">
<link href="../assets/plugins/simplebar/css/simplebar.css" rel="stylesheet">
<link href="../assets/plugins/perfect-scrollbar/css/perfect-scrollbar.css" rel="stylesheet">
<link href="../assets/plugins/metismenu/css/metisMenu.min.css" rel="stylesheet">
<!-- loader-->
<link href="../assets/css/pace.min.css" rel="stylesheet"/>
<script src="../assets/js/pace.min.js"></script>
<!-- Bootstrap CSS -->
<link href="../assets/css/bootstrap.min.css" rel="stylesheet">
<link href="../assets/css/bootstrap-extended.css" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500&display=swap" rel="stylesheet">
<link href="../assets/sass/app.css" rel="stylesheet">
<link href="../assets/css/icons.css" rel="stylesheet">
<link href='https://unpkg.com/boxicons@2.1.4/css/boxicons.min.css' rel='stylesheet'>
<!-- Theme Style CSS -->
<link rel="stylesheet" href="../assets/sass/dark-theme.css">
<link rel="stylesheet" href="../assets/sass/semi-dark.css">
<link rel="stylesheet" href="../assets/sass/bordered-theme.css">
<title>TOD Dashboard</title>
</head>
<body>
<!--wrapper-->
<div class="wrapper">
<!--sidebar wrapper -->
<?php include '../src/nav.php'; ?>
<!--end sidebar wrapper -->
<!--start header -->
<?php include '../src/header.php'; ?>
<!--end header -->
<!--start page wrapper -->
<div class="page-wrapper">
<div class="page-content">
<!--start page content -->
<!--breadcrumb-->
<div class="page-breadcrumb d-none d-sm-flex align-items-center mb-3">
<div class="breadcrumb-title pe-3">Filament Settings</div>
<div class="ps-3">
<nav aria-label="breadcrumb">
<ol class="breadcrumb mb-0 p-0">
<li class="breadcrumb-item"><a href="index.php"><i class="bx bx-home-alt"></i></a>
</li>
<li class="breadcrumb-item active" aria-current="page">Add Filament</li>
</ol>
</nav>
</div>
</div>
<!--end breadcrumb-->
<div class="row">
<div class="col-lg-8 mx-auto">
<div class="card">
<div class="card-header px-4 py-3">
<h5 class="mb-0">Add New Filament to Track</h5>
</div>
<div class="card-body p-4">
<!-- Alert Banner (Hidden Initially) -->
<div id="alertBanner" class="alert d-none" role="alert"></div>
<form id="filamentForm">
<div class="row mb-3">
<label for="productName" class="col-sm-3 col-form-label">Filament Name</label>
<div class="col-sm-9">
<input type="text" class="form-control" id="productName" name="productName" placeholder="Brand / Type of Filament" required>
</div>
</div>
<div class="row mb-3">
<label for="productUrl" class="col-sm-3 col-form-label">Amazon URL</label>
<div class="col-sm-9">
<input type="url" class="form-control" id="productUrl" name="productUrl" placeholder="https://www.amazon.co.uk/example" required>
</div>
</div>
<div class="row">
<label class="col-sm-3 col-form-label"></label>
<div class="col-sm-9">
<div class="d-md-flex d-grid align-items-center gap-3">
<button type="submit" class="btn btn-primary px-4">Track Filament</button>
<button type="reset" class="btn btn-light px-4">Reset</button>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- jQuery & AJAX Script -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script>
$(document).ready(function() {
$('#filamentForm').on('submit', function(event) {
event.preventDefault(); // Prevent traditional form submission
// Collect form data
let formData = $(this).serialize();
// AJAX request to submit form
$.ajax({
type: 'POST',
url: '../src/filamentTracker/addFilament.php',
data: formData,
dataType: 'json',
success: function(response) {
let banner = $('#alertBanner');
if (response.status === 'success') {
banner.removeClass('d-none alert-danger')
.addClass('alert-success')
.text(response.message);
} else {
banner.removeClass('d-none alert-success')
.addClass('alert-danger')
.text(response.message);
}
},
error: function() {
$('#alertBanner').removeClass('d-none alert-success')
.addClass('alert-danger')
.text('An error occurred while adding the filament.');
}
});
});
});
</script>
<!--end page content -->
</div>
</div>
<!--end page wrapper -->
<!--start overlay-->
<div class="overlay mobile-toggle-icon"></div>
<!--end overlay-->
<!--Start Back To Top Button-->
<a href="javaScript:;" class="back-to-top"><i class='bx bxs-up-arrow-alt'></i></a>
<!--End Back To Top Button-->
<footer class="page-footer">
<p class="mb-0">Copyright © 2024. All right reserved.</p>
</footer>
</div>
<!--end wrapper-->
<!-- search modal -->
<div class="modal" id="SearchModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable modal-fullscreen-md-down">
<div class="modal-content">
<div class="modal-header gap-2">
<div class="position-relative popup-search w-100">
<input class="form-control form-control-lg ps-5 border border-3 border-primary" type="search" placeholder="Search">
<span class="position-absolute top-50 search-show ms-3 translate-middle-y start-0 top-50 fs-4"><i class='bx bx-search'></i></span>
</div>
<button type="button" class="btn-close d-md-none" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
</div>
</div>
</div>
<!-- end search modal -->
<!-- Bootstrap JS -->
<script src="../assets/js/bootstrap.bundle.min.js"></script>
<!--plugins-->
<script src="../assets/js/jquery.min.js"></script>
<script src="../assets/plugins/simplebar/js/simplebar.min.js"></script>
<script src="../assets/plugins/metismenu/js/metisMenu.min.js"></script>
<script src="../assets/plugins/perfect-scrollbar/js/perfect-scrollbar.js"></script>
<script src="../assets/plugins/apexcharts-bundle/js/apexcharts.min.js"></script>
<!--app JS-->
<script src="../assets/js/app.js"></script>
<script src="../assets/js/index.js"></script>
<script src="../assets/plugins/peity/jquery.peity.min.js"></script>
<script>
$(".data-attributes span").peity("donut")
</script>
</body>
</html>

618
public/filamentDryer.php Normal file
View File

@@ -0,0 +1,618 @@
<?php include '../src/session_check.php';
checkUserRole(['admin']);
?>
<html lang="en" data-bs-theme="dark">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!--favicon-->
<link rel="icon" href="../assets/images/favicon-32x32.png" type="image/png">
<!--plugins-->
<link href="../assets/plugins/vectormap/jquery-jvectormap-2.0.2.css" rel="stylesheet">
<link href="../assets/plugins/simplebar/css/simplebar.css" rel="stylesheet">
<link href="../assets/plugins/perfect-scrollbar/css/perfect-scrollbar.css" rel="stylesheet">
<link href="../assets/plugins/metismenu/css/metisMenu.min.css" rel="stylesheet">
<!-- loader-->
<link href="../assets/css/pace.min.css" rel="stylesheet"/>
<script src="../assets/js/pace.min.js"></script>
<!-- Bootstrap CSS -->
<link href="../assets/css/bootstrap.min.css" rel="stylesheet">
<link href="../assets/css/bootstrap-extended.css" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500&display=swap" rel="stylesheet">
<link href="../assets/sass/app.css" rel="stylesheet">
<link href="../assets/css/icons.css" rel="stylesheet">
<link href='https://unpkg.com/boxicons@2.1.4/css/boxicons.min.css' rel='stylesheet'>
<!-- Theme Style CSS -->
<link rel="stylesheet" href="../assets/sass/dark-theme.css">
<link rel="stylesheet" href="../assets/sass/semi-dark.css">
<link rel="stylesheet" href="../assets/sass/bordered-theme.css">
<title>TOD Dashboard</title>
</head>
<body>
<!--wrapper-->
<div class="wrapper">
<!--sidebar wrapper -->
<?php include '../src/nav.php'; ?>
<!--end sidebar wrapper -->
<!--start header -->
<?php include '../src/header.php'; ?>
<!--end header -->
<!--start page wrapper -->
<div class="page-wrapper">
<div class="page-content">
<!-- Start Page Content -->
<!-- Start Drying Modal -->
<div class="modal fade" id="startDryingModal" tabindex="-1" aria-labelledby="startDryingModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="startDryingModalLabel">Start Drying</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="dryerControlForm">
<div class="mb-3">
<label for="filamentPreset" class="form-label">Filament Preset</label>
<select class="form-select" id="filamentPreset" name="filamentPreset" required>
<option value="">Select Filament Type</option>
<option value="PLA" data-temp="50">PLA</option>
<option value="ABS" data-temp="70">ABS</option>
<option value="PETG" data-temp="65">PETG</option>
<option value="Nylon" data-temp="80">Nylon</option>
<option value="TPU" data-temp="45">TPU</option>
<option value="Custom" data-temp="">Custom</option>
</select>
</div>
<div class="mb-3">
<label for="dryingTime" class="form-label">Drying Time (Hours)</label>
<input type="number" class="form-control" id="dryingTime" name="dryingTime" min="1" max="24" required>
</div>
<div class="mb-3">
<label for="temperature" class="form-label">Drying Temperature (°C)</label>
<input type="number" class="form-control" id="temperature" name="temperature" min="30" max="100" required>
</div>
<div class="d-flex justify-content-between">
<button type="submit" class="btn btn-success">Start Drying</button>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- Stop Drying Modal -->
<div class="modal fade" id="stopDryingModal" tabindex="-1" aria-labelledby="stopDryingModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="stopDryingModalLabel">Stop Drying</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p>Are you sure you want to stop the drying process?</p>
<div class="d-flex justify-content-between">
<button type="button" class="btn btn-danger" id="confirmStopDrying">Yes, Stop Drying</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
</div>
</div>
</div>
</div>
</div>
<!-- Page Content -->
<div class="container mt-4">
<div class="row">
<!-- Dryer Status Cards -->
<div class="col-lg-4">
<div class="card bg-danger radius-10 overflow-hidden">
<div class="card-body">
<div class="d-flex align-items-center">
<div>
<p class="mb-0">Dryer Status</p>
<h5 class="mb-0">STATUS</h5>
</div>
</div>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card bg-danger radius-10 overflow-hidden" id="heaterRelayStatus">
<div class="card-body">
<div class="d-flex align-items-center">
<div>
<p class="mb-0">Heater Status</p>
<h5 class="mb-0">STATUS</h5>
</div>
</div>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card bg-danger radius-10 overflow-hidden" id="fanRelayStatus">
<div class="card-body">
<div class="d-flex align-items-center">
<div>
<p class="mb-0">Fan Status</p>
<h5 class="mb-0">STATUS</h5>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<!-- Left Column - Charts -->
<div class="col-lg-7">
<div class="row">
<!-- Temperature Chart -->
<div class="col-lg-12 mb-0">
<div class="card radius-10 overflow-hidden">
<div class="card-body d-flex align-items-center justify-content-between">
<div>
<p class="mb-0 filament-name">Temp:</p>
<h5 class="mb-0" id="temperatureValue">--°C</h5>
</div>
</div>
<div id="tempChart"></div>
</div>
</div>
<!-- Humidity Chart -->
<div class="col-lg-12 mb-0">
<div class="card radius-10 overflow-hidden">
<div class="card-body d-flex align-items-center justify-content-between">
<div>
<p class="mb-0 filament-name">Humidity:</p>
<h5 class="mb-0" id="humidityValue">--%</h5>
</div>
</div>
<div id="humidityChart"></div>
</div>
</div>
</div>
</div>
<!-- Right Column - Dryer Control Card with Dropdown and Chart -->
<div class="col-lg-5">
<div class="card rounded-4 mb-0">
<div class="card-body">
<div class="d-flex align-items-start justify-content-between mb-0">
<div class="">
<h6 class="mb-0">Dryer Control</h6>
</div>
<div class="dropdown">
<a href="javascript:;" class="dropdown-toggle-nocaret more-options dropdown-toggle" data-bs-toggle="dropdown">
<i class='bx bx-dots-vertical-rounded'></i>
</a>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="javascript:;" data-bs-toggle="modal" data-bs-target="#startDryingModal">Start Drying</a></li>
<li><a class="dropdown-item" href="javascript:;" data-bs-toggle="modal" data-bs-target="#stopDryingModal">Stop Drying</a></li>
</ul>
</div>
</div>
<div class="chart-container2">
<div id="chartProgress"></div>
</div>
<div class="text-center">
<div class="">
<h4 id="dryingTimeDisplay" class="mb-1">Loadig..</h4>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- End Page Content -->
</div>
</div>
<!--end page wrapper -->
<!-- search modal -->
<div class="modal" id="SearchModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable modal-fullscreen-md-down">
<div class="modal-content">
<div class="modal-header gap-2">
<div class="position-relative popup-search w-100">
<input class="form-control form-control-lg ps-5 border border-3 border-primary" type="search" placeholder="Search">
<span class="position-absolute top-50 search-show ms-3 translate-middle-y start-0 top-50 fs-4"><i class='bx bx-search'></i></span>
</div>
<button type="button" class="btn-close d-md-none" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
</div>
</div>
</div>
<!-- end search modal -->
<!-- Bootstrap JS -->
<script src="../assets/js/bootstrap.bundle.min.js"></script>
<!--plugins-->
<script src="../assets/js/jquery.min.js"></script>
<script src="../assets/plugins/simplebar/js/simplebar.min.js"></script>
<script src="../assets/plugins/metismenu/js/metisMenu.min.js"></script>
<script src="../assets/plugins/perfect-scrollbar/js/perfect-scrollbar.js"></script>
<script src="../assets/plugins/apexcharts-bundle/js/apexcharts.min.js"></script>
<!--app JS-->
<script src="../assets/js/app.js"></script>
<script src="../assets/js/index.js"></script>
<script src="../assets/plugins/peity/jquery.peity.min.js"></script>
<script>
$(".data-attributes span").peity("donut")
</script>
<script>
document.addEventListener("DOMContentLoaded", function () {
const ESP32_IP = 'https://filamentdry.hickmeister.uk'; // Replace with your ESP32 IP
let tempChart, humidityChart;
let progressChart; // Declare a variable for the chart instance
const startDryingModal = new bootstrap.Modal(document.getElementById('startDryingModal'));
const stopDryingModal = new bootstrap.Modal(document.getElementById('stopDryingModal'));
// Fetch ESP32 Status Data
function fetchDryerStatus() {
fetch(`${ESP32_IP}/status`)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json();
})
.then(data => {
if (data.dryerOn !== undefined) {
updateTemperatureCard(data);
updateHumidityCard(data);
updateStatusCards(data);
updateChartProgress(data); // Update chart for drying progress
} else {
console.error("Dryer offline or no data available");
showOfflineStatus();
}
})
.catch(error => {
console.error("Failed to fetch dryer status:", error);
showOfflineStatus();
});
}
// Control Dryer
function controlDryer(dryerOn, targetTemperature, dryingTimeInMinutes) {
const formData = new URLSearchParams();
formData.append("dryerOn", dryerOn);
formData.append("targetTemperature", targetTemperature);
formData.append("dryingTime", dryingTimeInMinutes);
fetch(`${ESP32_IP}/control`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: formData.toString(),
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json();
})
.then(data => {
console.log('Dryer control updated:', data);
fetchDryerStatus(); // Refresh status after control update
if (dryerOn) {
startDryingModal.hide();
} else {
stopDryingModal.hide();
}
})
.catch(error => {
console.error('Failed to update dryer control:', error);
alert('Failed to update dryer control. Check your connection.');
});
}
// Handle Form Submission for Starting Drying
const dryerControlForm = document.getElementById("dryerControlForm");
dryerControlForm.addEventListener("submit", function (event) {
event.preventDefault();
// Collect form data
const formData = new FormData(dryerControlForm);
const dryerOn = true; // Always true when starting the dryer
const dryingTimeInHours = parseFloat(formData.get("dryingTime"));
const targetTemperature = parseFloat(formData.get("temperature"));
// Validate Input
if (isNaN(dryingTimeInHours) || isNaN(targetTemperature)) {
alert("Please complete all required fields.");
return;
}
// Convert drying time to minutes
const dryingTimeInMinutes = Math.round(dryingTimeInHours * 60);
// Control Dryer
controlDryer(dryerOn, targetTemperature, dryingTimeInMinutes);
});
// Handle Stop Dryer
document.getElementById("stopDryingModal").addEventListener("click", function () {
stopDryingModal.show();
});
document.getElementById("confirmStopDrying").addEventListener("click", function() {
controlDryer(false, 0, 0); // Turn off the dryer
});
// Auto-Fill Temperature Input Based on Filament Preset
const filamentPresetSelect = document.getElementById("filamentPreset");
const temperatureInput = document.getElementById("temperature");
filamentPresetSelect.addEventListener("change", function () {
const selectedOption = filamentPresetSelect.options[filamentPresetSelect.selectedIndex];
const presetTemperature = selectedOption.getAttribute("data-temp");
if (presetTemperature) {
temperatureInput.value = presetTemperature; // Auto-fill temperature input
} else {
temperatureInput.value = ""; // Clear the temperature input for "Custom"
}
});
// Update chart progress
function updateChartProgress(data) {
const chartProgressElement = document.getElementById("chartProgress");
const dryingTime = data.dryingTime || 0; // Total drying time in minutes
const remainingTime = data.remainingTime || 0; // Remaining time in minutes
// Convert remaining time and total drying time to h:mm format
const remainingTimeFormatted = convertToHoursAndMinutes(remainingTime);
const dryingTimeFormatted = convertToHoursAndMinutes(dryingTime);
const progressPercent = dryingTime > 0 ? ((dryingTime - remainingTime) / dryingTime) * 100 : 0; // Calculate the percentage of drying progress
// Initialize the chart if it doesn't exist yet
if (!progressChart) {
const options = {
series: [progressPercent],
chart: {
height: 385,
type: 'radialBar',
toolbar: {
show: false
}
},
plotOptions: {
radialBar: {
hollow: {
margin: 0,
size: '80%',
background: 'transparent',
},
track: {
background: 'rgba(0, 0, 0, 0.1)',
strokeWidth: '67%',
},
dataLabels: {
show: true,
name: {
offsetY: -10,
show: false,
},
value: {
offsetY: 10,
color: 'rgba(13, 160, 8, 0.82)',
fontSize: '20px',
show: true,
formatter: function () {
return remainingTimeFormatted; // Display remaining time in the center
}
}
}
}
},
fill: {
type: 'gradient',
gradient: {
shade: 'dark',
type: 'horizontal',
shadeIntensity: 02,
gradientToColors: ['#44FF00'],
opacityFrom: 1,
opacityTo: 1,
stops: [0, 100]
}
},
colors: ["#289600"],
stroke: {
lineCap: 'round'
},
labels: ['Drying Progress'],
};
progressChart = new ApexCharts(chartProgressElement, options);
progressChart.render();
} else {
progressChart.updateSeries([progressPercent]);
// Dynamically update the remaining time in the center
progressChart.updateOptions({
plotOptions: {
radialBar: {
dataLabels: {
value: {
formatter: function () {
return remainingTimeFormatted; // Dynamically update the remaining time
}
}
}
}
}
});
}
// Display drying time below the chart
const timeSetElement = document.getElementById('dryingTimeDisplay');
if (timeSetElement) {
timeSetElement.innerText = `Time Set: ${dryingTimeFormatted}`;
}
}
// Convert minutes to hours and minutes (e.g., 90 minutes -> 1h 30m)
function convertToHoursAndMinutes(minutes) {
const hours = Math.floor(minutes / 60);
const mins = minutes % 60;
return `${hours}h ${mins}m`;
}
// Update Temperature Card and Chart
function updateTemperatureCard(data) {
const temperature = data.temperature.toFixed(1); // Limit temperature to 1 decimal place
const tempHistory = data.tempHistory.map(temp => parseFloat(temp.toFixed(1))); // Limit history data
const timestamps = Array.from({ length: tempHistory.length }, (_, i) => `${i + 1}`);
document.getElementById('temperatureValue').innerText = `${temperature}°C`;
const options = {
series: [{ name: "Temperature", data: tempHistory }],
chart: {
type: 'area',
color: '#1df00a',
height: 150,
toolbar: { show: false },
sparkline: { enabled: true },
id: 'tempChart',
},
stroke: { curve: 'smooth' },
xaxis: { categories: timestamps, labels: { show: false } },
tooltip: {
theme: 'dark',
x: { show: false },
y: { formatter: value => `${value.toFixed(1)}°C` },
},
};
if (!tempChart) {
tempChart = new ApexCharts(document.querySelector("#tempChart"), options);
tempChart.render();
} else {
tempChart.updateSeries([{ data: tempHistory }]);
}
}
// Update Humidity Card and Chart
function updateHumidityCard(data) {
const humidity = data.humidity.toFixed(1);
const humidityHistory = data.humidityHistory || [];
const timestamps = Array.from({ length: humidityHistory.length }, (_, i) => `${i + 1}`);
document.getElementById('humidityValue').innerText = `${humidity}%`;
const options = {
series: [{ name: "Humidity", data: humidityHistory }],
chart: {
type: 'area',
height: 150,
toolbar: { show: false },
sparkline: { enabled: true },
id: 'humidityChart',
},
stroke: { curve: 'smooth' },
xaxis: { categories: timestamps, labels: { show: false } },
tooltip: {
theme: 'dark',
x: { show: false },
y: { formatter: value => `${value.toFixed(1)}%` },
},
};
if (!humidityChart) {
humidityChart = new ApexCharts(document.querySelector("#humidityChart"), options);
humidityChart.render();
} else {
humidityChart.updateSeries([{ data: humidityHistory }]);
}
}
// Update Status Cards (Dryer, Heater, Fan)
function updateStatusCards(data) {
const dryerStatusCard = document.querySelector('.card:nth-child(1)');
const heaterStatusCard = document.getElementById('heaterRelayStatus');
const fanStatusCard = document.getElementById('fanRelayStatus');
const dryerState = data.dryerOn ? "On" : "Off";
const fanRelayState = data.fanRelayState ? "On" : "Off";
const heaterRelayState = data.heaterRelayState ? "On" : "Off";
if (dryerStatusCard) {
dryerStatusCard.classList.toggle("bg-success", dryerState === "On");
dryerStatusCard.classList.toggle("bg-danger", dryerState === "Off");
dryerStatusCard.querySelector("h5").innerText = dryerState;
}
if (heaterStatusCard) {
heaterStatusCard.classList.toggle("bg-success", heaterRelayState === "On");
heaterStatusCard.classList.toggle("bg-danger", heaterRelayState === "Off");
heaterStatusCard.querySelector("h5").innerText = heaterRelayState;
}
if (fanStatusCard) {
fanStatusCard.classList.toggle("bg-success", fanRelayState === "On");
fanStatusCard.classList.toggle("bg-danger", fanRelayState === "Off");
fanStatusCard.querySelector("h5").innerText = fanRelayState;
}
}
// Show Offline Status
function showOfflineStatus() {
const dryerStatusCard = document.querySelector('.card:nth-child(1)');
const heaterStatusCard = document.getElementById('heaterRelayStatus');
const fanStatusCard = document.getElementById('fanRelayStatus');
if (dryerStatusCard) {
dryerStatusCard.classList.remove("bg-success");
dryerStatusCard.classList.add("bg-danger");
dryerStatusCard.querySelector("h5").innerText = "Offline";
}
if (heaterStatusCard) {
heaterStatusCard.classList.remove("bg-success");
heaterStatusCard.classList.add("bg-danger");
heaterStatusCard.querySelector("h5").innerText = "Offline";
}
if (fanStatusCard) {
fanStatusCard.classList.remove("bg-success");
fanStatusCard.classList.add("bg-danger");
fanStatusCard.querySelector("h5").innerText = "Offline";
}
}
// Fetch data every 10 seconds
fetchDryerStatus();
setInterval(fetchDryerStatus, 10000);
});
</script>
</body>
</html>

View File

@@ -1,32 +1,32 @@
<?php include 'assets/php/session_check.php'; ?>
<?php include '../src/session_check.php'; ?>
<html lang="en" data-bs-theme="light">
<html lang="en" data-bs-theme="dark">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!--favicon-->
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png">
<link rel="icon" href="../assets/images/favicon-32x32.png" type="image/png">
<!--plugins-->
<link href="assets/plugins/vectormap/jquery-jvectormap-2.0.2.css" rel="stylesheet">
<link href="assets/plugins/simplebar/css/simplebar.css" rel="stylesheet">
<link href="assets/plugins/perfect-scrollbar/css/perfect-scrollbar.css" rel="stylesheet">
<link href="assets/plugins/metismenu/css/metisMenu.min.css" rel="stylesheet">
<link href="../assets/plugins/vectormap/jquery-jvectormap-2.0.2.css" rel="stylesheet">
<link href="../assets/plugins/simplebar/css/simplebar.css" rel="stylesheet">
<link href="../assets/plugins/perfect-scrollbar/css/perfect-scrollbar.css" rel="stylesheet">
<link href="../assets/plugins/metismenu/css/metisMenu.min.css" rel="stylesheet">
<!-- loader-->
<link href="assets/css/pace.min.css" rel="stylesheet"/>
<script src="assets/js/pace.min.js"></script>
<link href="../assets/css/pace.min.css" rel="stylesheet"/>
<script src="../assets/js/pace.min.js"></script>
<!-- Bootstrap CSS -->
<link href="assets/css/bootstrap.min.css" rel="stylesheet">
<link href="assets/css/bootstrap-extended.css" rel="stylesheet">
<link href="../assets/css/bootstrap.min.css" rel="stylesheet">
<link href="../assets/css/bootstrap-extended.css" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500&display=swap" rel="stylesheet">
<link href="assets/sass/app.css" rel="stylesheet">
<link href="assets/css/icons.css" rel="stylesheet">
<link href="../assets/sass/app.css" rel="stylesheet">
<link href="../assets/css/icons.css" rel="stylesheet">
<link href='https://unpkg.com/boxicons@2.1.4/css/boxicons.min.css' rel='stylesheet'>
<!-- Theme Style CSS -->
<link rel="stylesheet" href="assets/sass/dark-theme.css">
<link rel="stylesheet" href="assets/sass/semi-dark.css">
<link rel="stylesheet" href="assets/sass/bordered-theme.css">
<link rel="stylesheet" href="../assets/sass/dark-theme.css">
<link rel="stylesheet" href="../assets/sass/semi-dark.css">
<link rel="stylesheet" href="../assets/sass/bordered-theme.css">
<title>TOD Dashboard</title>
</head>
@@ -35,88 +35,10 @@
<!--wrapper-->
<div class="wrapper">
<!--sidebar wrapper -->
<?php include 'assets/php/nav.php'; ?>
<?php include '../src/nav.php'; ?>
<!--end sidebar wrapper -->
<!--start header -->
<header>
<div class="topbar">
<nav class="navbar navbar-expand gap-2 align-items-center">
<div class="mobile-toggle-menu d-flex"><i class='bx bx-menu'></i>
</div>
<div class="search-bar d-lg-block d-none" data-bs-toggle="modal" data-bs-target="#SearchModal">
<a href="avascript:;" class="btn d-flex align-items-center"><i class="bx bx-search"></i>Search</a>
</div>
<div class="top-menu ms-auto">
<ul class="navbar-nav align-items-center gap-1">
<li class="nav-item mobile-search-icon d-flex d-lg-none" data-bs-toggle="modal" data-bs-target="#SearchModal">
<a class="nav-link" href="avascript:;"><i class='bx bx-search'></i>
</a>
</li>
<li class="nav-item dark-mode d-none d-sm-flex">
<a class="nav-link dark-mode-icon" href="javascript:;"><i class='bx bx-moon'></i>
</a>
</li>
<li class="nav-item dropdown dropdown-large">
<a class="nav-link dropdown-toggle dropdown-toggle-nocaret position-relative" href="#" data-bs-toggle="dropdown"><span class="alert-count">7</span>
<i class='bx bx-bell'></i>
</a>
<div class="dropdown-menu dropdown-menu-end">
<a href="javascript:;">
<div class="msg-header">
<p class="msg-header-title">Notifications</p>
<p class="msg-header-badge">8 New</p>
</div>
</a>
<div class="header-notifications-list">
<a class="dropdown-item" href="javascript:;">
<div class="d-flex align-items-center">
<div class="user-online">
<img src="assets/images/avatars/avatar-1.png" class="msg-avatar" alt="user avatar">
</div>
<div class="flex-grow-1">
<h6 class="msg-name">Daisy Anderson<span class="msg-time float-end">5 sec
ago</span></h6>
<p class="msg-info">The standard chunk of lorem</p>
</div>
</div>
</a>
</div>
<a href="javascript:;">
<div class="text-center msg-footer">
<button class="btn btn-primary w-100">View All Notifications</button>
</div>
</a>
</div>
</li>
</ul>
</div>
<div class="user-box dropdown px-3">
<a class="d-flex align-items-center nav-link dropdown-toggle gap-3 dropdown-toggle-nocaret" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
<img src="assets/images/avatars/avatar-2.png" class="user-img" alt="user avatar">
<div class="user-info">
<p class="user-name mb-0"><?php echo isset($_SESSION['username']) ? htmlspecialchars($_SESSION['username']) : 'Guest'; ?></p>
<p class="designattion mb-0"><?php echo isset($_SESSION['role']) ? htmlspecialchars($_SESSION['role']) : 'N/A'; ?></p>
</div>
</a>
<ul class="dropdown-menu dropdown-menu-end">
<li><a class="dropdown-item d-flex align-items-center" href="javascript:;"><i class="bx bx-user fs-5"></i><span>Profile</span></a>
</li>
<li><a class="dropdown-item d-flex align-items-center" href="javascript:;"><i class="bx bx-cog fs-5"></i><span>My Printers</span></a>
</li>
<li>
<div class="dropdown-divider mb-0"></div>
</li>
<li><a class="dropdown-item d-flex align-items-center" href="logout.php"><i class="bx bx-log-out-circle"></i><span>Logout</span></a>
</li>
</ul>
</div>
</nav>
</div>
</header>
<?php include '../src/header.php'; ?>
<!--end header -->
<!--start page wrapper -->
<div class="page-wrapper">
@@ -291,18 +213,18 @@
<!-- end search modal -->
<!-- Bootstrap JS -->
<script src="assets/js/bootstrap.bundle.min.js"></script>
<script src="../assets/js/bootstrap.bundle.min.js"></script>
<!--plugins-->
<script src="assets/js/jquery.min.js"></script>
<script src="assets/plugins/simplebar/js/simplebar.min.js"></script>
<script src="assets/plugins/metismenu/js/metisMenu.min.js"></script>
<script src="assets/plugins/perfect-scrollbar/js/perfect-scrollbar.js"></script>
<script src="assets/plugins/apexcharts-bundle/js/apexcharts.min.js"></script>
<script src="../assets/js/jquery.min.js"></script>
<script src="../assets/plugins/simplebar/js/simplebar.min.js"></script>
<script src="../assets/plugins/metismenu/js/metisMenu.min.js"></script>
<script src="../assets/plugins/perfect-scrollbar/js/perfect-scrollbar.js"></script>
<script src="../assets/plugins/apexcharts-bundle/js/apexcharts.min.js"></script>
<!--app JS-->
<script src="assets/js/app.js"></script>
<script src="../assets/js/app.js"></script>
<script src="assets/js/index.js"></script>
<script src="assets/plugins/peity/jquery.peity.min.js"></script>
<script src="../assets/js/index.js"></script>
<script src="../assets/plugins/peity/jquery.peity.min.js"></script>
<script>
$(".data-attributes span").peity("donut")
</script>

View File

@@ -1,22 +1,25 @@
<!doctype html>
<html lang="en" data-bs-theme="light">
<html lang="en" data-bs-theme="dark">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png">
<link href="assets/plugins/simplebar/css/simplebar.css" rel="stylesheet">
<link href="assets/plugins/perfect-scrollbar/css/perfect-scrollbar.css" rel="stylesheet">
<link href="assets/plugins/metismenu/css/metisMenu.min.css" rel="stylesheet">
<link href="assets/css/pace.min.css" rel="stylesheet">
<script src="assets/js/pace.min.js"></script>
<link href="assets/css/bootstrap.min.css" rel="stylesheet">
<link href="assets/css/bootstrap-extended.css" rel="stylesheet">
<link rel="icon" href="../assets/images/favicon-32x32.png" type="image/png">
<link href="../assets/plugins/simplebar/css/simplebar.css" rel="stylesheet">
<link href="../assets/plugins/perfect-scrollbar/css/perfect-scrollbar.css" rel="stylesheet">
<link href="../assets/plugins/metismenu/css/metisMenu.min.css" rel="stylesheet">
<link href="../assets/css/pace.min.css" rel="stylesheet">
<script src="../assets/js/pace.min.js"></script>
<link href="../assets/css/bootstrap.min.css" rel="stylesheet">
<link href="../assets/css/bootstrap-extended.css" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500&display=swap" rel="stylesheet">
<link href="assets/sass/app.css" rel="stylesheet">
<link rel="stylesheet" href="assets/sass/dark-theme.css">
<link href="assets/css/icons.css" rel="stylesheet">
<title>Syndron - Admin Dashboard</title>
<link href="../assets/sass/app.css" rel="stylesheet">
<!-- Theme Style CSS -->
<link rel="stylesheet" href="../assets/sass/dark-theme.css">
<link rel="stylesheet" href="../assets/sass/semi-dark.css">
<link rel="stylesheet" href="../assets/sass/bordered-theme.css">
<link href="../assets/css/icons.css" rel="stylesheet">
<title>TOD - Admin Dashboard</title>
</head>
<body>
@@ -26,7 +29,7 @@
<div class="col-12 col-xl-7 col-xxl-8 auth-cover-left d-none d-xl-flex align-items-center justify-content-center">
<div class="card shadow-none bg-transparent rounded-0">
<div class="card-body">
<img src="assets/images/login-images/login-cover.svg" class="img-fluid" width="650" alt=""/>
<img src="../assets/images/login-images/login-cover.svg" class="img-fluid" width="650" alt=""/>
</div>
</div>
</div>
@@ -34,13 +37,13 @@
<div class="card rounded-0 m-3 shadow-none bg-transparent">
<div class="card-body p-sm-5">
<div class="text-center mb-4">
<img src="assets/images/logo-icon.png" width="60" alt="">
<h5>Syndron Admin</h5>
<img src="../assets/images/logo-icon.png" width="60" alt="">
<h5>TechOdyssey Designs Dashboard</h5>
<p>Please log in to your account</p>
</div>
<form id="loginForm" class="row g-3">
<div class="col-12">
<label for="inputEmailAddress" class="form-label">Email</label>
<label for="inputEmailAddress" class="form-label">Username</label>
<input type="input" class="form-control" name="username" id="inputEmailAddress" required>
</div>
<div class="col-12">
@@ -62,11 +65,11 @@
</div>
</div>
<script src="assets/js/bootstrap.bundle.min.js"></script>
<script src="assets/js/jquery.min.js"></script>
<script src="assets/plugins/simplebar/js/simplebar.min.js"></script>
<script src="assets/plugins/metismenu/js/metisMenu.min.js"></script>
<script src="assets/plugins/perfect-scrollbar/js/perfect-scrollbar.js"></script>
<script src="../assets/js/bootstrap.bundle.min.js"></script>
<script src="../assets/js/jquery.min.js"></script>
<script src="../assets/plugins/simplebar/js/simplebar.min.js"></script>
<script src="../assets/plugins/metismenu/js/metisMenu.min.js"></script>
<script src="../assets/plugins/perfect-scrollbar/js/perfect-scrollbar.js"></script>
<script>
$(document).ready(function () {
@@ -75,7 +78,7 @@
const formData = $(this).serialize();
$.ajax({
url: 'assets/php/login.php',
url: '../src/login.php',
type: 'POST',
data: formData,
success: function (response) {
@@ -90,6 +93,6 @@
});
</script>
<script src="assets/js/app.js"></script>
<script src="../assets/js/app.js"></script>
</body>
</html>

View File

@@ -2,6 +2,6 @@
session_start();
session_unset();
session_destroy();
header("Location: /login.html");
header("Location: login.php");
exit();
?>

211
public/printerForm.php Normal file
View File

@@ -0,0 +1,211 @@
<?php include '../src/session_check.php'; ?>
<html lang="en" data-bs-theme="dark">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!--favicon-->
<link rel="icon" href="../assets/images/favicon-32x32.png" type="image/png">
<!--plugins-->
<link href="../assets/plugins/vectormap/jquery-jvectormap-2.0.2.css" rel="stylesheet">
<link href="../assets/plugins/simplebar/css/simplebar.css" rel="stylesheet">
<link href="../assets/plugins/perfect-scrollbar/css/perfect-scrollbar.css" rel="stylesheet">
<link href="../assets/plugins/metismenu/css/metisMenu.min.css" rel="stylesheet">
<!-- loader-->
<link href="../assets/css/pace.min.css" rel="stylesheet"/>
<script src="../assets/js/pace.min.js"></script>
<!-- Bootstrap CSS -->
<link href="../assets/css/bootstrap.min.css" rel="stylesheet">
<link href="../assets/css/bootstrap-extended.css" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500&display=swap" rel="stylesheet">
<link href="../assets/sass/app.css" rel="stylesheet">
<link href="../assets/css/icons.css" rel="stylesheet">
<link href='https://unpkg.com/boxicons@2.1.4/css/boxicons.min.css' rel='stylesheet'>
<!-- Theme Style CSS -->
<link rel="stylesheet" href="../assets/sass/dark-theme.css">
<link rel="stylesheet" href="../assets/sass/semi-dark.css">
<link rel="stylesheet" href="../assets/sass/bordered-theme.css">
<title>TOD Dashboard</title>
</head>
<body>
<!--wrapper-->
<div class="wrapper">
<!--sidebar wrapper -->
<?php include '../src/nav.php'; ?>
<!--end sidebar wrapper -->
<!--start header -->
<?php include '../src/header.php'; ?>
<!--end header -->
<!--start page wrapper -->
<div class="page-wrapper">
<div class="page-content">
<!--start page content -->
<!--breadcrumb-->
<div class="page-breadcrumb d-none d-sm-flex align-items-center mb-3">
<div class="breadcrumb-title pe-3">User Settings</div>
<div class="ps-3">
<nav aria-label="breadcrumb">
<ol class="breadcrumb mb-0 p-0">
<li class="breadcrumb-item"><a href="index.php"><i class="bx bx-home-alt"></i></a>
</li>
<li class="breadcrumb-item active" aria-current="page">Add Printer</li>
</ol>
</nav>
</div>
</div>
<!--end breadcrumb-->
<div class="row">
<div class="col-lg-8 mx-auto">
<div class="card">
<div class="card-header px-4 py-3">
<h5 class="mb-0">Add New Printer</h5>
</div>
<div class="card-body p-4">
<!-- Alert Banner (Hidden Initially) -->
<div id="alertBanner" class="alert d-none" role="alert"></div>
<form id="printerForm">
<div class="row mb-3">
<label for="printerName" class="col-sm-3 col-form-label">Printer Name</label>
<div class="col-sm-9">
<input type="text" class="form-control" id="printerName" name="printerName" placeholder="Bambu X1 or P1P" required>
</div>
</div>
<div class="row mb-3">
<label for="printerIp" class="col-sm-3 col-form-label">Printer IP</label>
<div class="col-sm-9">
<input type="text" class="form-control" id="printerIp" name="printerIp" placeholder="e.g., 192.168.1.100" required>
</div>
</div>
<div class="row mb-3">
<label for="serialNumber" class="col-sm-3 col-form-label">Serial Number</label>
<div class="col-sm-9">
<input type="text" class="form-control" id="serialNumber" name="serialNumber" placeholder="e.g., X1SN123456" required>
</div>
</div>
<div class="row mb-3">
<label for="accessCode" class="col-sm-3 col-form-label">Printer Access Code</label>
<div class="col-sm-9">
<input type="password" class="form-control" id="accessCode" name="accessCode" placeholder="Enter Printer Access Code" required>
</div>
</div>
<div class="row">
<label class="col-sm-3 col-form-label"></label>
<div class="col-sm-9">
<div class="d-md-flex d-grid align-items-center gap-3">
<button type="submit" class="btn btn-primary px-4">Add Printer</button>
<button type="reset" class="btn btn-light px-4">Reset</button>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- jQuery & AJAX Script -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script>
$(document).ready(function() {
$('#printerForm').on('submit', function(event) {
event.preventDefault(); // Prevent traditional form submission
// Collect form data
let formData = $(this).serialize();
// AJAX request to submit form
$.ajax({
type: 'POST',
url: '../src/printers/addPrinter.php',
data: formData,
dataType: 'json',
success: function(response) {
let banner = $('#alertBanner');
if (response.status === 'success') {
banner.removeClass('d-none alert-danger')
.addClass('alert-success')
.text(response.message);
} else {
banner.removeClass('d-none alert-success')
.addClass('alert-danger')
.text(response.message);
}
},
error: function() {
$('#alertBanner').removeClass('d-none alert-success')
.addClass('alert-danger')
.text('An error occurred while adding the printer.');
}
});
});
});
</script>
<!--end page content -->
</div>
</div>
<!--end page wrapper -->
<!--start overlay-->
<div class="overlay mobile-toggle-icon"></div>
<!--end overlay-->
<!--Start Back To Top Button-->
<a href="javaScript:;" class="back-to-top"><i class='bx bxs-up-arrow-alt'></i></a>
<!--End Back To Top Button-->
<footer class="page-footer">
<p class="mb-0">Copyright © 2024. All right reserved.</p>
</footer>
</div>
<!--end wrapper-->
<!-- search modal -->
<div class="modal" id="SearchModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable modal-fullscreen-md-down">
<div class="modal-content">
<div class="modal-header gap-2">
<div class="position-relative popup-search w-100">
<input class="form-control form-control-lg ps-5 border border-3 border-primary" type="search" placeholder="Search">
<span class="position-absolute top-50 search-show ms-3 translate-middle-y start-0 top-50 fs-4"><i class='bx bx-search'></i></span>
</div>
<button type="button" class="btn-close d-md-none" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
</div>
</div>
</div>
<!-- end search modal -->
<!-- Bootstrap JS -->
<script src="../assets/js/bootstrap.bundle.min.js"></script>
<!--plugins-->
<script src="../assets/js/jquery.min.js"></script>
<script src="../assets/plugins/simplebar/js/simplebar.min.js"></script>
<script src="../assets/plugins/metismenu/js/metisMenu.min.js"></script>
<script src="../assets/plugins/perfect-scrollbar/js/perfect-scrollbar.js"></script>
<script src="../assets/plugins/apexcharts-bundle/js/apexcharts.min.js"></script>
<!--app JS-->
<script src="../assets/js/app.js"></script>
<script src="../assets/js/index.js"></script>
<script src="../assets/plugins/peity/jquery.peity.min.js"></script>
<script>
$(".data-attributes span").peity("donut")
</script>
</body>
</html>

265
public/shipping.php Normal file
View File

@@ -0,0 +1,265 @@
<?php include '../src/session_check.php'; ?>
<html lang="en" data-bs-theme="dark">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!--favicon-->
<link rel="icon" href="../assets/images/favicon-32x32.png" type="image/png">
<!--plugins-->
<link href="../assets/plugins/vectormap/jquery-jvectormap-2.0.2.css" rel="stylesheet">
<link href="../assets/plugins/simplebar/css/simplebar.css" rel="stylesheet">
<link href="../assets/plugins/perfect-scrollbar/css/perfect-scrollbar.css" rel="stylesheet">
<link href="../assets/plugins/metismenu/css/metisMenu.min.css" rel="stylesheet">
<!-- loader-->
<link href="../assets/css/pace.min.css" rel="stylesheet"/>
<script src="../assets/js/pace.min.js"></script>
<!-- Bootstrap CSS -->
<link href="../assets/css/bootstrap.min.css" rel="stylesheet">
<link href="../assets/css/bootstrap-extended.css" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500&display=swap" rel="stylesheet">
<link href="../assets/sass/app.css" rel="stylesheet">
<link href="../assets/css/icons.css" rel="stylesheet">
<link href='https://unpkg.com/boxicons@2.1.4/css/boxicons.min.css' rel='stylesheet'>
<!-- Theme Style CSS -->
<link rel="stylesheet" href="../assets/sass/dark-theme.css">
<link rel="stylesheet" href="../assets/sass/semi-dark.css">
<link rel="stylesheet" href="../assets/sass/bordered-theme.css">
<title>TOD Dashboard</title>
</head>
<body>
<!--wrapper-->
<div class="wrapper">
<!--sidebar wrapper -->
<?php include '../src/nav.php'; ?>
<!--end sidebar wrapper -->
<!--start header -->
<?php include '../src/header.php'; ?>
<!--end header -->
<!--start page wrapper -->
<div class="page-wrapper">
<div class="page-content">
<div class="">
<div class="">
<!--start page content -->
<div class="row g-4">
<!-- Account Information Card -->
<div class="col-12 col-xl-6">
<div class="card rounded-4 mb-0 shadow-sm">
<div class="card-body">
<div class="d-flex align-items-center gap-3">
<div class="widgets-icons bg-light-success text-success rounded-circle d-flex align-items-center justify-content-center p-3">
<i class='bx bxs-user fs-3'></i>
</div>
<div id="accountInfo" class="ms-3">
<h5 class="mb-0 fw-bold">Loading...</h5>
<p class="text-muted mb-0">Account Information</p>
</div>
</div>
</div>
</div>
</div>
<!-- Prepay Balance Card -->
<div class="col-12 col-xl-6">
<div class="card rounded-4 mb-0 shadow-sm">
<div class="card-body">
<div class="d-flex align-items-center gap-3">
<div class="widgets-icons bg-light-info text-info rounded-circle d-flex align-items-center justify-content-center p-3">
<i class='bx bx-wallet fs-3'></i>
</div>
<div id="prepayBalance" class="ms-3">
<h5 class="mb-0 fw-bold">Loading...</h5>
<p class="text-muted mb-0">Prepay Balance</p>
</div>
</div>
</div>
</div>
</div>
<!-- Orders Table -->
<div class="col-12 col-xl-12">
<div class="card rounded-4 mb-0 shadow-sm">
<div class="card-body">
<h5 class="card-title fw-bold mb-3">Orders</h5>
<div id="ordersDetail" class="table-responsive">
<table class="table table-hover align-middle">
<thead class="table-dark">
<tr>
<th>Order ID</th>
<th>Courier</th>
<th>Service</th>
<th>Tracking Status</th>
<th>Estimated Delivery</th>
<th>Recipient</th>
</tr>
</thead>
<tbody>
<tr>
<td colspan="6" class="text-center text-muted">Loading orders...</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<!--end page content -->
</div>
<!--end page wrapper -->
<!--start overlay-->
<div class="overlay mobile-toggle-icon"></div>
<!--end overlay-->
<!--Start Back To Top Button-->
<a href="javaScript:;" class="back-to-top"><i class='bx bxs-up-arrow-alt'></i></a>
<!--End Back To Top Button-->
</div>
<!--end wrapper-->
<!-- search modal -->
<div class="modal" id="SearchModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable modal-fullscreen-md-down">
<div class="modal-content">
<div class="modal-header gap-2">
<div class="position-relative popup-search w-100">
<input class="form-control form-control-lg ps-5 border border-3 border-primary" type="search" placeholder="Search">
<span class="position-absolute top-50 search-show ms-3 translate-middle-y start-0 top-50 fs-4"><i class='bx bx-search'></i></span>
</div>
<button type="button" class="btn-close d-md-none" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
</div>
</div>
</div>
<!-- end search modal -->
<!-- Bootstrap JS -->
<script src="../assets/js/bootstrap.bundle.min.js"></script>
<!--plugins-->
<script src="../assets/js/jquery.min.js"></script>
<script src="../assets/plugins/simplebar/js/simplebar.min.js"></script>
<script src="../assets/plugins/metismenu/js/metisMenu.min.js"></script>
<script src="../assets/plugins/perfect-scrollbar/js/perfect-scrollbar.js"></script>
<script src="../assets/plugins/apexcharts-bundle/js/apexcharts.min.js"></script>
<!--app JS-->
<script src="../assets/js/app.js"></script>
<script src="../assets/js/index.js"></script>
<script src="../assets/plugins/peity/jquery.peity.min.js"></script>
<script>
$(".data-attributes span").peity("donut")
</script>
<script>
$(document).ready(function () {
// Fetch account information via AJAX
function fetchAccountInfo() {
$.ajax({
url: '../src/shipping/get_account_info.php',
type: 'POST',
dataType: 'json',
success: function (response) {
if (response.success) {
const data = response.data;
let html = `
<h5 class="mb-0 fw-bold">${data.Forename || 'N/A'} ${data.Surname || ''}</h5>
<p class="text-muted mb-0">Email: ${data.Email || 'N/A'}</p>
<p class="text-muted mb-0">Member Since: ${data.MemberSince || 'N/A'}</p>
`;
$('#accountInfo').html(html);
} else {
$('#accountInfo').html('<p class="text-danger">Error fetching account information.</p>');
}
},
error: function () {
$('#accountInfo').html('<p class="text-danger">Error loading account information.</p>');
}
});
}
// Fetch prepay balance via AJAX
function fetchPrepayBalance() {
$.ajax({
url: '../src/shipping/get_prepay_balance.php',
type: 'POST',
dataType: 'json',
success: function (response) {
if (response.success) {
const data = response.data;
const balance = data.Balance || 0;
$('#prepayBalance').html(`
<h5 class="mb-0 fw-bold">£ ${balance}</h5>
<p class="text-muted mb-0">Prepay Balance</p>
`);
} else {
$('#prepayBalance').html('<p class="text-danger">Error fetching balance.</p>');
}
},
error: function () {
$('#prepayBalance').html('<p class="text-danger">Error loading balance.</p>');
}
});
}
// Fetch order details via AJAX
function fetchOrdersDetail() {
$.ajax({
url: '../src/shipping/get_orders_detail.php',
type: 'POST',
dataType: 'json',
success: function (response) {
if (response.success) {
const orders = response.data.Orders || [];
let html = '';
if (orders.length === 0) {
html = '<tr><td colspan="6" class="text-center text-muted">No orders found.</td></tr>';
} else {
orders.forEach(order => {
const trackingStatus = order.Tracking?.Delivered ?
'<span class="badge bg-success">Delivered</span>' :
'<span class="badge bg-warning text-dark">In Transit</span>';
html += `
<tr>
<td>${order.OrderLineId}</td>
<td>${order.Courier}</td>
<td>${order.Service}</td>
<td>${trackingStatus}</td>
<td>${order.EstimatedDeliveryDate ? new Date(order.EstimatedDeliveryDate).toLocaleString() : 'N/A'}</td>
<td>${order.DeliveryAddress?.ContactName || 'N/A'}</td>
</tr>
`;
});
}
$('#ordersDetail tbody').html(html);
} else {
$('#ordersDetail tbody').html('<tr><td colspan="6" class="text-center text-danger">Error fetching orders.</td></tr>');
}
},
error: function () {
$('#ordersDetail tbody').html('<tr><td colspan="6" class="text-center text-danger">Error loading orders.</td></tr>');
}
});
}
// Trigger all fetches on page load
fetchAccountInfo();
fetchPrepayBalance();
fetchOrdersDetail();
});
</script>
</body>
</html>

319
public/userManagement.php Normal file
View File

@@ -0,0 +1,319 @@
<?php include '../src/session_check.php';
checkUserRole(['admin']);
?>
<html lang="en" data-bs-theme="dark">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!--favicon-->
<link rel="icon" href="../assets/images/favicon-32x32.png" type="image/png">
<!--plugins-->
<link href="../assets/plugins/vectormap/jquery-jvectormap-2.0.2.css" rel="stylesheet">
<link href="../assets/plugins/simplebar/css/simplebar.css" rel="stylesheet">
<link href="../assets/plugins/perfect-scrollbar/css/perfect-scrollbar.css" rel="stylesheet">
<link href="../assets/plugins/metismenu/css/metisMenu.min.css" rel="stylesheet">
<!-- loader-->
<link href="../assets/css/pace.min.css" rel="stylesheet"/>
<script src="../assets/js/pace.min.js"></script>
<!-- Bootstrap CSS -->
<link href="../assets/css/bootstrap.min.css" rel="stylesheet">
<link href="../assets/css/bootstrap-extended.css" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500&display=swap" rel="stylesheet">
<link href="../assets/sass/app.css" rel="stylesheet">
<link href="../assets/css/icons.css" rel="stylesheet">
<link href='https://unpkg.com/boxicons@2.1.4/css/boxicons.min.css' rel='stylesheet'>
<!-- Theme Style CSS -->
<link rel="stylesheet" href="../assets/sass/dark-theme.css">
<link rel="stylesheet" href="../assets/sass/semi-dark.css">
<link rel="stylesheet" href="../assets/sass/bordered-theme.css">
<title>TOD Dashboard</title>
</head>
<body>
<!--wrapper-->
<div class="wrapper">
<!--sidebar wrapper -->
<?php include '../src/nav.php'; ?>
<!--end sidebar wrapper -->
<!--start header -->
<?php include '../src/header.php'; ?>
<!--end header -->
<!--start page wrapper -->
<div class="page-wrapper">
<div class="page-content">
<!--start page content -->
<div class="container mt-4">
<h2 class="text-center mb-4">User Management</h2>
<div class="d-flex justify-content-between mb-3">
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#createUserModal">
<i class="fas fa-user-plus"></i> Create New User
</button>
</div>
<div class="table-responsive">
<table class="table table-striped table-hover table-bordered align-middle">
<thead class="table-dark">
<tr>
<th>ID</th>
<th>Username</th>
<th>Email</th>
<th>Role</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="userTableBody">
<!-- Rows dynamically populated -->
</tbody>
</table>
</div>
<!-- Create User Modal -->
<div class="modal fade" id="createUserModal" tabindex="-1" aria-labelledby="createUserModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="createUserModalLabel">Create New User</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="createUserForm">
<div class="mb-3">
<label for="username" class="form-label">Username</label>
<input type="text" class="form-control" id="username" required>
</div>
<div class="mb-3">
<label for="email" class="form-label">Email</label>
<input type="email" class="form-control" id="email" required>
</div>
<div class="mb-3">
<label for="role" class="form-label">Role</label>
<select id="role" class="form-select" required>
<option value="">Select Role</option>
<option value="admin">Admin</option>
<option value="user">User</option>
</select>
</div>
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<input type="password" class="form-control" id="password" required>
</div>
<button type="submit" class="btn btn-primary w-100">Create User</button>
</form>
</div>
</div>
</div>
</div>
<!-- Reset Password Modal -->
<div class="modal fade" id="resetPasswordModal" tabindex="-1" aria-labelledby="resetPasswordModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="resetPasswordModalLabel">Reset Password</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="resetPasswordForm">
<p>Enter a new password for <span id="resetUserName"></span>:</p>
<div class="mb-3">
<label for="newPassword" class="form-label">New Password</label>
<input type="password" class="form-control" id="newPassword" required>
</div>
<button type="submit" class="btn btn-warning w-100">Reset Password</button>
</form>
</div>
</div>
</div>
</div>
</div>
<!--end page content -->
</div>
</div>
<!--end page wrapper -->
<!--start overlay-->
<div class="overlay mobile-toggle-icon"></div>
<!--end overlay-->
<!--Start Back To Top Button-->
<a href="javaScript:;" class="back-to-top"><i class='bx bxs-up-arrow-alt'></i></a>
<!--End Back To Top Button-->
<footer class="page-footer">
<p class="mb-0">Copyright © 2024. All right reserved.</p>
</footer>
</div>
<!--end wrapper-->
<!-- search modal -->
<div class="modal" id="SearchModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable modal-fullscreen-md-down">
<div class="modal-content">
<div class="modal-header gap-2">
<div class="position-relative popup-search w-100">
<input class="form-control form-control-lg ps-5 border border-3 border-primary" type="search" placeholder="Search">
<span class="position-absolute top-50 search-show ms-3 translate-middle-y start-0 top-50 fs-4"><i class='bx bx-search'></i></span>
</div>
<button type="button" class="btn-close d-md-none" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
</div>
</div>
</div>
<!-- end search modal -->
<!-- Bootstrap JS -->
<script src="../assets/js/bootstrap.bundle.min.js"></script>
<!--plugins-->
<script src="../assets/js/jquery.min.js"></script>
<script src="../assets/plugins/simplebar/js/simplebar.min.js"></script>
<script src="../assets/plugins/metismenu/js/metisMenu.min.js"></script>
<script src="../assets/plugins/perfect-scrollbar/js/perfect-scrollbar.js"></script>
<script src="../assets/plugins/apexcharts-bundle/js/apexcharts.min.js"></script>
<!--app JS-->
<script src="../assets/js/app.js"></script>
<script src="../assets/js/index.js"></script>
<script src="../assets/plugins/peity/jquery.peity.min.js"></script>
<script>
$(".data-attributes span").peity("donut")
</script>
<script>
$(document).ready(function () {
const apiUrl = '../src/userMananagementService.php';
// Load users on page load
fetchUsers();
// Create new user form submission
$('#createUserForm').submit(function (e) {
e.preventDefault();
const username = $('#username').val();
const email = $('#email').val();
const role = $('#role').val();
const password = $('#password').val();
$.ajax({
url: apiUrl,
method: 'POST',
dataType: 'json',
data: JSON.stringify({ username, email, role, password }),
contentType: 'application/json',
success: function () {
alert('User created successfully!');
fetchUsers();
$('#createUserModal').modal('hide');
$('#createUserForm')[0].reset();
},
error: function (xhr) {
alert('Error creating user: ' + xhr.responseText);
},
});
});
// Reset password form submission
$('#resetPasswordForm').submit(function (e) {
e.preventDefault();
const userId = $('#resetPasswordModal').data('userId');
const password = $('#newPassword').val();
$.ajax({
url: `${apiUrl}?reset-password=${userId}`,
method: 'POST',
dataType: 'json',
data: JSON.stringify({ password }),
contentType: 'application/json',
success: function () {
alert('Password reset successfully!');
$('#resetPasswordModal').modal('hide');
$('#resetPasswordForm')[0].reset();
},
error: function (xhr) {
alert('Error resetting password: ' + xhr.responseText);
},
});
});
});
// Fetch users from the backend
function fetchUsers() {
const apiUrl = '../src/userMananagementService.php';
$.ajax({
url: apiUrl,
method: 'GET',
dataType: 'json',
success: function (users) {
const tableBody = $('#userTableBody');
tableBody.empty();
users.forEach(function (user) {
const row = `
<tr>
<td>${user.id}</td>
<td>${user.username}</td>
<td>${user.email}</td>
<td>${user.role}</td>
<td>
<button class="btn btn-warning btn-sm" onclick="openResetPasswordModal(${user.id}, '${user.username}')">
<i class="fas fa-key"></i> Reset Password
</button>
<button class="btn btn-danger btn-sm" onclick="deleteUser(${user.id})">
<i class="fas fa-trash"></i> Delete
</button>
</td>
</tr>
`;
tableBody.append(row);
});
},
error: function (xhr) {
alert('Error fetching users: ' + xhr.responseText);
},
});
}
// Open the reset password modal
function openResetPasswordModal(userId, username) {
$('#resetPasswordModal').data('userId', userId);
$('#resetUserName').text(username);
$('#resetPasswordModal').modal('show');
}
// Delete user
function deleteUser(userId) {
const apiUrl = '../src/userMananagementService.php';
if (confirm('Are you sure you want to delete this user?')) {
$.ajax({
url: `${apiUrl}?delete-user=${userId}`,
method: 'POST',
success: function () {
alert('User deleted successfully!');
fetchUsers();
},
error: function (xhr) {
alert('Error deleting user: ' + xhr.responseText);
},
});
}
}
</script>
</body>
</html>

283
public/viewFilament.php Normal file
View File

@@ -0,0 +1,283 @@
<?php include '../src/session_check.php'; ?>
<html lang="en" data-bs-theme="dark">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!--favicon-->
<link rel="icon" href="../assets/images/favicon-32x32.png" type="image/png">
<!--plugins-->
<link href="../assets/plugins/vectormap/jquery-jvectormap-2.0.2.css" rel="stylesheet">
<link href="../assets/plugins/simplebar/css/simplebar.css" rel="stylesheet">
<link href="../assets/plugins/perfect-scrollbar/css/perfect-scrollbar.css" rel="stylesheet">
<link href="../assets/plugins/metismenu/css/metisMenu.min.css" rel="stylesheet">
<!-- loader-->
<link href="../assets/css/pace.min.css" rel="stylesheet"/>
<script src="../assets/js/pace.min.js"></script>
<!-- Bootstrap CSS -->
<link href="../assets/css/bootstrap.min.css" rel="stylesheet">
<link href="../assets/css/bootstrap-extended.css" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500&display=swap" rel="stylesheet">
<link href="../assets/sass/app.css" rel="stylesheet">
<link href="../assets/css/icons.css" rel="stylesheet">
<link href='https://unpkg.com/boxicons@2.1.4/css/boxicons.min.css' rel='stylesheet'>
<!-- Theme Style CSS -->
<link rel="stylesheet" href="../assets/sass/dark-theme.css">
<link rel="stylesheet" href="../assets/sass/semi-dark.css">
<link rel="stylesheet" href="../assets/sass/bordered-theme.css">
<title>TOD Dashboard</title>
</head>
<body>
<!--wrapper-->
<div class="wrapper">
<!--sidebar wrapper -->
<?php include '../src/nav.php'; ?>
<!--end sidebar wrapper -->
<!--start header -->
<?php include '../src/header.php'; ?>
<!--end header -->
<!--start page wrapper -->
<div class="page-wrapper">
<div class="page-content">
<!--start page content -->
<div class="container">
<div class="row" id="filamentCardContainer">
<!-- Cards will be inserted here dynamically -->
</div>
</div>
<!--end page content -->
</div>
</div>
<!--end page wrapper -->
<!--start overlay-->
<div class="overlay mobile-toggle-icon"></div>
<!--end overlay-->
<!--Start Back To Top Button-->
<a href="javaScript:;" class="back-to-top"><i class='bx bxs-up-arrow-alt'></i></a>
<!--End Back To Top Button-->
</div>
<!--end wrapper-->
<!-- search modal -->
<div class="modal" id="SearchModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable modal-fullscreen-md-down">
<div class="modal-content">
<div class="modal-header gap-2">
<div class="position-relative popup-search w-100">
<input class="form-control form-control-lg ps-5 border border-3 border-primary" type="search" placeholder="Search">
<span class="position-absolute top-50 search-show ms-3 translate-middle-y start-0 top-50 fs-4"><i class='bx bx-search'></i></span>
</div>
<button type="button" class="btn-close d-md-none" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
</div>
</div>
</div>
<!-- end search modal -->
<!-- Bootstrap JS -->
<script src="../assets/js/bootstrap.bundle.min.js"></script>
<!--plugins-->
<script src="../assets/js/jquery.min.js"></script>
<script src="../assets/plugins/simplebar/js/simplebar.min.js"></script>
<script src="../assets/plugins/metismenu/js/metisMenu.min.js"></script>
<script src="../assets/plugins/perfect-scrollbar/js/perfect-scrollbar.js"></script>
<script src="../assets/plugins/apexcharts-bundle/js/apexcharts.min.js"></script>
<!--app JS-->
<script src="../assets/js/app.js"></script>
<script src="../assets/js/index.js"></script>
<script src="../assets/plugins/peity/jquery.peity.min.js"></script>
<script>
$(".data-attributes span").peity("donut")
</script>
<script>
document.addEventListener("DOMContentLoaded", function () {
const container = document.getElementById('filamentCardContainer');
// Function to generate random colors
function getRandomColor() {
const letters = '0123456789ABCDEF';
let color = '#';
for (let i = 0; i < 6; i++) {
color += letters[Math.floor(Math.random() * 16)];
}
return color;
}
// Skeleton Loader Template
function showSkeletonLoader() {
container.innerHTML = `
<div class="col-lg-4 mb-1">
<div class="card radius-10 overflow-hidden skeleton-loader" style="height: 200px;">
<div class="card-body d-flex align-items-center justify-content-between">
<div>
<div class="skeleton-text mb-2" style="width: 70px; height: 20px;"></div>
<div class="skeleton-text" style="width: 50px; height: 30px;"></div>
</div>
<div class="skeleton-button" style="width: 40px; height: 40px;"></div>
</div>
<div class="skeleton-chart" style="height: 120px;"></div>
</div>
</div>
`;
}
fetch('../src/filamentTracker/getFilamentPrices.php')
.then(response => response.json())
.then(data => {
if (data.status === 'success') {
container.innerHTML = ''; // Clear Skeleton
Object.keys(data.data).forEach(filament => {
const filamentData = data.data[filament];
const prices = filamentData.prices.map(entry => parseFloat(entry.price));
const timestamps = filamentData.prices.map(entry => entry.recordedAt);
let latestPrice = prices[prices.length - 1] || 0;
// Adjust price for voucher
const currentDiscount = filamentData.currentDiscount || {};
const voucher = currentDiscount.voucher || {};
if (voucher.value > 0) {
if (voucher.type === 'percentage') {
latestPrice -= (latestPrice * voucher.value) / 100; // Apply percentage voucher
} else if (voucher.type === 'fixed') {
latestPrice -= voucher.value; // Subtract fixed voucher value
}
}
// Ensure price doesn't go below zero
latestPrice = Math.max(latestPrice, 0);
// Find the last distinct price (where the price actually changed)
let lastDistinctPrice = latestPrice;
for (let i = prices.length - 2; i >= 0; i--) {
if (prices[i] !== latestPrice) {
lastDistinctPrice = prices[i];
break;
}
}
const priceDifference = latestPrice - lastDistinctPrice;
// Price Change Indicator
let priceChangeIndicator;
if (priceDifference > 0) {
priceChangeIndicator = `<span class="text-danger ms-4">
<i class="bx bx-up-arrow-alt"></i> +£${priceDifference.toFixed(2)}</span>`;
} else if (priceDifference < 0) {
priceChangeIndicator = `<span class="text-success ms-4">
<i class="bx bx-down-arrow-alt"></i> -£${Math.abs(priceDifference).toFixed(2)}</span>`;
} else {
priceChangeIndicator = `<span class="text-muted ms-4">
<i class="bx bx-minus"></i> £0.00</span>`;
}
const amazonUrl = filamentData.amazonUrl || '#';
// Extract discount details
const discount = currentDiscount.discount || {};
let discountText = '';
if (voucher.value > 0) {
discountText = voucher.type === 'percentage'
? `-${voucher.value}% Voucher`
: `-£${voucher.value} Voucher`;
} else if (discount.value > 0) {
discountText = discount.type === 'percentage'
? `-${discount.value}%`
: `-£${discount.value}`;
}
const chartId = `chart-${filament.replace(/\s+/g, '-')}`;
const chartColor = getRandomColor();
// Create Card HTML
const cardHTML = `
<div class="col-lg-4 mb-1">
<div class="card radius-10 overflow-hidden">
<div class="card-body d-flex align-items-center justify-content-between">
<div>
<a href="${amazonUrl}" target="_blank" class="mb-0 filament-name">${filament}</a>
<h5 class="mb-0">
£${latestPrice.toFixed(2)} ${priceChangeIndicator}
</h5>
</div>
<div>
${
discountText
? `<span class="text-success fw-bold" style="font-size: 1rem;">
${discountText}
</span>`
: ''
}
</div>
</div>
<div id="${chartId}"></div>
</div>
</div>
`;
container.insertAdjacentHTML('beforeend', cardHTML);
// Chart Rendering
const chartOptions = {
series: [{ name: "Price", data: prices }],
chart: {
type: "area",
height: 110,
toolbar: { show: false },
zoom: { enabled: false },
sparkline: { enabled: true },
},
markers: { size: 0 },
dataLabels: { enabled: false },
stroke: { show: true, width: 2.4, curve: "smooth" },
colors: [chartColor],
xaxis: { categories: timestamps, labels: { show: false } },
fill: { opacity: 1 },
tooltip: {
theme: "dark",
y: {
formatter: function (value, { dataPointIndex }) {
const date = new Date(timestamps[dataPointIndex]);
return `£${value} - ${date.toLocaleDateString()}`;
},
},
},
};
const chartElement = document.getElementById(chartId);
if (chartElement) {
const chart = new ApexCharts(chartElement, chartOptions);
chart.render();
}
});
} else {
container.innerHTML = '<p class="text-center">No filament data available.</p>';
}
})
.catch(error => {
console.error("Error fetching filament data:", error);
});
showSkeletonLoader();
});
</script>
</body>
</html>

249
public/viewPrinters.php Normal file
View File

@@ -0,0 +1,249 @@
<?php include '../src/session_check.php'; ?>
<html lang="en" data-bs-theme="dark">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!--favicon-->
<link rel="icon" href="../assets/images/favicon-32x32.png" type="image/png">
<!--plugins-->
<link href="../assets/plugins/vectormap/jquery-jvectormap-2.0.2.css" rel="stylesheet">
<link href="../assets/plugins/simplebar/css/simplebar.css" rel="stylesheet">
<link href="../assets/plugins/perfect-scrollbar/css/perfect-scrollbar.css" rel="stylesheet">
<link href="../assets/plugins/metismenu/css/metisMenu.min.css" rel="stylesheet">
<!-- loader-->
<link href="../assets/css/pace.min.css" rel="stylesheet"/>
<script src="../assets/js/pace.min.js"></script>
<!-- Bootstrap CSS -->
<link href="../assets/css/bootstrap.min.css" rel="stylesheet">
<link href="../assets/css/bootstrap-extended.css" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500&display=swap" rel="stylesheet">
<link href="../assets/sass/app.css" rel="stylesheet">
<link href="../assets/css/icons.css" rel="stylesheet">
<link href='https://unpkg.com/boxicons@2.1.4/css/boxicons.min.css' rel='stylesheet'>
<!-- Theme Style CSS -->
<link rel="stylesheet" href="../assets/sass/dark-theme.css">
<link rel="stylesheet" href="../assets/sass/semi-dark.css">
<link rel="stylesheet" href="../assets/sass/bordered-theme.css">
<title>TOD Dashboard</title>
</head>
<body>
<!--wrapper-->
<div class="wrapper">
<!--sidebar wrapper -->
<?php include '../src/nav.php'; ?>
<!--end sidebar wrapper -->
<!--start header -->
<?php include '../src/header.php'; ?>
<!--end header -->
<!--start page wrapper -->
<div class="page-wrapper">
<div class="page-content">
<!--start page content -->
<!-- Breadcrumb -->
<div class="page-breadcrumb d-none d-sm-flex align-items-center mb-3">
<div class="breadcrumb-title pe-3">Printer Dashboard</div>
<div class="ps-3">
<nav aria-label="breadcrumb">
<ol class="breadcrumb mb-0 p-0">
<li class="breadcrumb-item"><a href="index.php"><i class="bx bx-home-alt"></i></a></li>
<li class="breadcrumb-item active" aria-current="page">Your Printers</li>
</ol>
</nav>
</div>
<div class="ms-auto">
<div class="btn-group">
<button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
Refresh Interval: <span id="selectedIntervalText">60s</span>
</button>
<ul class="dropdown-menu dropdown-menu-end" id="intervalDropdown">
<li><a class="dropdown-item set-interval" data-interval="5000" href="#">5 seconds</a></li>
<li><a class="dropdown-item set-interval" data-interval="10000" href="#">10 seconds</a></li>
<li><a class="dropdown-item set-interval" data-interval="30000" href="#">30 seconds</a></li>
<li><a class="dropdown-item set-interval" data-interval="60000" href="#">60 seconds</a></li>
<li><a class="dropdown-item set-interval" data-interval="off" href="#">Off</a></li>
</ul>
</div>
</div>
</div>
<!-- End Breadcrumb -->
<div class="container">
<div class="main-body">
<div id="alertContainer" class="container mt-3">
<!-- Alerts will be dynamically inserted here -->
</div>
<div class="row" id="printerContainer">
<!-- Printer cards will be dynamically inserted here -->
</div>
</div>
</div>
<!--end page content -->
</div>
</div>
<!--end page wrapper -->
<!--start overlay-->
<div class="overlay mobile-toggle-icon"></div>
<!--end overlay-->
<!--Start Back To Top Button-->
<a href="javaScript:;" class="back-to-top"><i class='bx bxs-up-arrow-alt'></i></a>
<!--End Back To Top Button-->
</div>
<!--end wrapper-->
<!-- search modal -->
<div class="modal" id="SearchModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable modal-fullscreen-md-down">
<div class="modal-content">
<div class="modal-header gap-2">
<div class="position-relative popup-search w-100">
<input class="form-control form-control-lg ps-5 border border-3 border-primary" type="search" placeholder="Search">
<span class="position-absolute top-50 search-show ms-3 translate-middle-y start-0 top-50 fs-4"><i class='bx bx-search'></i></span>
</div>
<button type="button" class="btn-close d-md-none" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
</div>
</div>
</div>
<!-- end search modal -->
<!-- Bootstrap JS -->
<script src="../assets/js/bootstrap.bundle.min.js"></script>
<!--plugins-->
<script src="../assets/js/jquery.min.js"></script>
<script src="../assets/plugins/simplebar/js/simplebar.min.js"></script>
<script src="../assets/plugins/metismenu/js/metisMenu.min.js"></script>
<script src="../assets/plugins/perfect-scrollbar/js/perfect-scrollbar.js"></script>
<script src="../assets/plugins/apexcharts-bundle/js/apexcharts.min.js"></script>
<!--app JS-->
<script src="../assets/js/app.js"></script>
<script src="../assets/js/index.js"></script>
<script src="../assets/plugins/peity/jquery.peity.min.js"></script>
<script>
$(".data-attributes span").peity("donut")
</script>
<script>
$(document).ready(function() {
let refreshInterval = 60000; // Default interval of 10s
let intervalHandler;
// Load printers asynchronously
async function loadPrinters() {
try {
const response = await $.ajax({
url: '../src/printers/getPrinters.php',
method: 'GET',
dataType: 'json',
cache: false
});
let container = $('#printerContainer');
if (response.status === 'success' && response.data.length > 0) {
response.data.forEach(function(printer) {
let existingCard = container.find(`.printer-card[data-serial="${printer.serialNumber}"]`);
let trayHTML = '';
printer.trays.forEach(tray => {
let trayColor = `#${tray.color.slice(0, 6)}`;
trayHTML += `
<div class="tray-slot text-center">
<div class="tray-circle" style="background-color: ${trayColor}; width: 40px; height: 40px; border-radius: 50%;"></div>
<p>Slot ${parseInt(tray.id) + 1}</p>
</div>`;
});
let printerCard = `
<div class="col-lg-4 printer-card" data-serial="${printer.serialNumber}">
<div class="card mb-4">
<div class="card-body text-center">
<img src="../assets/images/printer-icon.png" alt="Printer" class="rounded-circle p-1" width="110">
<div class="mt-3">
<h4>${printer.printerName}</h4>
<p class="text-secondary mb-1">Current Job: <span>${printer.jobName}</span></p>
<p class="text-muted font-size-sm">
Bed Temp: <span>${printer.bedTemp}°C</span> |
Nozzle Temp: <span>${printer.nozzleTemp}°C</span>
</p>
</div>
<div class="d-flex justify-content-center gap-4 mt-3">
${trayHTML}
</div>
<div class="mt-4 w-100">
<label for="humidityBar${printer.serialNumber}" class="form-label">AMS Humidity</label>
<div class="progress" style="height: 20px;">
<div id="humidityBar${printer.serialNumber}" class="progress-bar" role="progressbar"
style="width: ${printer.humidity * 20}%;"
aria-valuemin="1"
aria-valuemax="5">
${printer.humidity}/5
</div>
</div>
</div>
</div>
</div>
</div>`;
if (existingCard.length) {
existingCard.replaceWith(printerCard);
} else {
container.append(printerCard);
}
});
}
} catch (error) {
console.log('Failed to load printers:', error);
}
}
// Polling function
function startPolling(interval) {
clearInterval(intervalHandler);
if (interval !== 'off') {
intervalHandler = setInterval(loadPrinters, interval);
}
}
// Handle refresh dropdown
$('.set-interval').on('click', function(e) {
e.preventDefault();
let selectedInterval = $(this).data('interval');
if (selectedInterval === 'off') {
clearInterval(intervalHandler);
console.log("Auto-refresh turned off.");
} else {
refreshInterval = parseInt(selectedInterval);
startPolling(refreshInterval);
console.log(`Refresh interval set to ${refreshInterval / 1000} seconds.`);
}
$('#selectedIntervalText').text(selectedInterval === 'off' ? 'Off' : `${selectedInterval / 1000}s`);
});
loadPrinters();
startPolling(refreshInterval); // Start polling immediately
});
</script>
</body>
</html>

View File

@@ -1,8 +1,11 @@
<?php
$host = '172.16.18.3';
$db = 'TechOdysseyDashboard';
$user = 'tod_admin';
$pass = 'QprczJwYor./_.T*';
require_once 'envLoader.php';
loadEnv(__DIR__ . '/../.env');
$host = $_ENV['DB_HOST'];
$db = $_ENV['DB_NAME'];
$user = $_ENV['DB_USER'];
$pass = $_ENV['DB_PASS'];
try {
$pdo = new PDO("mysql:host=$host;dbname=$db", $user, $pass);

View File

@@ -0,0 +1,162 @@
<?php
use Twig\Environment;
use Twig\Error\LoaderError;
use Twig\Error\RuntimeError;
use Twig\Extension\CoreExtension;
use Twig\Extension\SandboxExtension;
use Twig\Markup;
use Twig\Sandbox\SecurityError;
use Twig\Sandbox\SecurityNotAllowedTagError;
use Twig\Sandbox\SecurityNotAllowedFilterError;
use Twig\Sandbox\SecurityNotAllowedFunctionError;
use Twig\Source;
use Twig\Template;
use Twig\TemplateWrapper;
/* filament_price_summary.html.twig */
class __TwigTemplate_fb2b23b970fb7474e15dec31809a1a27 extends Template
{
private Source $source;
/**
* @var array<string, Template>
*/
private array $macros = [];
public function __construct(Environment $env)
{
parent::__construct($env);
$this->source = $this->getSourceContext();
$this->parent = false;
$this->blocks = [
];
}
protected function doDisplay(array $context, array $blocks = []): iterable
{
$macros = $this->macros;
// line 1
yield "<!DOCTYPE html>
<html lang=\"en\">
<head>
<meta charset=\"UTF-8\">
<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">
<link href=\"https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css\" rel=\"stylesheet\">
<title>Filament Price Change Summary</title>
<style>
.table-container {
overflow-x: auto;
}
.price-drop {
color: #28a745; /* Green for price drop */
font-weight: bold;
}
.price-rise {
color: #dc3545; /* Red for price rise */
font-weight: bold;
}
</style>
</head>
<body>
<div class=\"container py-5\">
<h1 class=\"mb-4\">Filament Price Change Summary</h1>
<p class=\"lead\">Here's the latest update on filament prices from your tracker:</p>
<div class=\"table-container\">
<table class=\"table table-striped table-bordered\">
<thead class=\"thead-dark\">
<tr>
<th>Filament Name</th>
<th>Brand</th>
<th>New Price</th>
<th>Old Price</th>
<th>Change</th>
<th>Amazon Link</th>
</tr>
</thead>
<tbody>
";
// line 39
$context['_parent'] = $context;
$context['_seq'] = CoreExtension::ensureTraversable(($context["filaments"] ?? null));
foreach ($context['_seq'] as $context["_key"] => $context["filament"]) {
// line 40
yield " <tr>
<td>";
// line 41
yield $this->env->getRuntime('Twig\Runtime\EscaperRuntime')->escape(CoreExtension::getAttribute($this->env, $this->source, $context["filament"], "filamentName", [], "any", false, false, false, 41), "html", null, true);
yield "</td>
<td>";
// line 42
yield $this->env->getRuntime('Twig\Runtime\EscaperRuntime')->escape(CoreExtension::getAttribute($this->env, $this->source, $context["filament"], "brand", [], "any", false, false, false, 42), "html", null, true);
yield "</td>
<td>";
// line 43
yield $this->env->getRuntime('Twig\Runtime\EscaperRuntime')->escape(CoreExtension::getAttribute($this->env, $this->source, $context["filament"], "newPrice", [], "any", false, false, false, 43), "html", null, true);
yield "</td>
<td>";
// line 44
yield $this->env->getRuntime('Twig\Runtime\EscaperRuntime')->escape(CoreExtension::getAttribute($this->env, $this->source, $context["filament"], "oldPrice", [], "any", false, false, false, 44), "html", null, true);
yield "</td>
<td class=\"price-drop\">";
// line 45
yield $this->env->getRuntime('Twig\Runtime\EscaperRuntime')->escape(CoreExtension::getAttribute($this->env, $this->source, $context["filament"], "priceChange", [], "any", false, false, false, 45), "html", null, true);
yield "</td>
<td><a href=\"";
// line 46
yield $this->env->getRuntime('Twig\Runtime\EscaperRuntime')->escape(CoreExtension::getAttribute($this->env, $this->source, $context["filament"], "amazonUrl", [], "any", false, false, false, 46), "html", null, true);
yield "\" target=\"_blank\">View on Amazon</a></td>
</tr>
";
}
$_parent = $context['_parent'];
unset($context['_seq'], $context['_key'], $context['filament'], $context['_parent']);
$context = array_intersect_key($context, $_parent) + $_parent;
// line 49
yield " </tbody>
</table>
</div>
<p class=\"mt-4\">
Visit your <a href=\"";
// line 53
yield $this->env->getRuntime('Twig\Runtime\EscaperRuntime')->escape(($context["dashboardUrl"] ?? null), "html", null, true);
yield "\" target=\"_blank\">dashboard</a> for more details.
</p>
</div>
</body>
</html>
";
yield from [];
}
/**
* @codeCoverageIgnore
*/
public function getTemplateName(): string
{
return "filament_price_summary.html.twig";
}
/**
* @codeCoverageIgnore
*/
public function isTraitable(): bool
{
return false;
}
/**
* @codeCoverageIgnore
*/
public function getDebugInfo(): array
{
return array ( 124 => 53, 118 => 49, 109 => 46, 105 => 45, 101 => 44, 97 => 43, 93 => 42, 89 => 41, 86 => 40, 82 => 39, 42 => 1,);
}
public function getSourceContext(): Source
{
return new Source("", "filament_price_summary.html.twig", "/mnt/www-live/TechOdyssey_Designs_Dashboard/src/emailService/templates/filament_price_summary.html.twig");
}
}

View File

@@ -0,0 +1,9 @@
body {
font-family: Arial, sans-serif;
background-color: #f9f9f9;
color: #333;
}
.alert {
border-radius: 10px;
}

View File

@@ -0,0 +1,54 @@
<?php
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\Exception;
require_once '../envLoader.php';
loadEnv('../../.env');
require '../../vendor/autoload.php';
function sendEmail($to, $subject, $template, $variables = []) {
$mail = new PHPMailer(true);
try {
// Load the template
$templatePath = __DIR__ . "/templates/$template.html";
if (!file_exists($templatePath)) {
throw new Exception("Template not found: $template");
}
$body = file_get_contents($templatePath);
// Replace variables
foreach ($variables as $key => $value) {
print_r($key);
$body = str_replace("{{" . $key . "}}", strval($value), $body);
}
// Add environment-based dynamic replacements
$body = str_replace("{{website_url}}", strval($_ENV['WEBSITE_URL']), $body);
$body = str_replace("{{business_name}}", strval($_ENV['BUSINESS_NAME']), $body);
// SMTP Configuration
$mail->isSMTP();
$mail->Host = $_ENV['SMTP_HOST'];
$mail->SMTPAuth = true;
$mail->Username = $_ENV['SMTP_USER'];
$mail->Password = $_ENV['SMTP_PASS'];
$mail->SMTPSecure = PHPMailer::ENCRYPTION_SMTPS;
$mail->Port = $_ENV['SMTP_PORT'];
// Email Settings
$mail->setFrom($_ENV['SMTP_USER'], $_ENV['BUSINESS_NAME']);
$mail->addAddress($to);
$mail->Subject = $subject;
$mail->isHTML(true);
$mail->Body = $body;
$mail->send();
echo "Email sent successfully to $to";
} catch (Exception $e) {
echo "Error: {$mail->ErrorInfo}";
}
}
?>

View File

@@ -0,0 +1,60 @@
<?php
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\Exception;
use Twig\Environment;
use Twig\Loader\FilesystemLoader;
require_once '../envLoader.php';
loadEnv('../../.env');
require '../../vendor/autoload.php';
function sendEmail($to, $subject, $template, $variables = []) {
$mail = new PHPMailer(true);
try {
// Set up Twig
$loader = new FilesystemLoader(__DIR__ . '/templates');
$twig = new Environment($loader, [
'cache' => __DIR__ . '/cache', // Optional: Enable Twig cache
'auto_reload' => true,
]);
// Render the Twig template
$body = $twig->render($template . '.html.twig', $variables);
// SMTP Configuration
$mail->isSMTP();
$mail->Host = $_ENV['SMTP_HOST'];
$mail->SMTPAuth = true;
$mail->Username = $_ENV['SMTP_USER'];
$mail->Password = $_ENV['SMTP_PASS'];
$mail->SMTPSecure = PHPMailer::ENCRYPTION_SMTPS;
$mail->Port = $_ENV['SMTP_PORT'];
// Email Charset and Settings
$mail->CharSet = 'UTF-8'; // Ensure correct encoding
$mail->setFrom($_ENV['SMTP_USER'], $_ENV['BUSINESS_NAME']);
// Handle multiple recipients
if (is_array($to)) {
foreach ($to as $recipient) {
$mail->addAddress($recipient);
}
} else {
$mail->addAddress($to); // Single recipient
}
$mail->Subject = $subject;
$mail->isHTML(true);
$mail->Body = $body;
$mail->send();
foreach ($to as $recipient) {
echo "Email sent successfully to $recipient\n";
}
} catch (Exception $e) {
echo "Error: {$mail->ErrorInfo}";
}
}
?>

View File

@@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<title>Error Alert</title>
</head>
<body>
<div class="container py-5">
<div class="alert alert-danger" role="alert">
<h4 class="alert-heading">Error!</h4>
<p>{{message}}</p>
<hr>
<p class="mb-0">For more details, visit our <a href="https://yourwebsite.com">dashboard</a>.</p>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<title>Info Alert</title>
</head>
<body>
<div class="container py-5">
<div class="alert alert-info" role="alert">
<h4 class="alert-heading">Information</h4>
<p>{{message}}</p>
<hr>
<p class="mb-0">For more details, visit our <a href="https://yourwebsite.com">dashboard</a>.</p>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<title>Success Alert</title>
</head>
<body>
<div class="container py-5">
<div class="alert alert-success" role="alert">
<h4 class="alert-heading">Success!</h4>
<p>{{message}}</p>
<hr>
<p class="mb-0">For more details, visit our <a href="https://yourwebsite.com">dashboard</a>.</p>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,57 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<title>Filament Price Change Summary</title>
<style>
.table-container {
overflow-x: auto;
}
.price-drop {
color: #28a745; /* Green for price drop */
font-weight: bold;
}
.price-rise {
color: #dc3545; /* Red for price rise */
font-weight: bold;
}
</style>
</head>
<body>
<div class="container py-5">
<h1 class="mb-4">Filament Price Change Summary</h1>
<p class="lead">Here's the latest update on filament prices from your tracker:</p>
<div class="table-container">
<table class="table table-striped table-bordered">
<thead class="thead-dark">
<tr>
<th>Filament Name</th>
<th>Brand</th>
<th>New Price</th>
<th>Old Price</th>
<th>Change</th>
<th>Amazon Link</th>
</tr>
</thead>
<tbody>
{% for filament in filaments %}
<tr>
<td>{{ filament.filamentName }}</td>
<td>{{ filament.brand }}</td>
<td>{{ filament.newPrice }}</td>
<td>{{ filament.oldPrice }}</td>
<td class="price-drop">{{ filament.priceChange }}</td>
<td><a href="{{ filament.amazonUrl }}" target="_blank">View on Amazon</a></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<p class="mt-4">
Visit your <a href="{{ dashboardUrl }}" target="_blank">dashboard</a> for more details.
</p>
</div>
</body>
</html>

15
src/envLoader.php Normal file
View File

@@ -0,0 +1,15 @@
<?php
function loadEnv($path) {
if (!file_exists($path)) {
throw new Exception('.env file not found.');
}
$lines = file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {
if (strpos($line, '=') !== false) {
list($key, $value) = explode('=', $line, 2);
$_ENV[$key] = trim($value);
}
}
}
?>

View File

@@ -0,0 +1,25 @@
<?php
include '../session_check.php';
require '../envLoader.php'; // Load envLoader from php/
loadEnv(__DIR__ . '/../../.env'); // Go up three levels to root for .env
$clientId = $_ENV['ETSY_KEYSTRING'];
$redirectUri = $_ENV['ETSY_REDIRECT_URI'];
$scope = 'transactions_r';
$state = bin2hex(random_bytes(16));
session_start();
$_SESSION['oauth_state'] = $state;
// Authorization URL
$url = "https://www.etsy.com/oauth/connect?" . http_build_query([
'response_type' => 'code',
'client_id' => $clientId,
'redirect_uri' => $redirectUri,
'scope' => $scope,
'state' => $state
]);
header("Location: $url");
exit;
?>

View File

@@ -0,0 +1,33 @@
<?php
include '../session_check.php';
require '../envLoader.php'; // Load envLoader from php/
loadEnv(__DIR__ . '/../../../.env'); // Go up three levels to find .env
session_start();
if ($_GET['state'] !== $_SESSION['oauth_state']) {
die('Invalid state. Possible CSRF attack.');
}
$code = $_GET['code'];
$clientId = $_ENV['ETSY_KEYSTRING'];
$clientSecret = $_ENV['ETSY_SECRET'];
$redirectUri = $_ENV['ETSY_REDIRECT_URI'];
$data = [
'grant_type' => 'authorization_code',
'client_id' => $clientId,
'client_secret' => $clientSecret,
'code' => $code,
'redirect_uri' => $redirectUri
];
$ch = curl_init("https://api.etsy.com/v3/public/oauth/token");
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
$response = curl_exec($ch);
$tokens = json_decode($response, true);
file_put

View File

@@ -0,0 +1,27 @@
<?php
include '../session_check.php';
require '../envLoader.php'; // Load envLoader from php/
loadEnv(__DIR__ . '/../../../.env'); // Go up three levels to find .env
function refreshAccessToken() {
$tokens = json_decode(file_get_contents('etsyTokens.json'), true);
$refreshToken = $tokens['refresh_token'];
$data = [
'grant_type' => 'refresh_token',
'client_id' => $_ENV['ETSY_KEYSTRING'],
'client_secret' => $_ENV['ETSY_SECRET'],
'refresh_token' => $refreshToken
];
$ch = curl_init("https://api.etsy.com/v3/public/oauth/token");
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
$response = curl_exec($ch);
$newTokens = json_decode($response, true);
file_put_contents('etsyTokens.json', json_encode($newTokens));
echo "Token refreshed successfully.";
}
?>

View File

@@ -0,0 +1,122 @@
<?php
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
require '../../vendor/autoload.php';
require '../db.php';
require_once '../envLoader.php';
require 'scraper.php'; // Include the scraper file
loadEnv(__DIR__ . '/../../.env');
include '../session_check.php';
checkUserRole(['admin']);
// Start session to get user ID
if (!isset($_SESSION['userId'])) {
echo json_encode(['status' => 'error', 'message' => 'User not authenticated.']);
exit;
}
// Check if the request method is POST
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$filamentName = $_POST['productName'] ?? '';
$amazonUrl = $_POST['productUrl'] ?? '';
$userId = $_SESSION['userId'];
// Validate the input
if (empty($filamentName) || empty($amazonUrl)) {
echo json_encode(['status' => 'error', 'message' => 'Filament name and URL are required.']);
exit;
}
try {
// Scrape data from Amazon
$scrapedData = scrapeAmazonData($amazonUrl);
if (!$scrapedData) {
throw new Exception('Scraping failed for the provided URL.');
}
$price = $scrapedData['price'];
$currentDiscount = json_encode($scrapedData['currentDiscount']); // Convert discount to JSON
$details = $scrapedData['details'] ?? [];
// Extract details or use defaults
$brand = $details['Brand'] ?? 'Unknown';
$material = $details['Material'] ?? 'Unknown';
$color = $details['Colour'] ?? 'Unknown';
$filamentWeight = isset($details['Item weight']) ? preg_replace('/[^0-9.]/', '', $details['Item weight']) : 1;
if (stripos($details['Item weight'] ?? '', 'kilograms') !== false || stripos($details['Item weight'] ?? '', 'kg') !== false) {
$filamentWeight *= 1000; // Convert KG to G
}
$itemDiameter = isset($details['Item diameter']) ? preg_replace('/[^0-9.]/', '', $details['Item diameter']) : 1.75;
// Begin transaction for database operations
$pdo->beginTransaction();
// Check if the filament already exists in the database
$stmt = $pdo->prepare("SELECT id FROM filamentTracker WHERE amazonUrl = :amazonUrl");
$stmt->execute([':amazonUrl' => $amazonUrl]);
$existingFilament = $stmt->fetch(PDO::FETCH_ASSOC);
if ($existingFilament) {
// If the filament exists, update the price and discount
$filamentId = $existingFilament['id'];
$stmt = $pdo->prepare("
INSERT INTO filamentPriceHistory (filamentId, price, currentDiscount)
VALUES (:filamentId, :price, :currentDiscount)
");
$stmt->execute([
':filamentId' => $filamentId,
':price' => $price,
':currentDiscount' => $currentDiscount
]);
$message = 'Filament price and discount updated successfully.';
} else {
// If the filament does not exist, insert it into filamentTracker
$stmt = $pdo->prepare("
INSERT INTO filamentTracker (userId, filamentName, amazonUrl, filamentWeight, brand, material, color, itemDiameter)
VALUES (:userId, :filamentName, :amazonUrl, :filamentWeight, :brand, :material, :color, :itemDiameter)
");
$stmt->execute([
':userId' => $userId,
':filamentName' => $filamentName,
':amazonUrl' => $amazonUrl,
':filamentWeight' => $filamentWeight,
':brand' => $brand,
':material' => $material,
':color' => $color,
':itemDiameter' => $itemDiameter
]);
// Get the ID of the newly inserted filament
$filamentId = $pdo->lastInsertId();
// Insert the initial price and discount into filamentPriceHistory
$stmt = $pdo->prepare("
INSERT INTO filamentPriceHistory (filamentId, price, currentDiscount)
VALUES (:filamentId, :price, :currentDiscount)
");
$stmt->execute([
':filamentId' => $filamentId,
':price' => $price,
':currentDiscount' => $currentDiscount
]);
$message = "$filamentName added successfully and price with discount tracked.";
}
// Commit the transaction
$pdo->commit();
echo json_encode(['status' => 'success', 'message' => $message]);
} catch (Exception $e) {
// Rollback transaction if any exception occurs
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
echo json_encode(['status' => 'error', 'message' => $e->getMessage()]);
}
} else {
echo json_encode(['status' => 'error', 'message' => 'Invalid request method.']);
}
?>

View File

@@ -0,0 +1,82 @@
<?php
require '../../vendor/autoload.php';
require '../db.php';
require_once '../envLoader.php';
loadEnv(__DIR__ . '/../../.env');
include '../src/session_check.php';
header('Content-Type: application/json');
try {
// Query 1: Fetch filament details and price history
$stmt1 = $pdo->query("
SELECT ft.id AS filamentId,
ft.filamentName,
ft.brand,
ft.material,
ft.color,
ft.amazonUrl,
fp.price,
fp.recordedAt
FROM filamentTracker ft
JOIN filamentPriceHistory fp ON ft.id = fp.filamentId
ORDER BY ft.filamentName ASC, fp.recordedAt ASC;
");
$filaments = $stmt1->fetchAll(PDO::FETCH_ASSOC);
// Query 2: Fetch the latest discount for each filament
$stmt2 = $pdo->query("
SELECT filamentId,
JSON_OBJECT(
'discount', JSON_OBJECT('value', MAX(JSON_EXTRACT(currentDiscount, '$.discount.value')), 'type', 'percentage'),
'voucher', JSON_OBJECT('value', MAX(JSON_EXTRACT(currentDiscount, '$.voucher.value')), 'type', MAX(JSON_UNQUOTE(JSON_EXTRACT(currentDiscount, '$.voucher.type'))))
) AS currentDiscount
FROM filamentPriceHistory
GROUP BY filamentId;
");
$discounts = $stmt2->fetchAll(PDO::FETCH_ASSOC);
// Create a mapping of filamentId to discounts
$discountMap = [];
foreach ($discounts as $discount) {
$discountMap[$discount['filamentId']] = json_decode($discount['currentDiscount'], true);
}
// Format data for response
$result = [];
foreach ($filaments as $filament) {
$name = $filament['filamentName'];
if (!isset($result[$name])) {
$result[$name] = [
'brand' => $filament['brand'],
'material' => $filament['material'],
'color' => $filament['color'],
'amazonUrl' => $filament['amazonUrl'],
'prices' => [],
'currentDiscount' => $discountMap[$filament['filamentId']] ?? [
'discount' => ['value' => 0, 'type' => 'none'],
'voucher' => ['value' => 0, 'type' => 'none']
]
];
}
$result[$name]['prices'][] = [
'price' => (float)$filament['price'],
'recordedAt' => $filament['recordedAt']
];
}
// Return as JSON
echo json_encode([
'status' => 'success',
'data' => $result
], JSON_PRETTY_PRINT);
} catch (PDOException $e) {
echo json_encode([
'status' => 'error',
'message' => 'Failed to fetch filament prices: ' . $e->getMessage()
]);
}
?>

View File

@@ -0,0 +1,132 @@
<?php
require '../../vendor/autoload.php';
require '../db.php';
require_once '../envLoader.php';
loadEnv('../../.env');
require_once '../emailService/sendPriceDropEmailService.php';
// Fetch list of all filaments being tracked
function getFilaments() {
global $pdo;
try {
$stmt = $pdo->query("SELECT id, filamentName, brand, amazonUrl FROM filamentTracker");
return $stmt->fetchAll(PDO::FETCH_ASSOC);
} catch (PDOException $e) {
echo "Error fetching filaments: " . $e->getMessage();
return [];
}
}
// Fetch the latest prices for each filament
function getCurrentPrices() {
global $pdo;
try {
$stmt = $pdo->query("
SELECT fp.filamentId, fp.price, fp.recordedAt
FROM filamentPriceHistory fp
INNER JOIN (
SELECT filamentId, MAX(recordedAt) AS latestRecordedAt
FROM filamentPriceHistory
GROUP BY filamentId
) latest ON fp.filamentId = latest.filamentId AND fp.recordedAt = latest.latestRecordedAt
");
return $stmt->fetchAll(PDO::FETCH_ASSOC);
} catch (PDOException $e) {
echo "Error fetching current prices: " . $e->getMessage();
return [];
}
}
// Fetch prices from the last 24 hours for each filament
function getPriceHistoryForLast24Hours() {
global $pdo;
try {
$stmt = $pdo->query("
SELECT filamentId, price, recordedAt
FROM filamentPriceHistory
WHERE recordedAt >= NOW() - INTERVAL 1 DAY
ORDER BY filamentId, recordedAt DESC
");
return $stmt->fetchAll(PDO::FETCH_ASSOC);
} catch (PDOException $e) {
echo "Error fetching price history for the last 24 hours: " . $e->getMessage();
return [];
}
}
// Compare prices and find drops
function checkPriceDropsAndNotify() {
$filaments = getFilaments();
$currentPrices = getCurrentPrices();
$priceHistory = getPriceHistoryForLast24Hours();
// Index prices by filamentId for quick lookup
$currentPricesMap = [];
foreach ($currentPrices as $current) {
$currentPricesMap[$current['filamentId']] = $current['price'];
}
// Group price history by filamentId
$priceHistoryMap = [];
foreach ($priceHistory as $history) {
$priceHistoryMap[$history['filamentId']][] = $history['price'];
}
$priceDrops = [];
foreach ($filaments as $filament) {
$filamentId = $filament['id'];
$currentPrice = $currentPricesMap[$filamentId] ?? null;
if ($currentPrice !== null && isset($priceHistoryMap[$filamentId])) {
foreach ($priceHistoryMap[$filamentId] as $oldPrice) {
if ($currentPrice < (float)$oldPrice) {
$priceDrops[] = [
'filamentName' => $filament['filamentName'],
'brand' => $filament['brand'] ?: 'Unknown',
'newPrice' => "£" . number_format($currentPrice, 2),
'oldPrice' => "£" . number_format((float)$oldPrice, 2),
'priceChange' => "" . number_format((float)$oldPrice - $currentPrice, 2),
'amazonUrl' => $filament['amazonUrl']
];
break; // Stop checking once a price drop is detected
}
}
}
}
function getRecipients() {
global $pdo;
try {
$stmt = $pdo->query("SELECT `email` FROM `users` WHERE `alertEmails` = 1");
return $stmt->fetchAll(PDO::FETCH_COLUMN);
} catch (PDOException $e) {
echo "Error fetching recipients: " . $e->getMessage();
return [];
}
}
// Send email if there are price drops
if (!empty($priceDrops)) {
$recipient = getRecipients();
$subject = 'Filament Price Change Summary';
$data = [
'filaments' => $priceDrops,
'dashboardUrl' => $_ENV['WEBSITE_URL'] . '/dashboard'
];
sendEmail($recipient, $subject, 'filament_price_summary', $data);
echo "Email sent for the following price drops:\n";
foreach ($priceDrops as $drop) {
echo "- {$drop['filamentName']}: Price dropped from {$drop['oldPrice']} to {$drop['newPrice']}\n";
}
} else {
echo "No price drops detected.\n";
}
}
checkPriceDropsAndNotify();
?>

View File

@@ -0,0 +1,67 @@
<?php
require '/mnt/www-live/TechOdyssey_Designs_Dashboard/vendor/autoload.php';
use Goutte\Client;
function scrapeAmazonData($url) {
$client = new Client();
try {
$crawler = $client->request('GET', $url);
// Filter by `centerCol`
$centerCol = $crawler->filter('#centerCol');
// Scrape price
$whole = $centerCol->filter('.a-price-whole')->count() ? $centerCol->filter('.a-price-whole')->text() : '0';
$fraction = $centerCol->filter('.a-price-fraction')->count() ? $centerCol->filter('.a-price-fraction')->text() : '00';
$whole = preg_replace('/[^0-9]/', '', $whole);
$fraction = preg_replace('/[^0-9]/', '', $fraction);
$fraction = strlen($fraction) === 1 ? $fraction . '0' : substr($fraction, 0, 2);
$totalPrice = floatval($whole . '.' . $fraction);
// Scrape discount
$discount = $centerCol->filter('.savingsPercentage')->count()
? preg_replace('/[^0-9]/', '', $centerCol->filter('.savingsPercentage')->text())
: 0;
// Scrape voucher
$voucherText = '';
$centerCol->filter('.promoPriceBlockMessage')->each(function ($node) use (&$voucherText) {
$text = $node->text();
if (preg_match('/Apply (\d+%|£\d+) voucher/', $text, $voucherMatch)) {
$voucherText = $voucherMatch[1];
}
});
// Parse voucher
$voucherValue = 0;
$voucherType = null;
if (!empty($voucherText)) {
if (strpos($voucherText, '%') !== false) {
$voucherValue = (int) preg_replace('/[^0-9]/', '', $voucherText); // Extract percentage
$voucherType = 'percentage';
} elseif (strpos($voucherText, '£') !== false) {
$voucherValue = (float) preg_replace('/[^0-9.]/', '', $voucherText); // Extract £ value
$voucherType = 'fixed';
}
}
return [
'price' => $totalPrice,
'currentDiscount' => [
'discount' => [
'value' => $discount,
'type' => 'percentage'
],
'voucher' => [
'value' => $voucherValue,
'type' => $voucherType
]
],
'details' => [] // Placeholder for additional details if needed
];
} catch (Exception $e) {
return null; // Return null if scraping fails
}
}

View File

@@ -0,0 +1,53 @@
<?php
require '/mnt/www-live/TechOdyssey_Designs_Dashboard/vendor/autoload.php';
require '/mnt/www-live/TechOdyssey_Designs_Dashboard/src/db.php';
require_once '/mnt/www-live/TechOdyssey_Designs_Dashboard/src/envLoader.php';
require 'scraper.php';
loadEnv(__DIR__ . '/../../.env');
// Fetch all filaments
try {
$stmt = $pdo->query("SELECT * FROM filamentTracker");
$filaments = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($filaments as $filament) {
$amazonUrl = $filament['amazonUrl'];
$filamentId = $filament['id'];
// Use the scraper function to fetch price and discount
$scrapedData = scrapeAmazonData($amazonUrl);
if ($scrapedData && $scrapedData['price'] > 0) {
// Prepare currentDiscount JSON structure with the correct format
$currentDiscount = json_encode([
'discount' => [
'value' => $scrapedData['currentDiscount']['discount']['value'] ?? 0,
'type' => $scrapedData['currentDiscount']['discount']['type'] ?? 'percentage',
],
'voucher' => [
'value' => $scrapedData['currentDiscount']['voucher']['value'] ?? 0,
'type' => $scrapedData['currentDiscount']['voucher']['type'] ?? null,
],
]);
$stmt = $pdo->prepare("
INSERT INTO filamentPriceHistory (filamentId, price, currentDiscount)
VALUES (:filamentId, :price, :currentDiscount)
");
$stmt->execute([
':filamentId' => $filamentId,
':price' => $scrapedData['price'],
':currentDiscount' => $currentDiscount
]);
echo "Updated price for {$filament['filamentName']}: £{$scrapedData['price']}, Current Discount: {$currentDiscount}\n";
} else {
echo "Failed to update {$filament['filamentName']} (no price found or £0).\n";
}
// Add a small random delay between 1 to 5 seconds
sleep(rand(1, 5));
}
} catch (PDOException $e) {
echo "Database error: " . $e->getMessage();
}

104
src/header.php Normal file
View File

@@ -0,0 +1,104 @@
<header>
<div class="topbar">
<nav class="navbar navbar-expand gap-2 align-items-center">
<div class="mobile-toggle-menu d-flex"><i class='bx bx-menu'></i>
</div>
<div class="search-bar d-lg-block d-none" data-bs-toggle="modal" data-bs-target="#SearchModal">
<a href="avascript:;" class="btn d-flex align-items-center"><i class="bx bx-search"></i>Search</a>
</div>
<div class="top-menu ms-auto">
<ul class="navbar-nav align-items-center gap-1">
<li class="nav-item mobile-search-icon d-flex d-lg-none" data-bs-toggle="modal" data-bs-target="#SearchModal">
<a class="nav-link" href="avascript:;"><i class='bx bx-search'></i>
</a>
</li>
<li class="nav-item dark-mode d-none d-sm-flex">
<a class="nav-link dark-mode-icon" href="javascript:;"><i class='bx bx-moon'></i>
</a>
</li>
<li class="nav-item dropdown dropdown-large">
<a class="nav-link dropdown-toggle dropdown-toggle-nocaret position-relative" href="#" data-bs-toggle="dropdown"><span class="alert-count">7</span>
<i class='bx bx-bell'></i>
</a>
<div class="dropdown-menu dropdown-menu-end">
<a href="javascript:;">
<div class="msg-header">
<p class="msg-header-title">Notifications</p>
<p class="msg-header-badge">8 New</p>
</div>
</a>
<div class="header-notifications-list">
<a class="dropdown-item" href="javascript:;">
<div class="d-flex align-items-center">
<div class="user-online">
<img src="../assets/images/avatars/avatar-1.png" class="msg-avatar" alt="user avatar">
</div>
<div class="flex-grow-1">
<h6 class="msg-name">Daisy Anderson<span class="msg-time float-end">5 sec
ago</span></h6>
<p class="msg-info">The standard chunk of lorem</p>
</div>
</div>
</a>
</div>
<a href="javascript:;">
<div class="text-center msg-footer">
<button class="btn btn-primary w-100">View All Notifications</button>
</div>
</a>
</div>
</li>
</ul>
</div>
<div class="user-box dropdown px-3">
<a class="d-flex align-items-center nav-link dropdown-toggle gap-3 dropdown-toggle-nocaret" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
<img src="../assets/images/avatars/avatar-2.png" class="user-img" alt="user avatar">
<div class="user-info">
<p class="user-name mb-0"><?php echo isset($_SESSION['username']) ? htmlspecialchars($_SESSION['username']) : 'Guest'; ?></p>
<p class="designattion mb-0"><?php echo isset($_SESSION['role']) ? htmlspecialchars($_SESSION['role']) : 'N/A'; ?></p>
</div>
</a>
<ul class="dropdown-menu dropdown-menu-end">
<li><a class="dropdown-item d-flex align-items-center" href="javascript:;"><i class="bx bx-user fs-5"></i><span>Profile</span></a>
</li>
<li><a class="dropdown-item d-flex align-items-center" href="printerForm.php"><i class="bx bx-cog fs-5"></i><span>Add Printer</span></a>
</li>
<li><a class="dropdown-item d-flex align-items-center" href="userManagement.php"><i class="bx bx-cog fs-5"></i><span>user Management</span></a>
</li>
<li>
<div class="dropdown-divider mb-0"></div>
</li>
<li><a class="dropdown-item d-flex align-items-center" href="../public/logout.php"><i class="bx bx-log-out-circle"></i><span>Logout</span></a>
</li>
</ul>
</div>
</nav>
<?php if (isset($_SESSION['errorMessage'])): ?>
<div id="sessionAlert" class="alert alert-danger alert-dismissible fade show" role="alert">
<?php echo $_SESSION['errorMessage']; ?>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
setTimeout(function () {
let alertElement = document.getElementById('sessionAlert');
if (alertElement) {
alertElement.classList.remove('show');
alertElement.classList.add('fade');
alertElement.addEventListener('transitionend', function () {
alertElement.remove();
});
}
}, 4000);
// Clear the session variable after rendering
<?php unset($_SESSION['errorMessage']); ?>
});
</script>
<?php endif; ?>
</div>
</header>

View File

@@ -1,6 +1,6 @@
<?php
session_start();
require 'db.php';
require_once 'db.php';
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
$username = htmlspecialchars($_POST['username']);
@@ -12,7 +12,7 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
if ($user && password_verify($password, $user['password']) && !$user['disabled']) {
// Store user ID, username, and role in session
$_SESSION['user_id'] = $user['id'];
$_SESSION['userId'] = $user['id'];
$_SESSION['username'] = $user['username'];
$_SESSION['role'] = $user['role']; // Store user role
echo 'success';

80
src/nav.php Normal file
View File

@@ -0,0 +1,80 @@
<!--sidebar wrapper -->
<div class="sidebar-wrapper" data-simplebar="true">
<div class="sidebar-header">
<div>
<img src="../assets/images/logo-icon.png" class="logo-icon" alt="logo icon">
</div>
<div>
<h4 class="logo-text">TechOdyssey</h4>
</div>
<div class="mobile-toggle-icon ms-auto"><i class='bx bx-x'></i>
</div>
</div>
<!--navigation-->
<ul class="metismenu" id="menu">
<li>
<a href="index.php">
<div class="parent-icon"><i class='bx bx-home-alt'></i>
</div>
<div class="menu-title">Dashboard</div>
</a>
</li>
<!-- Shipping Section -->
<li class="menu-label">Shipping</li>
<li>
<a href="shipping.php">
<div class="parent-icon"><i class='bx bx-code-alt'></i>
</div>
<div class="menu-title">Shipping Dashboard</div>
</a>
</li>
<!-- Printing Section -->
<li class="menu-label">Printing Control</li>
<li>
<a href="viewPrinters.php">
<div class="parent-icon"><i class='bx bx-code-alt'></i>
</div>
<div class="menu-title">Printer Dashboard</div>
</a>
</li>
<li>
<a href="filamentDryer.php">
<div class="parent-icon"><i class='bx bx-plus-circle'></i>
</div>
<div class="menu-title">Dryer Control</div>
</a>
</li>
<!-- Filament Tracking Section -->
<li class="menu-label">Filament Tracker</li>
<li>
<a href="viewFilament.php">
<div class="parent-icon"><i class='bx bx-layer'></i>
</div>
<div class="menu-title">Filament Overview</div>
</a>
</li>
<li>
<a href="addFilament.php">
<div class="parent-icon"><i class='bx bx-plus-circle'></i>
</div>
<div class="menu-title">Add Filament</div>
</a>
</li>
<!-- Settings or Admin Section -->
<li class="menu-label">Settings</li>
<li>
<a href="settings.php">
<div class="parent-icon"><i class='bx bx-cog'></i>
</div>
<div class="menu-title">User Settings</div>
</a>
</li>
</ul>
<!--end navigation-->
</div>
<!--end sidebar wrapper -->

View File

@@ -0,0 +1,53 @@
<?php
session_start(); // Ensure session is started to get logged-in user ID
require '../db.php';
require_once '../envLoader.php';
loadEnv(__DIR__ . '/../../.env');
header('Content-Type: application/json');
// Check if the user is logged in
if (!isset($_SESSION['userId'])) {
echo json_encode(['status' => 'error', 'message' => 'User not authenticated.']);
exit;
}
$userId = $_SESSION['userId']; // Retrieve user ID from session
// Handle POST Request
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Collect and sanitize form data
$printerName = filter_input(INPUT_POST, 'printerName', FILTER_SANITIZE_STRING);
$printerIp = filter_input(INPUT_POST, 'printerIp', FILTER_VALIDATE_IP);
$serialNumber = filter_input(INPUT_POST, 'serialNumber', FILTER_SANITIZE_STRING);
$accessCode = filter_input(INPUT_POST, 'accessCode', FILTER_SANITIZE_STRING);
if (!$printerName || !$printerIp || !$serialNumber || !$accessCode) {
echo json_encode(['status' => 'error', 'message' => 'All fields are required and IP must be valid.']);
exit;
}
try {
// Prepare SQL insert statement with user ID
$stmt = $pdo->prepare("
INSERT INTO bambuPrinters (printerName, printerIp, mqttPassword, serialNumber, userId)
VALUES (:printerName, :printerIp, :mqttPassword, :serialNumber, :userId)
");
// Execute the query with bound parameters
$stmt->execute([
':printerName' => $printerName,
':printerIp' => $printerIp,
':mqttPassword' => $accessCode,
':serialNumber' => $serialNumber,
':userId' => $userId
]);
echo json_encode(['status' => 'success', 'message' => 'Printer successfully added!']);
} catch (PDOException $e) {
echo json_encode(['status' => 'error', 'message' => 'Error adding printer: ' . $e->getMessage()]);
}
} else {
echo json_encode(['status' => 'error', 'message' => 'Invalid request method.']);
}
?>

View File

@@ -0,0 +1,79 @@
<?php
session_start();
require '../db.php';
header('Content-Type: application/json');
if (!isset($_SESSION['userId'])) {
echo json_encode(['status' => 'error', 'message' => 'User not authenticated.']);
exit;
}
$userId = $_SESSION['userId'];
try {
// Subquery to fetch latest telemetry for each printer
$stmt = $pdo->prepare("
SELECT p.*, t.telemetry, t.recordedAt
FROM bambuPrinters p
LEFT JOIN printerTelemetry t
ON t.id = (
SELECT id FROM printerTelemetry
WHERE printerId = p.id
ORDER BY recordedAt DESC
LIMIT 1
)
WHERE p.userId = :userId
");
$stmt->execute([':userId' => $userId]);
$printers = $stmt->fetchAll(PDO::FETCH_ASSOC);
$result = [];
foreach ($printers as $printer) {
$telemetry = json_decode($printer['telemetry'], true);
$bedTemp = isset($telemetry['print']['bed_temper']) ? $telemetry['print']['bed_temper'] : 'N/A';
$nozzleTemp = isset($telemetry['print']['device']['nozzle']['0']['temp'])
? $telemetry['print']['device']['nozzle']['0']['temp']
: 'N/A';
$jobName = isset($telemetry['subtask_name']) ? $telemetry['subtask_name'] : 'No job running';
$status = isset($telemetry['print']['gcode_state']) ? $telemetry['print']['gcode_state'] : 'Unknown';
// AMS Data
$humidity = isset($telemetry['print']['ams']['ams'][0]['humidity'])
? $telemetry['print']['ams']['ams'][0]['humidity']
: 0;
$trays = isset($telemetry['print']['ams']['ams'][0]['tray'])
? $telemetry['print']['ams']['ams'][0]['tray']
: [];
// Structure tray data for direct output
$trayData = [];
foreach ($trays as $tray) {
$trayData[] = [
'id' => $tray['id'],
'color' => isset($tray['tray_color']) ? $tray['tray_color'] : 'ccc'
];
}
$result[] = [
'printerId' => $printer['id'],
'printerName' => $printer['printerName'],
'serialNumber' => $printer['serialNumber'],
'printerIp' => $printer['printerIp'],
'bedTemp' => $bedTemp,
'nozzleTemp' => $nozzleTemp,
'jobName' => $jobName,
'status' => $status,
'humidity' => $humidity,
'trays' => $trayData,
'recordedAt' => $printer['recordedAt'] ?? 'Never'
];
}
echo json_encode(['status' => 'success', 'data' => $result]);
} catch (PDOException $e) {
echo json_encode(['status' => 'error', 'message' => 'Failed to fetch printers: ' . $e->getMessage()]);
}
?>

View File

@@ -0,0 +1,105 @@
<?php
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
require '/mnt/www-live/TechOdyssey_Designs_Dashboard/vendor/autoload.php';
require '/mnt/www-live/TechOdyssey_Designs_Dashboard/src/db.php';
require_once '/mnt/www-live/TechOdyssey_Designs_Dashboard/src/envLoader.php';
loadEnv(__DIR__ . '/../../.env');
use PhpMqtt\Client\MqttClient;
use PhpMqtt\Client\ConnectionSettings;
session_start();
// Enable asynchronous signal handling
pcntl_async_signals(true);
// MQTT Configuration from .env
$mqttPort = $_ENV['MQTT_PORT'];
$mqttUsername = $_ENV['MQTT_USERNAME'];
$clientId = 'printer_monitor_' . uniqid();
// Fetch printers associated with the logged-in user
try {
$stmt = $pdo->prepare("SELECT * FROM bambuPrinters");
$stmt->execute();
$printers = $stmt->fetchAll(PDO::FETCH_ASSOC);
} catch (PDOException $e) {
die(json_encode(['status' => 'error', 'message' => 'Failed to fetch printers.']));
}
// Process 2 messages per printer
$maxMessagesPerPrinter = 2;
try {
foreach ($printers as $printer) {
echo "Connecting to " . $printer['printerName'] . " (" . $printer['serialNumber'] . ")...\n";
echo "IP Address: " . $printer['printerIp'] . "\n";
echo "MQTT Password: " . $printer['mqttPassword'] . "\n";
$mqttPassword = $printer['mqttPassword']; // Use mqttPassword from DB
$printerIp = $printer['printerIp']; // Printer IP from DB
$messageCount = 0; // Reset counter per printer
$mqtt = new MqttClient($printerIp, $mqttPort, $clientId);
// Handle SIGINT (CTRL + C) to gracefully exit
pcntl_signal(SIGINT, function () use ($mqtt) {
echo "Interrupt received. Disconnecting MQTT...\n";
$mqtt->interrupt();
});
$connectionSettings = (new ConnectionSettings())
->setUsername($mqttUsername)
->setPassword($mqttPassword)
->setKeepAliveInterval(60)
->setUseTls(true)
->setTlsVerifyPeer(false)
->setTlsVerifyPeerName(false);
// Establish Connection
$mqtt->connect($connectionSettings, true);
// Subscribe to the printer's report topic
$topic = "device/{$printer['serialNumber']}/report";
$mqtt->subscribe($topic, function (string $topic, string $message) use (
$printer, $pdo, &$messageCount, $maxMessagesPerPrinter, $mqtt
) {
echo "[" . $printer['printerName'] . "] [$topic] $message\n";
// Store full JSON telemetry report
try {
$stmt = $pdo->prepare("
INSERT INTO printerTelemetry
(printerId, telemetry)
VALUES
(:printerId, :telemetry)
");
$stmt->execute([
':printerId' => $printer['id'],
':telemetry' => $message
]);
} catch (PDOException $e) {
error_log("Failed to insert telemetry: " . $e->getMessage());
}
$messageCount++;
// Stop loop after 2 messages for this printer
if ($messageCount >= $maxMessagesPerPrinter) {
echo "Processed $maxMessagesPerPrinter messages for " . $printer['printerName'] . ". Disconnecting...\n";
$mqtt->interrupt();
}
}, 0);
// Start Listening for Messages
$mqtt->loop(true); // Loop until interrupted after processing 2 messages
$mqtt->disconnect();
}
} catch (Exception $e) {
die(json_encode(['status' => 'error', 'message' => 'MQTT connection failed. ' . $e->getMessage()]));
}
?>

39
src/session_check.php Normal file
View File

@@ -0,0 +1,39 @@
<?php
session_start();
// Configuration for session timeout (in seconds)
define('SESSION_TIMEOUT', 1800); // 30 minutes
// Check if the user is logged in
if (!isset($_SESSION['userId'])) {
redirectToLogin("You must be logged in to access this page.");
}
// Session Timeout Check
if (isset($_SESSION['lastActivity']) && (time() - $_SESSION['lastActivity']) > SESSION_TIMEOUT) {
// Session expired
session_unset();
session_destroy();
redirectToLogin("Session expired. Please log in again.");
} else {
$_SESSION['lastActivity'] = time(); // Update activity timestamp
}
// Function to check user roles
function checkUserRole($allowedRoles = []) {
if (!isset($_SESSION['role']) || !in_array($_SESSION['role'], $allowedRoles)) {
$_SESSION['errorMessage'] = "Access denied: You do not have the required permissions.";
header("Location: dashboard.php"); // Redirect to dashboard or another page
exit();
}
}
// Function to redirect to login with optional message
function redirectToLogin($message = '') {
if (!empty($message)) {
$_SESSION['errorMessage'] = $message;
}
header("Location: login.php");
exit();
}
?>

View File

@@ -0,0 +1,74 @@
<?php
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
require_once '/mnt/www-live/TechOdyssey_Designs_Dashboard/src/envLoader.php';
loadEnv(__DIR__ . '/../../.env');
// Get API credentials from the environment
$client_id = $_ENV['P2G_CLIENT_ID'];
$client_secret = $_ENV['P2G_CLIENT_SECRET'];
$auth_url = $_ENV['P2G_API_AUTH_URL'];
$account_info_url = 'https://www.parcel2go.com/api/me/detail';
// Function to get the API token
function getApiToken($client_id, $client_secret, $auth_url) {
$ch = curl_init($auth_url);
$data = http_build_query([
'grant_type' => 'client_credentials',
'scope' => 'public-api',
'client_id' => $client_id,
'client_secret' => $client_secret,
]);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/x-www-form-urlencoded',
'Accept: */*',
]);
$response = curl_exec($ch);
curl_close($ch);
$result = json_decode($response, true);
return $result['access_token'] ?? null;
}
// Function to fetch account information
function getAccountInfo($token, $account_info_url) {
$ch = curl_init($account_info_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer $token",
'Accept: application/json',
]);
$response = curl_exec($ch);
curl_close($ch);
return json_decode($response, true);
}
// Main logic
$token = getApiToken($client_id, $client_secret, $auth_url);
$account_info = getAccountInfo($token, $account_info_url);
if ($token) {
$account_info = getAccountInfo($token, $account_info_url);
if ($account_info) {
echo json_encode(['success' => true, 'data' => $account_info]);
} else {
http_response_code(402);
echo json_encode(['success' => false, 'error' => 'Failed to fetch account information.']);
}
} else {
http_response_code(402);
echo json_encode(['success' => false, 'error' => 'Failed to authenticate.']);
}
?>

View File

@@ -0,0 +1,73 @@
<?php
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
require_once '/mnt/www-live/TechOdyssey_Designs_Dashboard/src/envLoader.php';
loadEnv(__DIR__ . '/../../.env');
// Get API credentials from the environment
$client_id = $_ENV['P2G_CLIENT_ID'];
$client_secret = $_ENV['P2G_CLIENT_SECRET'];
$auth_url = $_ENV['P2G_API_AUTH_URL'];
$orders_detail_url = 'https://www.parcel2go.com/api/me/orders/detail';
// Function to get the API token
function getApiToken($client_id, $client_secret, $auth_url) {
$ch = curl_init($auth_url);
$data = http_build_query([
'grant_type' => 'client_credentials',
'scope' => 'public-api',
'client_id' => $client_id,
'client_secret' => $client_secret,
]);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/x-www-form-urlencoded',
'Accept: */*',
]);
$response = curl_exec($ch);
curl_close($ch);
$result = json_decode($response, true);
return $result['access_token'] ?? null;
}
// Function to fetch order details
function getOrdersDetail($token, $orders_detail_url) {
$ch = curl_init($orders_detail_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer $token",
'Accept: application/json',
]);
$response = curl_exec($ch);
curl_close($ch);
return json_decode($response, true);
}
// Main logic
$token = getApiToken($client_id, $client_secret, $auth_url);
if ($token) {
$orders_detail = getOrdersDetail($token, $orders_detail_url);
if ($orders_detail) {
echo json_encode(['success' => true, 'data' => $orders_detail]);
} else {
http_response_code(402);
echo json_encode(['success' => false, 'error' => 'Failed to fetch order details.']);
}
} else {
http_response_code(402);
echo json_encode(['success' => false, 'error' => 'Failed to authenticate.']);
}
?>

View File

@@ -0,0 +1,73 @@
<?php
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
require_once '/mnt/www-live/TechOdyssey_Designs_Dashboard/src/envLoader.php';
loadEnv(__DIR__ . '/../../.env');
// Get API credentials from the environment
$client_id = $_ENV['P2G_CLIENT_ID'];
$client_secret = $_ENV['P2G_CLIENT_SECRET'];
$auth_url = $_ENV['P2G_API_AUTH_URL'];
$prepay_balance_url = 'https://www.parcel2go.com/api/me/prepay/balance';
// Function to get the API token
function getApiToken($client_id, $client_secret, $auth_url) {
$ch = curl_init($auth_url);
$data = http_build_query([
'grant_type' => 'client_credentials',
'scope' => 'public-api',
'client_id' => $client_id,
'client_secret' => $client_secret,
]);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/x-www-form-urlencoded',
'Accept: */*',
]);
$response = curl_exec($ch);
curl_close($ch);
$result = json_decode($response, true);
return $result['access_token'] ?? null;
}
// Function to fetch prepay balance
function getPrepayBalance($token, $prepay_balance_url) {
$ch = curl_init($prepay_balance_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer $token",
'Accept: application/json',
]);
$response = curl_exec($ch);
curl_close($ch);
return json_decode($response, true);
}
// Main logic
$token = getApiToken($client_id, $client_secret, $auth_url);
if ($token) {
$balance_info = getPrepayBalance($token, $prepay_balance_url);
if ($balance_info) {
echo json_encode(['success' => true, 'data' => $balance_info]);
} else {
http_response_code(402);
echo json_encode(['success' => false, 'error' => 'Failed to fetch prepay balance.']);
}
} else {
http_response_code(402);
echo json_encode(['success' => false, 'error' => 'Failed to authenticate.']);
}
?>

View File

@@ -0,0 +1,111 @@
<?php
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
require_once 'db.php';
require_once 'session_check.php';
checkUserRole(['admin']);
header('Content-Type: application/json');
try {
$pdo = new PDO("mysql:host=$host;dbname=$db", $user, $pass);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException $e) {
die("Database connection failed: " . $e->getMessage());
}
// Handle requests
$method = $_SERVER['REQUEST_METHOD'];
if ($method === 'GET') {
fetchUsers();
} elseif ($method === 'POST') {
$input = json_decode(file_get_contents('php://input'), true);
if (isset($input['username']) && isset($input['email']) && isset($input['role']) && isset($input['password'])) {
createUser($input);
} elseif (isset($_GET['reset-password'])) {
$userId = intval($_GET['reset-password']);
resetPassword($userId, $input);
} elseif (isset($_GET['delete-user'])) {
$userId = intval($_GET['delete-user']);
deleteUser($userId);
} else {
http_response_code(400);
echo json_encode(["error" => "Invalid request"]);
}
} else {
http_response_code(405);
echo json_encode(["error" => "Method not allowed"]);
}
function fetchUsers()
{
global $pdo;
$stmt = $pdo->prepare("SELECT id, username, email, role, disabled, created_at FROM users");
$stmt->execute();
$users = $stmt->fetchAll(PDO::FETCH_ASSOC);
echo json_encode($users);
}
function createUser($input)
{
global $pdo;
$hashedPassword = password_hash($input['password'], PASSWORD_BCRYPT);
$stmt = $pdo->prepare("INSERT INTO users (username, email, role, password, disabled, created_at) VALUES (:username, :email, :role, :password, 0, NOW())");
if ($stmt->execute([
':username' => $input['username'],
':email' => $input['email'],
':role' => $input['role'],
':password' => $hashedPassword
])) {
http_response_code(201);
echo json_encode(["success" => "User created successfully"]);
} else {
http_response_code(500);
echo json_encode(["error" => "Failed to create user"]);
}
}
function resetPassword($userId, $input)
{
global $pdo;
if (!isset($input['password'])) {
http_response_code(400);
echo json_encode(["error" => "Password not provided"]);
return;
}
$newPassword = password_hash($input['password'], PASSWORD_BCRYPT);
$stmt = $pdo->prepare("UPDATE users SET password = :password WHERE id = :id");
if ($stmt->execute([
':password' => $newPassword,
':id' => $userId
])) {
echo json_encode(["success" => "Password reset successfully"]);
} else {
http_response_code(500);
echo json_encode(["error" => "Failed to reset password"]);
}
}
function deleteUser($userId)
{
global $pdo;
$stmt = $pdo->prepare("DELETE FROM users WHERE id = :id");
if ($stmt->execute([':id' => $userId])) {
echo json_encode(["success" => "User deleted successfully"]);
} else {
http_response_code(500);
echo json_encode(["error" => "Failed to delete user"]);
}
}

107
template.php Normal file
View File

@@ -0,0 +1,107 @@
<?php include '../src/session_check.php'; ?>
<html lang="en" data-bs-theme="dark">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!--favicon-->
<link rel="icon" href="../assets/images/favicon-32x32.png" type="image/png">
<!--plugins-->
<link href="../assets/plugins/vectormap/jquery-jvectormap-2.0.2.css" rel="stylesheet">
<link href="../assets/plugins/simplebar/css/simplebar.css" rel="stylesheet">
<link href="../assets/plugins/perfect-scrollbar/css/perfect-scrollbar.css" rel="stylesheet">
<link href="../assets/plugins/metismenu/css/metisMenu.min.css" rel="stylesheet">
<!-- loader-->
<link href="../assets/css/pace.min.css" rel="stylesheet"/>
<script src="../assets/js/pace.min.js"></script>
<!-- Bootstrap CSS -->
<link href="../assets/css/bootstrap.min.css" rel="stylesheet">
<link href="../assets/css/bootstrap-extended.css" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500&display=swap" rel="stylesheet">
<link href="../assets/sass/app.css" rel="stylesheet">
<link href="../assets/css/icons.css" rel="stylesheet">
<link href='https://unpkg.com/boxicons@2.1.4/css/boxicons.min.css' rel='stylesheet'>
<!-- Theme Style CSS -->
<link rel="stylesheet" href="../assets/sass/dark-theme.css">
<link rel="stylesheet" href="../assets/sass/semi-dark.css">
<link rel="stylesheet" href="../assets/sass/bordered-theme.css">
<title>TOD Dashboard</title>
</head>
<body>
<!--wrapper-->
<div class="wrapper">
<!--sidebar wrapper -->
<?php include '../src/nav.php'; ?>
<!--end sidebar wrapper -->
<!--start header -->
<?php include '../src/header.php'; ?>
<!--end header -->
<!--start page wrapper -->
<div class="page-wrapper">
<div class="page-content">
<!--start page content -->
<!--end page content -->
</div>
</div>
<!--end page wrapper -->
<!--start overlay-->
<div class="overlay mobile-toggle-icon"></div>
<!--end overlay-->
<!--Start Back To Top Button-->
<a href="javaScript:;" class="back-to-top"><i class='bx bxs-up-arrow-alt'></i></a>
<!--End Back To Top Button-->
<footer class="page-footer">
<p class="mb-0">Copyright © 2024. All right reserved.</p>
</footer>
</div>
<!--end wrapper-->
<!-- search modal -->
<div class="modal" id="SearchModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable modal-fullscreen-md-down">
<div class="modal-content">
<div class="modal-header gap-2">
<div class="position-relative popup-search w-100">
<input class="form-control form-control-lg ps-5 border border-3 border-primary" type="search" placeholder="Search">
<span class="position-absolute top-50 search-show ms-3 translate-middle-y start-0 top-50 fs-4"><i class='bx bx-search'></i></span>
</div>
<button type="button" class="btn-close d-md-none" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
</div>
</div>
</div>
<!-- end search modal -->
<!-- Bootstrap JS -->
<script src="../assets/js/bootstrap.bundle.min.js"></script>
<!--plugins-->
<script src="../assets/js/jquery.min.js"></script>
<script src="../assets/plugins/simplebar/js/simplebar.min.js"></script>
<script src="../assets/plugins/metismenu/js/metisMenu.min.js"></script>
<script src="../assets/plugins/perfect-scrollbar/js/perfect-scrollbar.js"></script>
<script src="../assets/plugins/apexcharts-bundle/js/apexcharts.min.js"></script>
<!--app JS-->
<script src="../assets/js/app.js"></script>
<script src="../assets/js/index.js"></script>
<script src="../assets/plugins/peity/jquery.peity.min.js"></script>
<script>
$(".data-attributes span").peity("donut")
</script>
</body>
</html>