Compare commits

...

11 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
42 changed files with 3241 additions and 305 deletions

1
.gitignore vendored
View File

@@ -11,3 +11,4 @@ etsyTokens.json
.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)
}

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%);

View File

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

239
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "a6dcfb9667ce99508becf6cd0f9310b9",
"content-hash": "8ffb6b03780af1286dc4d1cd3d8f0445",
"packages": [
{
"name": "fabpot/goutte",
@@ -250,6 +250,87 @@
},
"time": "2024-11-24T20:54:32+00:00"
},
{
"name": "phpmailer/phpmailer",
"version": "v6.9.3",
"source": {
"type": "git",
"url": "https://github.com/PHPMailer/PHPMailer.git",
"reference": "2f5c94fe7493efc213f643c23b1b1c249d40f47e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/2f5c94fe7493efc213f643c23b1b1c249d40f47e",
"reference": "2f5c94fe7493efc213f643c23b1b1c249d40f47e",
"shasum": ""
},
"require": {
"ext-ctype": "*",
"ext-filter": "*",
"ext-hash": "*",
"php": ">=5.5.0"
},
"require-dev": {
"dealerdirect/phpcodesniffer-composer-installer": "^1.0",
"doctrine/annotations": "^1.2.6 || ^1.13.3",
"php-parallel-lint/php-console-highlighter": "^1.0.0",
"php-parallel-lint/php-parallel-lint": "^1.3.2",
"phpcompatibility/php-compatibility": "^9.3.5",
"roave/security-advisories": "dev-latest",
"squizlabs/php_codesniffer": "^3.7.2",
"yoast/phpunit-polyfills": "^1.0.4"
},
"suggest": {
"decomplexity/SendOauth2": "Adapter for using XOAUTH2 authentication",
"ext-mbstring": "Needed to send email in multibyte encoding charset or decode encoded addresses",
"ext-openssl": "Needed for secure SMTP sending and DKIM signing",
"greew/oauth2-azure-provider": "Needed for Microsoft Azure XOAUTH2 authentication",
"hayageek/oauth2-yahoo": "Needed for Yahoo XOAUTH2 authentication",
"league/oauth2-google": "Needed for Google XOAUTH2 authentication",
"psr/log": "For optional PSR-3 debug logging",
"symfony/polyfill-mbstring": "To support UTF-8 if the Mbstring PHP extension is not enabled (^1.2)",
"thenetworg/oauth2-azure": "Needed for Microsoft XOAUTH2 authentication"
},
"type": "library",
"autoload": {
"psr-4": {
"PHPMailer\\PHPMailer\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"LGPL-2.1-only"
],
"authors": [
{
"name": "Marcus Bointon",
"email": "phpmailer@synchromedia.co.uk"
},
{
"name": "Jim Jagielski",
"email": "jimjag@gmail.com"
},
{
"name": "Andy Prevost",
"email": "codeworxtech@users.sourceforge.net"
},
{
"name": "Brent R. Matzelle"
}
],
"description": "PHPMailer is a full-featured email creation and transfer class for PHP",
"support": {
"issues": "https://github.com/PHPMailer/PHPMailer/issues",
"source": "https://github.com/PHPMailer/PHPMailer/tree/v6.9.3"
},
"funding": [
{
"url": "https://github.com/Synchro",
"type": "github"
}
],
"time": "2024-11-24T18:04:13+00:00"
},
{
"name": "psr/container",
"version": "2.0.2",
@@ -1199,6 +1280,82 @@
],
"time": "2024-09-09T11:45:10+00:00"
},
{
"name": "symfony/polyfill-php81",
"version": "v1.31.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php81.git",
"reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c",
"reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c",
"shasum": ""
},
"require": {
"php": ">=7.2"
},
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/polyfill",
"name": "symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Php81\\": ""
},
"classmap": [
"Resources/stubs"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php81/tree/v1.31.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-09-09T11:45:10+00:00"
},
{
"name": "symfony/service-contracts",
"version": "v3.5.1",
@@ -1281,6 +1438,86 @@
}
],
"time": "2024-09-25T14:20:29+00:00"
},
{
"name": "twig/twig",
"version": "v3.18.0",
"source": {
"type": "git",
"url": "https://github.com/twigphp/Twig.git",
"reference": "acffa88cc2b40dbe42eaf3a5025d6c0d4600cc50"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/twigphp/Twig/zipball/acffa88cc2b40dbe42eaf3a5025d6c0d4600cc50",
"reference": "acffa88cc2b40dbe42eaf3a5025d6c0d4600cc50",
"shasum": ""
},
"require": {
"php": ">=8.0.2",
"symfony/deprecation-contracts": "^2.5|^3",
"symfony/polyfill-ctype": "^1.8",
"symfony/polyfill-mbstring": "^1.3",
"symfony/polyfill-php81": "^1.29"
},
"require-dev": {
"phpstan/phpstan": "^2.0",
"psr/container": "^1.0|^2.0",
"symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0"
},
"type": "library",
"autoload": {
"files": [
"src/Resources/core.php",
"src/Resources/debug.php",
"src/Resources/escaper.php",
"src/Resources/string_loader.php"
],
"psr-4": {
"Twig\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com",
"homepage": "http://fabien.potencier.org",
"role": "Lead Developer"
},
{
"name": "Twig Team",
"role": "Contributors"
},
{
"name": "Armin Ronacher",
"email": "armin.ronacher@active-4.com",
"role": "Project Founder"
}
],
"description": "Twig, the flexible, fast, and secure template language for PHP",
"homepage": "https://twig.symfony.com",
"keywords": [
"templating"
],
"support": {
"issues": "https://github.com/twigphp/Twig/issues",
"source": "https://github.com/twigphp/Twig/tree/v3.18.0"
},
"funding": [
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/twig/twig",
"type": "tidelift"
}
],
"time": "2024-12-29T10:51:50+00:00"
}
],
"packages-dev": [],

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'.");
}
}
}

View File

@@ -1,6 +1,8 @@
<?php include '../src/session_check.php'; ?>
<?php include '../src/session_check.php';
checkUserRole(['admin']);
?>
<html lang="en" data-bs-theme="light">
<html lang="en" data-bs-theme="dark">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">

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,6 +1,6 @@
<?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">

View File

@@ -1,5 +1,5 @@
<!doctype html>
<html lang="en" data-bs-theme="light">
<html lang="en" data-bs-theme="dark">
<head>
<meta charset="utf-8">
@@ -14,7 +14,10 @@
<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">
<!-- 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>
@@ -40,7 +43,7 @@
</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">

View File

@@ -1,6 +1,6 @@
<?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">

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>

View File

@@ -1,6 +1,6 @@
<?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">

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>

View File

@@ -1,91 +1,80 @@
<?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');
// Check if the user is logged in
if (!isset($_SESSION['userId'])) {
echo json_encode(['status' => 'error', 'message' => 'User not authenticated.']);
exit;
}
use Goutte\Client;
include '../session_check.php';
checkUserRole(['admin']);
// Start session to get user ID
session_start();
if (!isset($_SESSION['userId'])) {
echo json_encode(['status' => 'error', 'message' => 'User not authenticated.']);
exit;
}
// Check for POST data
// Check if the request method is POST
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$filamentName = $_POST['productName'] ?? '';
$amazonUrl = $_POST['productUrl'] ?? '';
$userId = $_SESSION['userId'];
// Basic validation
// Validate the input
if (empty($filamentName) || empty($amazonUrl)) {
echo json_encode(['status' => 'error', 'message' => 'Filament name and URL are required.']);
exit;
}
$client = new Client();
try {
// Scrape the Amazon page for initial price and other details
$crawler = $client->request('GET', $amazonUrl);
// Scrape Price
$whole = $crawler->filter('.a-price-whole')->count() ? $crawler->filter('.a-price-whole')->text() : '0';
$fraction = $crawler->filter('.a-price-fraction')->count() ? $crawler->filter('.a-price-fraction')->text() : '00';
// Scrape data from Amazon
$scrapedData = scrapeAmazonData($amazonUrl);
if (!$scrapedData) {
throw new Exception('Scraping failed for the provided URL.');
}
$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);
$price = $scrapedData['price'];
$currentDiscount = json_encode($scrapedData['currentDiscount']); // Convert discount to JSON
$details = $scrapedData['details'] ?? [];
// Scrape Filament Details from Table
$details = [];
$crawler->filter('table.a-normal tr')->each(function ($node) use (&$details) {
$label = trim($node->filter('td.a-span3')->text());
$value = trim($node->filter('td.a-span9')->text());
$details[$label] = $value;
});
// Extract details or use default if not found
// 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 = $filamentWeight * 1000; // Convert KG to G
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;
// Start a transaction to ensure consistency
// Begin transaction for database operations
$pdo->beginTransaction();
// Check if filament already exists
// 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) {
// Filament exists just update the price history
// If the filament exists, update the price and discount
$filamentId = $existingFilament['id'];
$stmt = $pdo->prepare("
INSERT INTO filamentPriceHistory (filamentId, price)
VALUES (:filamentId, :price)
INSERT INTO filamentPriceHistory (filamentId, price, currentDiscount)
VALUES (:filamentId, :price, :currentDiscount)
");
$stmt->execute([
':filamentId' => $filamentId,
':price' => $totalPrice
':price' => $price,
':currentDiscount' => $currentDiscount
]);
$message = 'Filament price updated successfully.';
$message = 'Filament price and discount updated successfully.';
} else {
// Insert new filament into filamentTracker
// 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)
@@ -101,29 +90,31 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
':itemDiameter' => $itemDiameter
]);
// Get the last inserted filament ID
// Get the ID of the newly inserted filament
$filamentId = $pdo->lastInsertId();
// Insert initial price into filamentPriceHistory
// Insert the initial price and discount into filamentPriceHistory
$stmt = $pdo->prepare("
INSERT INTO filamentPriceHistory (filamentId, price)
VALUES (:filamentId, :price)
INSERT INTO filamentPriceHistory (filamentId, price, currentDiscount)
VALUES (:filamentId, :price, :currentDiscount)
");
$stmt->execute([
':filamentId' => $filamentId,
':price' => $totalPrice
':price' => $price,
':currentDiscount' => $currentDiscount
]);
$message = $filamentName . ' Filament added successfully and price tracked.';
$message = "$filamentName added successfully and price with discount tracked.";
}
// Commit the transaction
$pdo->commit();
echo json_encode(['status' => 'success', 'message' => $message]);
} catch (Exception $e) {
$pdo->rollBack();
echo json_encode(['status' => 'error', 'message' => 'Failed to scrape or insert data. Error: ' . $e->getMessage()]);
// 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

@@ -4,52 +4,66 @@ require '../db.php';
require_once '../envLoader.php';
loadEnv(__DIR__ . '/../../.env');
// Check if the user is logged in
if (!isset($_SESSION['userId'])) {
echo json_encode(['status' => 'error', 'message' => 'User not authenticated.']);
exit;
}
include '../src/session_check.php';
header('Content-Type: application/json');
// Fetch all filament prices from the database
try {
$stmt = $pdo->query("
SELECT
ft.filamentName,
ft.brand,
ft.material,
ft.color,
fp.price,
fp.recordedAt
FROM
filamentTracker ft
JOIN
filamentPriceHistory fp ON ft.id = fp.filamentId
ORDER BY
ft.filamentName,
fp.recordedAt ASC
// 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 = $stmt->fetchAll(PDO::FETCH_ASSOC);
$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 = [];
// Format data for charts (grouped by filament)
foreach ($filaments as $filament) {
$name = $filament['filamentName'];
if (!isset($result[$name])) {
$result[$name] = [
'brand' => $filament['brand'],
'material' => $filament['material'],
'color' => $filament['color'],
'prices' => []
'amazonUrl' => $filament['amazonUrl'],
'prices' => [],
'currentDiscount' => $discountMap[$filament['filamentId']] ?? [
'discount' => ['value' => 0, 'type' => 'none'],
'voucher' => ['value' => 0, 'type' => 'none']
]
];
}
$result[$name]['prices'][] = [
'price' => $filament['price'],
'price' => (float)$filament['price'],
'recordedAt' => $filament['recordedAt']
];
}

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

@@ -1,98 +0,0 @@
<?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';
loadEnv(__DIR__ . '/../../.env');
use Goutte\Client;
function scrapeFilamentPrice($productName, $productUrl) {
$client = new Client();
$crawler = $client->request('GET', $productUrl);
// Extract the current price
$price = $crawler->filter('span.a-price-whole')->first()->text();
$price = str_replace(',', '', $price); // Clean the price string
// Extract the original price (if available)
$originalPriceNode = $crawler->filter('span.a-price.a-text-price')->first();
$originalPrice = $originalPriceNode->count() ? str_replace(',', '', $originalPriceNode->text()) : null;
// Calculate the discount percentage
$discountPercentage = null;
if ($originalPrice) {
$discountPercentage = round(100 - (($price / $originalPrice) * 100), 2);
}
$currency = 'GBP';
return [
'productName' => $productName,
'productUrl' => $productUrl,
'price' => $price,
'originalPrice' => $originalPrice,
'discountPercentage' => $discountPercentage,
'currency' => $currency
];
}
// Handle Form Submission (Add New Filament to Track)
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$productName = $_POST['productName'];
$productUrl = $_POST['productUrl'];
$result = scrapeFilamentPrice($productName, $productUrl);
try {
$stmt = $pdo->prepare("
INSERT INTO filamentPrices (productName, productUrl, price, originalPrice, discountPercentage, currency)
VALUES (:productName, :productUrl, :price, :originalPrice, :discountPercentage, :currency)
");
$stmt->execute([
':productName' => $result['productName'],
':productUrl' => $result['productUrl'],
':price' => $result['price'],
':originalPrice' => $result['originalPrice'],
':discountPercentage' => $result['discountPercentage'],
':currency' => $result['currency']
]);
echo json_encode(['status' => 'success', 'message' => 'Filament price recorded successfully!']);
} catch (PDOException $e) {
echo json_encode(['status' => 'error', 'message' => 'Failed to record price: ' . $e->getMessage()]);
}
}
// Handle Scheduled Scraping for Existing URLs (Cron Job)
if (isset($argv[1]) && $argv[1] === 'cron') {
try {
$stmt = $pdo->query("SELECT * FROM filamentPrices GROUP BY productUrl");
$filaments = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($filaments as $filament) {
$result = scrapeFilamentPrice($filament['productName'], $filament['productUrl']);
$stmt = $pdo->prepare("
INSERT INTO filamentPrices (productName, productUrl, price, originalPrice, discountPercentage, currency)
VALUES (:productName, :productUrl, :price, :originalPrice, :discountPercentage, :currency)
");
$stmt->execute([
':productName' => $result['productName'],
':productUrl' => $result['productUrl'],
':price' => $result['price'],
':originalPrice' => $result['originalPrice'],
':discountPercentage' => $result['discountPercentage'],
':currency' => $result['currency']
]);
echo "Recorded: {$result['productName']} - £{$result['price']} (Discount: {$result['discountPercentage']}%)\n";
}
} catch (PDOException $e) {
echo "Failed to fetch filament data: " . $e->getMessage();
}
}
?>

View File

@@ -2,31 +2,9 @@
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');
use Goutte\Client;
// Function to scrape price
function scrapePrice($url) {
$client = new Client(); // Reinitialize client for each request
try {
$crawler = $client->request('GET', $url);
$whole = $crawler->filter('.a-price-whole')->count() ? $crawler->filter('.a-price-whole')->text() : '0';
$fraction = $crawler->filter('.a-price-fraction')->count() ? $crawler->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);
return $totalPrice;
} catch (Exception $e) {
return null;
}
}
// Fetch all filaments
try {
$stmt = $pdo->query("SELECT * FROM filamentTracker");
@@ -36,20 +14,33 @@ try {
$amazonUrl = $filament['amazonUrl'];
$filamentId = $filament['id'];
// Scrape price
$totalPrice = scrapePrice($amazonUrl);
// 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,
],
]);
if ($totalPrice !== null && $totalPrice > 0) {
$stmt = $pdo->prepare("
INSERT INTO filamentPriceHistory (filamentId, price)
VALUES (:filamentId, :price)
INSERT INTO filamentPriceHistory (filamentId, price, currentDiscount)
VALUES (:filamentId, :price, :currentDiscount)
");
$stmt->execute([
':filamentId' => $filamentId,
':price' => $totalPrice
':price' => $scrapedData['price'],
':currentDiscount' => $currentDiscount
]);
echo "Updated price for {$filament['filamentName']}: £{$totalPrice}\n";
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";
}
@@ -60,4 +51,3 @@ try {
} catch (PDOException $e) {
echo "Database error: " . $e->getMessage();
}
?>

View File

@@ -65,7 +65,9 @@
<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>My Printers</span></a>
<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>
@@ -75,5 +77,28 @@
</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,40 +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="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="viewPrinters.php">
<div class="parent-icon"><i class='bx bx-code-alt'></i>
</div>
<div class="menu-title">Printer Dashboard</div>
</a>
</li>
</ul>
<!--end navigation-->
</div>
<!--end sidebar wrapper -->
<!--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

@@ -1,6 +1,38 @@
<?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"]);
}
}

View File

@@ -1,6 +1,6 @@
<?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">