I cba with this commit message tbh

This commit is contained in:
Hickmeister
2025-01-14 20:28:59 +00:00
parent a1aee4134c
commit a08c4eba3b
26 changed files with 1135 additions and 231 deletions

View File

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

View File

@@ -23,33 +23,33 @@ a {
} }
.valid-feedback { .valid-feedback {
color: #15ca20 color: #23ac2c
} }
.form-control.is-valid, .was-validated .form-control:valid { .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 { .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 { .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 { .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 { .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 { .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 { .form-check-input.is-valid~.form-check-label, .was-validated .form-check-input:valid~.form-check-label {
color: #15ca20 color: #23ac2c
} }
.invalid-feedback { .invalid-feedback {
@@ -120,8 +120,8 @@ a {
.btn-success { .btn-success {
color: #fff; color: #fff;
background-color: #15ca20; background-color: #2375ac;
border-color: #15ca20 border-color: #23ac2c
} }
.btn-success:hover { .btn-success:hover {
color: #fff; color: #fff;
@@ -207,27 +207,27 @@ a {
.btn-outline-success { .btn-outline-success {
color: #15ca20; color: #23ac2c;
border-color: #15ca20 border-color: #23ac2c
} }
.btn-outline-success:hover { .btn-outline-success:hover {
color: #fff; color: #fff;
background-color: #15ca20; background-color: #23ac2c;
border-color: #15ca20 border-color: #23ac2c
} }
.btn-check:focus+.btn-outline-success, .btn-outline-success:focus { .btn-check:focus+.btn-outline-success, .btn-outline-success:focus {
box-shadow: 0 0 0 .25rem rgb(23 160 14 / 52%) 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 { .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; color: #fff;
background-color: #15ca20; background-color: #23ac2c;
border-color: #15ca20 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 { .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%) box-shadow: 0 0 0 .25rem rgb(23 160 14 / 52%)
} }
.btn-outline-success.disabled, .btn-outline-success:disabled { .btn-outline-success.disabled, .btn-outline-success:disabled {
color: #15ca20; color: #23ac2c;
background-color: transparent background-color: transparent
} }
@@ -304,7 +304,7 @@ a {
} }
.border-success { .border-success {
border-color: #15ca20!important border-color: #23ac2c!important
} }
.border-danger { .border-danger {
@@ -324,7 +324,7 @@ a {
color: #6c757d!important color: #6c757d!important
} }
.text-success { .text-success {
color: #15ca20!important color: #23ac2c!important
} }
.text-info { .text-info {
color: #0dcaf0!important color: #0dcaf0!important
@@ -364,7 +364,7 @@ a {
} }
.bg-success { .bg-success {
background-color: #15ca20 !important; background-color: #23ac2c !important;
} }
.bg-danger { .bg-danger {
@@ -373,19 +373,19 @@ a {
.form-check-success .form-check-input:checked { .form-check-success .form-check-input:checked {
background-color: #15ca20; background-color: #23ac2c;
border-color: #15ca20 border-color: #23ac2c
} }
.form-check-success .form-check-input[type=checkbox]:indeterminate { .form-check-success .form-check-input[type=checkbox]:indeterminate {
background-color: #15ca20; background-color: #23ac2c;
border-color: #15ca20; 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") 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 { .form-check-success .form-check-input:focus {
border-color: #15ca20; border-color: #23ac2c;
outline: 0; outline: 0;
box-shadow: 0 0 0 .25rem rgba(21, 202, 33, 0.25) box-shadow: 0 0 0 .25rem rgba(21, 202, 33, 0.25)
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,7 +2,7 @@
checkUserRole(['admin']); checkUserRole(['admin']);
?> ?>
<html lang="en" data-bs-theme="light"> <html lang="en" data-bs-theme="dark">
<head> <head>
<!-- Required meta tags --> <!-- Required meta tags -->
<meta charset="utf-8"> <meta charset="utf-8">

View File

@@ -2,7 +2,7 @@
checkUserRole(['admin']); checkUserRole(['admin']);
?> ?>
<html lang="en" data-bs-theme="light"> <html lang="en" data-bs-theme="dark">
<head> <head>
<!-- Required meta tags --> <!-- Required meta tags -->
<meta charset="utf-8"> <meta charset="utf-8">

View File

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

View File

@@ -1,5 +1,5 @@
<!doctype html> <!doctype html>
<html lang="en" data-bs-theme="light"> <html lang="en" data-bs-theme="dark">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
@@ -14,7 +14,10 @@
<link href="../assets/css/bootstrap-extended.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="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/sass/app.css" rel="stylesheet">
<!-- Theme Style CSS -->
<link rel="stylesheet" href="../assets/sass/dark-theme.css"> <link rel="stylesheet" href="../assets/sass/dark-theme.css">
<link rel="stylesheet" href="../assets/sass/semi-dark.css">
<link rel="stylesheet" href="../assets/sass/bordered-theme.css">
<link href="../assets/css/icons.css" rel="stylesheet"> <link href="../assets/css/icons.css" rel="stylesheet">
<title>TOD - Admin Dashboard</title> <title>TOD - Admin Dashboard</title>
</head> </head>
@@ -40,7 +43,7 @@
</div> </div>
<form id="loginForm" class="row g-3"> <form id="loginForm" class="row g-3">
<div class="col-12"> <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> <input type="input" class="form-control" name="username" id="inputEmailAddress" required>
</div> </div>
<div class="col-12"> <div class="col-12">

View File

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

265
public/shipping.php Normal file
View File

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

319
public/userManagement.php Normal file
View File

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

View File

@@ -1,6 +1,6 @@
<?php include '../src/session_check.php'; ?> <?php include '../src/session_check.php'; ?>
<html lang="en" data-bs-theme="light"> <html lang="en" data-bs-theme="dark">
<head> <head>
<!-- Required meta tags --> <!-- Required meta tags -->
<meta charset="utf-8"> <meta charset="utf-8">
@@ -139,16 +139,6 @@ document.addEventListener("DOMContentLoaded", function () {
`; `;
} }
// Debounce function to reduce DOM updates
function debounceRender(func, delay) {
let inDebounce;
return function() {
const context = this, args = arguments;
clearTimeout(inDebounce);
inDebounce = setTimeout(() => func.apply(context, args), delay);
};
}
fetch('../src/filamentTracker/getFilamentPrices.php') fetch('../src/filamentTracker/getFilamentPrices.php')
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
@@ -159,26 +149,37 @@ document.addEventListener("DOMContentLoaded", function () {
const prices = data.data[filament].prices.map(entry => entry.price); const prices = data.data[filament].prices.map(entry => entry.price);
const timestamps = data.data[filament].prices.map(entry => entry.recordedAt); const timestamps = data.data[filament].prices.map(entry => entry.recordedAt);
const latestPrice = prices[prices.length - 1] || 0; const latestPrice = prices[prices.length - 1] || 0;
const previousPrice = prices[prices.length - 2] || latestPrice;
const priceDifference = latestPrice - previousPrice;
// 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; let priceChangeIndicator;
if (priceDifference > 0) { if (priceDifference > 0) {
priceChangeIndicator = `<span class="text-danger ms-4"> priceChangeIndicator = `<span class="text-danger ms-4">
<i class="bx bx-up-arrow-alt"></i> +£${priceDifference.toFixed(2)}</span>`; <i class="bx bx-up-arrow-alt"></i> +£${priceDifference.toFixed(2)}</span>`;
} else if (priceDifference < 0) { } else if (priceDifference < 0) {
priceChangeIndicator = `<span class="text-success ms-4"> priceChangeIndicator = `<span class="text-success ms-4">
<i class="bx bx-down-arrow-alt"></i> £${Math.abs(priceDifference).toFixed(2)}</span>`; <i class="bx bx-down-arrow-alt"></i> -£${Math.abs(priceDifference).toFixed(2)}</span>`;
} else { } else {
priceChangeIndicator = `<span class="text-muted ms-4"> priceChangeIndicator = `<span class="text-muted ms-4">
<i class="bx bx-minus"></i> £0.00</span>`; <i class="bx bx-minus"></i> £0.00</span>`;
} }
const amazonUrl = data.data[filament].amazonUrl || '#'; const amazonUrl = data.data[filament].amazonUrl || '#';
const discount = data.data[filament].currentDiscount || 0; // Get discount percentage
const chartId = `chart-${filament.replace(/\s+/g, '-')}`; const chartId = `chart-${filament.replace(/\s+/g, '-')}`;
const chartColor = getRandomColor(); const chartColor = getRandomColor();
// Create Card HTML with cart button // Create Card HTML
const cardHTML = ` const cardHTML = `
<div class="col-lg-4 mb-1"> <div class="col-lg-4 mb-1">
<div class="card radius-10 overflow-hidden"> <div class="card radius-10 overflow-hidden">
@@ -189,6 +190,15 @@ document.addEventListener("DOMContentLoaded", function () {
£${latestPrice} ${priceChangeIndicator} £${latestPrice} ${priceChangeIndicator}
</h5> </h5>
</div> </div>
<div>
${
discount > 0
? `<span class="text-success fw-bold" style="font-size: 1rem;">
-${discount}%
</span>`
: ''
}
</div>
</div> </div>
<div id="${chartId}"></div> <div id="${chartId}"></div>
</div> </div>
@@ -197,84 +207,38 @@ document.addEventListener("DOMContentLoaded", function () {
container.insertAdjacentHTML('beforeend', cardHTML); container.insertAdjacentHTML('beforeend', cardHTML);
// Observer to Wait for DOM Insertion // Chart Rendering
const observer = new MutationObserver(debounceRender(() => {
const chartElement = document.getElementById(chartId);
if (chartElement) {
const chartOptions = { const chartOptions = {
series: [{ series: [{ name: "Price", data: prices }],
name: "Price",
data: prices
}],
chart: { chart: {
type: "area", type: "area",
height: 110, height: 110,
toolbar: { toolbar: { show: false },
show: false zoom: { enabled: false },
sparkline: { enabled: true },
}, },
zoom: { markers: { size: 0 },
enabled: false dataLabels: { enabled: false },
}, stroke: { show: true, width: 2.4, curve: "smooth" },
dropShadow: {
enabled: true,
top: 3,
left: 14,
blur: 4,
opacity: 0.12,
color: chartColor
},
sparkline: {
enabled: true
}
},
markers: {
size: 0,
colors: [chartColor], colors: [chartColor],
strokeColors: "#fff", xaxis: { categories: timestamps, labels: { show: false } },
strokeWidth: 2, fill: { opacity: 1 },
hover: {
size: 7
}
},
dataLabels: {
enabled: false
},
stroke: {
show: true,
width: 2.4,
curve: "smooth"
},
colors: [chartColor],
xaxis: {
categories: timestamps,
labels: {
show: false
}
},
fill: {
opacity: 1
},
tooltip: { tooltip: {
theme: "dark", theme: "dark",
x: {
show: false
},
y: { y: {
formatter: function (value, { dataPointIndex }) { formatter: function (value, { dataPointIndex }) {
const date = new Date(timestamps[dataPointIndex]); const date = new Date(timestamps[dataPointIndex]);
return `£${value} - ${date.toLocaleDateString()}`; return `£${value} - ${date.toLocaleDateString()}`;
} },
} },
} },
}; };
const chartElement = document.getElementById(chartId);
if (chartElement) {
const chart = new ApexCharts(chartElement, chartOptions); const chart = new ApexCharts(chartElement, chartOptions);
chart.render(); chart.render();
observer.disconnect();
} }
}, 300));
observer.observe(container, { childList: true, subtree: true });
}); });
} else { } else {
container.innerHTML = '<p class="text-center">No filament data available.</p>'; container.innerHTML = '<p class="text-center">No filament data available.</p>';
@@ -288,6 +252,9 @@ document.addEventListener("DOMContentLoaded", function () {
}); });
</script> </script>
</body> </body>
</html> </html>

View File

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

View File

@@ -1,57 +1,45 @@
<?php <?php
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
require '../../vendor/autoload.php'; require '../../vendor/autoload.php';
require '../db.php'; require '../db.php';
require_once '../envLoader.php'; require_once '../envLoader.php';
require 'scraper.php';
loadEnv(__DIR__ . '/../../.env'); loadEnv(__DIR__ . '/../../.env');
include '../src/session_check.php'; include '../session_check.php';
checkUserRole(['admin']); checkUserRole(['admin']);
use Goutte\Client;
// Start session to get user ID
session_start();
if (!isset($_SESSION['userId'])) { if (!isset($_SESSION['userId'])) {
echo json_encode(['status' => 'error', 'message' => 'User not authenticated.']); echo json_encode(['status' => 'error', 'message' => 'User not authenticated.']);
exit; exit;
} }
// Check for POST data
if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$filamentName = $_POST['productName'] ?? ''; $filamentName = $_POST['productName'] ?? '';
$amazonUrl = $_POST['productUrl'] ?? ''; $amazonUrl = $_POST['productUrl'] ?? '';
$userId = $_SESSION['userId']; $userId = $_SESSION['userId'];
// Basic validation
if (empty($filamentName) || empty($amazonUrl)) { if (empty($filamentName) || empty($amazonUrl)) {
echo json_encode(['status' => 'error', 'message' => 'Filament name and URL are required.']); echo json_encode(['status' => 'error', 'message' => 'Filament name and URL are required.']);
exit; exit;
} }
$client = new Client();
try { try {
// Scrape the Amazon page for initial price and other details // Scrape data from Amazon
$crawler = $client->request('GET', $amazonUrl); $scrapedData = scrapeAmazonData($amazonUrl);
if (!$scrapedData) {
throw new Exception('Scraping failed for the provided URL.');
}
// Scrape Price $price = $scrapedData['price'];
$whole = $crawler->filter('.a-price-whole')->count() ? $crawler->filter('.a-price-whole')->text() : '0'; $discount = $scrapedData['discount'];
$fraction = $crawler->filter('.a-price-fraction')->count() ? $crawler->filter('.a-price-fraction')->text() : '00'; $details = $scrapedData['details'];
$whole = preg_replace('/[^0-9]/', '', $whole); // Extract details or use defaults
$fraction = preg_replace('/[^0-9]/', '', $fraction);
$fraction = strlen($fraction) === 1 ? $fraction . '0' : substr($fraction, 0, 2);
$totalPrice = floatval($whole . '.' . $fraction);
// 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
$brand = $details['Brand'] ?? 'Unknown'; $brand = $details['Brand'] ?? 'Unknown';
$material = $details['Material'] ?? 'Unknown'; $material = $details['Material'] ?? 'Unknown';
$color = $details['Colour'] ?? 'Unknown'; $color = $details['Colour'] ?? 'Unknown';
@@ -61,28 +49,25 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
} }
$itemDiameter = isset($details['Item diameter']) ? preg_replace('/[^0-9.]/', '', $details['Item diameter']) : 1.75; $itemDiameter = isset($details['Item diameter']) ? preg_replace('/[^0-9.]/', '', $details['Item diameter']) : 1.75;
// Start a transaction to ensure consistency // Database operations
$pdo->beginTransaction(); $pdo->beginTransaction();
// Check if filament already exists
$stmt = $pdo->prepare("SELECT id FROM filamentTracker WHERE amazonUrl = :amazonUrl"); $stmt = $pdo->prepare("SELECT id FROM filamentTracker WHERE amazonUrl = :amazonUrl");
$stmt->execute([':amazonUrl' => $amazonUrl]); $stmt->execute([':amazonUrl' => $amazonUrl]);
$existingFilament = $stmt->fetch(PDO::FETCH_ASSOC); $existingFilament = $stmt->fetch(PDO::FETCH_ASSOC);
if ($existingFilament) { if ($existingFilament) {
// Filament exists just update the price history
$filamentId = $existingFilament['id']; $filamentId = $existingFilament['id'];
$stmt = $pdo->prepare(" $stmt = $pdo->prepare("
INSERT INTO filamentPriceHistory (filamentId, price) INSERT INTO filamentPriceHistory (filamentId, price, currentDiscount)
VALUES (:filamentId, :price) VALUES (:filamentId, :price, :currentDiscount)
"); ");
$stmt->execute([ $stmt->execute([
':filamentId' => $filamentId, ':filamentId' => $filamentId,
':price' => $totalPrice ':price' => $price,
':currentDiscount' => $discount
]); ]);
$message = 'Filament price updated successfully.'; $message = 'Filament price and discount updated successfully.';
} else { } else {
// Insert new filament into filamentTracker
$stmt = $pdo->prepare(" $stmt = $pdo->prepare("
INSERT INTO filamentTracker (userId, filamentName, amazonUrl, filamentWeight, brand, material, color, itemDiameter) INSERT INTO filamentTracker (userId, filamentName, amazonUrl, filamentWeight, brand, material, color, itemDiameter)
VALUES (:userId, :filamentName, :amazonUrl, :filamentWeight, :brand, :material, :color, :itemDiameter) VALUES (:userId, :filamentName, :amazonUrl, :filamentWeight, :brand, :material, :color, :itemDiameter)
@@ -98,29 +83,24 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
':itemDiameter' => $itemDiameter ':itemDiameter' => $itemDiameter
]); ]);
// Get the last inserted filament ID
$filamentId = $pdo->lastInsertId(); $filamentId = $pdo->lastInsertId();
// Insert initial price into filamentPriceHistory
$stmt = $pdo->prepare(" $stmt = $pdo->prepare("
INSERT INTO filamentPriceHistory (filamentId, price) INSERT INTO filamentPriceHistory (filamentId, price, currentDiscount)
VALUES (:filamentId, :price) VALUES (:filamentId, :price, :currentDiscount)
"); ");
$stmt->execute([ $stmt->execute([
':filamentId' => $filamentId, ':filamentId' => $filamentId,
':price' => $totalPrice ':price' => $price,
':currentDiscount' => $discount
]); ]);
$message = "$filamentName added successfully and price with discount tracked.";
$message = $filamentName . ' Filament added successfully and price tracked.';
} }
// Commit the transaction
$pdo->commit(); $pdo->commit();
echo json_encode(['status' => 'success', 'message' => $message]); echo json_encode(['status' => 'success', 'message' => $message]);
} catch (Exception $e) { } catch (Exception $e) {
$pdo->rollBack(); $pdo->rollBack();
echo json_encode(['status' => 'error', 'message' => 'Failed to scrape or insert data. Error: ' . $e->getMessage()]); echo json_encode(['status' => 'error', 'message' => $e->getMessage()]);
} }
} else { } else {
echo json_encode(['status' => 'error', 'message' => 'Invalid request method.']); echo json_encode(['status' => 'error', 'message' => 'Invalid request method.']);

View File

@@ -9,7 +9,7 @@ include '../src/session_check.php';
header('Content-Type: application/json'); header('Content-Type: application/json');
try { try {
// Fetch all filament prices with a limit of 180 entries per filament using JOIN // Fetch all filament prices with the latest discount
$stmt = $pdo->query(" $stmt = $pdo->query("
SELECT ft.filamentName, SELECT ft.filamentName,
ft.brand, ft.brand,
@@ -17,6 +17,7 @@ try {
ft.color, ft.color,
ft.amazonUrl, ft.amazonUrl,
fp.price, fp.price,
fp.currentDiscount,
fp.recordedAt fp.recordedAt
FROM filamentTracker ft FROM filamentTracker ft
JOIN filamentPriceHistory fp ON ft.id = fp.filamentId JOIN filamentPriceHistory fp ON ft.id = fp.filamentId
@@ -27,7 +28,7 @@ try {
ROW_NUMBER() OVER (PARTITION BY filamentId ORDER BY recordedAt DESC) as rn ROW_NUMBER() OVER (PARTITION BY filamentId ORDER BY recordedAt DESC) as rn
FROM filamentPriceHistory FROM filamentPriceHistory
) ranked ) ranked
WHERE rn <= 180 WHERE rn <= 280
) filtered ON fp.filamentId = filtered.filamentId AND fp.recordedAt = filtered.recordedAt ) filtered ON fp.filamentId = filtered.filamentId AND fp.recordedAt = filtered.recordedAt
ORDER BY ft.filamentName, fp.recordedAt ASC ORDER BY ft.filamentName, fp.recordedAt ASC
"); ");
@@ -46,7 +47,8 @@ try {
'material' => $filament['material'], 'material' => $filament['material'],
'color' => $filament['color'], 'color' => $filament['color'],
'amazonUrl' => $filament['amazonUrl'], 'amazonUrl' => $filament['amazonUrl'],
'prices' => [] 'prices' => [],
'currentDiscount' => $filament['currentDiscount'] ?? 0 // Include the latest discount
]; ];
} }

View File

@@ -0,0 +1,46 @@
<?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);
// 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';
$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 = $crawler->filter('.savingsPercentage')->count()
? $crawler->filter('.savingsPercentage')->text()
: null;
if ($discount) {
$discount = preg_replace('/[^0-9]/', '', $discount);
} else {
$discount = 0; // Default to 0 if no discount is found
}
// Scrape additional details
$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;
});
return [
'price' => $totalPrice,
'discount' => $discount,
'details' => $details
];
} catch (Exception $e) {
return null; // Return null if scraping fails
}
}

View File

@@ -2,31 +2,9 @@
require '/mnt/www-live/TechOdyssey_Designs_Dashboard/vendor/autoload.php'; require '/mnt/www-live/TechOdyssey_Designs_Dashboard/vendor/autoload.php';
require '/mnt/www-live/TechOdyssey_Designs_Dashboard/src/db.php'; require '/mnt/www-live/TechOdyssey_Designs_Dashboard/src/db.php';
require_once '/mnt/www-live/TechOdyssey_Designs_Dashboard/src/envLoader.php'; require_once '/mnt/www-live/TechOdyssey_Designs_Dashboard/src/envLoader.php';
require 'scraper.php';
loadEnv(__DIR__ . '/../../.env'); 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 // Fetch all filaments
try { try {
$stmt = $pdo->query("SELECT * FROM filamentTracker"); $stmt = $pdo->query("SELECT * FROM filamentTracker");
@@ -36,20 +14,21 @@ try {
$amazonUrl = $filament['amazonUrl']; $amazonUrl = $filament['amazonUrl'];
$filamentId = $filament['id']; $filamentId = $filament['id'];
// Scrape price // Use the scraper function to fetch price and discount
$totalPrice = scrapePrice($amazonUrl); $scrapedData = scrapeAmazonData($amazonUrl);
if ($totalPrice !== null && $totalPrice > 0) { if ($scrapedData && $scrapedData['price'] > 0) {
$stmt = $pdo->prepare(" $stmt = $pdo->prepare("
INSERT INTO filamentPriceHistory (filamentId, price) INSERT INTO filamentPriceHistory (filamentId, price, currentDiscount)
VALUES (:filamentId, :price) VALUES (:filamentId, :price, :currentDiscount)
"); ");
$stmt->execute([ $stmt->execute([
':filamentId' => $filamentId, ':filamentId' => $filamentId,
':price' => $totalPrice ':price' => $scrapedData['price'],
':currentDiscount' => $scrapedData['discount']
]); ]);
echo "Updated price for {$filament['filamentName']}: £{$totalPrice}\n"; echo "Updated price for {$filament['filamentName']}: £{$scrapedData['price']}, Discount: {$scrapedData['discount']}%\n";
} else { } else {
echo "Failed to update {$filament['filamentName']} (no price found or £0).\n"; echo "Failed to update {$filament['filamentName']} (no price found or £0).\n";
} }

View File

@@ -67,6 +67,8 @@
</li> </li>
<li><a class="dropdown-item d-flex align-items-center" href="printerForm.php"><i class="bx bx-cog fs-5"></i><span>Add Printer</span></a> <li><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>
<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> <li>
<div class="dropdown-divider mb-0"></div> <div class="dropdown-divider mb-0"></div>
</li> </li>

View File

@@ -21,6 +21,16 @@
</a> </a>
</li> </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 --> <!-- Printing Section -->
<li class="menu-label">Printing Control</li> <li class="menu-label">Printing Control</li>
<li> <li>

View File

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

View File

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

View File

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

View File

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

View File

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