Compare commits
11 Commits
5a11305caf
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8410a50f26 | ||
|
|
053662c2e8 | ||
|
|
cc4e915cf5 | ||
|
|
030d72c202 | ||
|
|
a08c4eba3b | ||
|
|
a1aee4134c | ||
|
|
9424f1857f | ||
|
|
bcb7d7ea7d | ||
|
|
e9908bd65b | ||
|
|
8c9cb042ff | ||
|
|
d96fa63298 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -11,3 +11,4 @@ etsyTokens.json
|
||||
.DS_Store
|
||||
node_modules/
|
||||
vendor/
|
||||
logs/
|
||||
57
.htaccess
Normal file
57
.htaccess
Normal 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>
|
||||
@@ -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);
|
||||
|
||||
|
||||
50
assets/css/bootstrap-extended.css
vendored
50
assets/css/bootstrap-extended.css
vendored
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}]
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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%);
|
||||
|
||||
@@ -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
239
composer.lock
generated
@@ -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
226
esp32Code/dryer/main.ino
Normal 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'.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
618
public/filamentDryer.php
Normal 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>
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
265
public/shipping.php
Normal 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
319
public/userManagement.php
Normal 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
283
public/viewFilament.php
Normal 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>
|
||||
@@ -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">
|
||||
|
||||
162
src/emailService/cache/3e/3eb72c23c9363b64752d0504bdd61ac5.php
vendored
Normal file
162
src/emailService/cache/3e/3eb72c23c9363b64752d0504bdd61ac5.php
vendored
Normal 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");
|
||||
}
|
||||
}
|
||||
9
src/emailService/css/email_styles.css
Normal file
9
src/emailService/css/email_styles.css
Normal file
@@ -0,0 +1,9 @@
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
background-color: #f9f9f9;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.alert {
|
||||
border-radius: 10px;
|
||||
}
|
||||
54
src/emailService/emailService.php
Normal file
54
src/emailService/emailService.php
Normal 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}";
|
||||
}
|
||||
}
|
||||
?>
|
||||
60
src/emailService/sendPriceDropEmailService.php
Normal file
60
src/emailService/sendPriceDropEmailService.php
Normal 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}";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
?>
|
||||
19
src/emailService/templates/alert_error.html
Normal file
19
src/emailService/templates/alert_error.html
Normal 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>
|
||||
19
src/emailService/templates/alert_info.html
Normal file
19
src/emailService/templates/alert_info.html
Normal 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>
|
||||
19
src/emailService/templates/alert_success.html
Normal file
19
src/emailService/templates/alert_success.html
Normal 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>
|
||||
57
src/emailService/templates/filament_price_summary.html.twig
Normal file
57
src/emailService/templates/filament_price_summary.html.twig
Normal 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>
|
||||
@@ -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.']);
|
||||
|
||||
@@ -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']
|
||||
];
|
||||
}
|
||||
|
||||
132
src/filamentTracker/priceDropEmailCheck.php
Normal file
132
src/filamentTracker/priceDropEmailCheck.php
Normal 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();
|
||||
|
||||
?>
|
||||
67
src/filamentTracker/scraper.php
Normal file
67
src/filamentTracker/scraper.php
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
?>
|
||||
@@ -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();
|
||||
}
|
||||
?>
|
||||
|
||||
@@ -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>
|
||||
120
src/nav.php
120
src/nav.php
@@ -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 -->
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
74
src/shipping/get_account_info.php
Normal file
74
src/shipping/get_account_info.php
Normal 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.']);
|
||||
}
|
||||
?>
|
||||
73
src/shipping/get_orders_detail.php
Normal file
73
src/shipping/get_orders_detail.php
Normal 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.']);
|
||||
}
|
||||
?>
|
||||
73
src/shipping/get_prepay_balance.php
Normal file
73
src/shipping/get_prepay_balance.php
Normal 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.']);
|
||||
}
|
||||
?>
|
||||
111
src/userMananagementService.php
Normal file
111
src/userMananagementService.php
Normal 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"]);
|
||||
}
|
||||
}
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user