Update Filament Dryer

This commit is contained in:
Hickmeister
2025-01-15 17:50:57 +00:00
parent cc4e915cf5
commit 053662c2e8
3 changed files with 454 additions and 329 deletions

View File

@@ -46,132 +46,177 @@ checkUserRole(['admin']);
<!--start page wrapper -->
<div class="page-wrapper">
<div class="page-content">
<!--start page content -->
<div class="container mt-4">
<div class="row">
<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-4">
<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-4">
<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 Form -->
<div class="col-lg-5">
<div class="card radius-10 overflow-hidden">
<div class="card-header bg-primary text-white">
<h5 class="mb-0">Filament Dryer Control</h5>
</div>
<div class="card-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>
<button type="button" class="btn btn-danger" id="dryerOff">Turn Off Dryer</button>
</div>
</form>
</div>
</div>
<!-- Progress Bar Section -->
<div class="card mt-4 radius-10 overflow-hidden">
<div class="card-header bg-primary text-white d-flex justify-content-between align-items-center">
<h5 class="mb-0">Drying Progress</h5>
</div>
<div class="card-body">
<div class="progress mb-2">
<div class="progress-bar bg-success" id="dryingProgress" role="progressbar" style="width: 0%;" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div>
</div>
<p class="mb-0">Time Set: <span id="dryingTimeDisplay">--</span></p>
<p class="mb-0">Remaining Time: <span id="remainingTimeDisplay">--</span></p>
</div>
</div>
</div>
<!-- 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>
<!--end page content -->
<!-- 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 -->
@@ -213,10 +258,13 @@ checkUserRole(['admin']);
$(".data-attributes span").peity("donut")
</script>
<script>
document.addEventListener("DOMContentLoaded", function () {
<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() {
@@ -232,7 +280,7 @@ document.addEventListener("DOMContentLoaded", function () {
updateTemperatureCard(data);
updateHumidityCard(data);
updateStatusCards(data);
updateProgressBar(data); // Update progress bar and time displays
updateChartProgress(data); // Update chart for drying progress
} else {
console.error("Dryer offline or no data available");
showOfflineStatus();
@@ -267,105 +315,167 @@ document.addEventListener("DOMContentLoaded", function () {
.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.');
});
}
// Prevent Form Submission Refresh
// Handle Form Submission for Starting Drying
const dryerControlForm = document.getElementById("dryerControlForm");
if (dryerControlForm) {
dryerControlForm.addEventListener("submit", function (event) {
event.preventDefault();
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 filamentPreset = formData.get("filamentPreset");
const dryingTimeInHours = parseFloat(formData.get("dryingTime"));
const targetTemperature = parseFloat(formData.get("temperature"));
// 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 (!filamentPreset || isNaN(dryingTimeInHours) || isNaN(targetTemperature)) {
alert("Please complete all required fields.");
return;
}
// 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);
// Convert drying time to minutes
const dryingTimeInMinutes = Math.round(dryingTimeInHours * 60);
// Control Dryer
controlDryer(dryerOn, targetTemperature, dryingTimeInMinutes);
});
}
// Control Dryer
controlDryer(dryerOn, targetTemperature, dryingTimeInMinutes);
});
// Handle Turn Off Dryer
const dryerOffButton = document.getElementById("dryerOff");
if (dryerOffButton) {
dryerOffButton.addEventListener("click", function () {
controlDryer(false, 0, 0); // Turn off the dryer
});
}
// 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");
if (filamentPresetSelect && temperatureInput) {
filamentPresetSelect.addEventListener("change", function () {
const selectedOption = filamentPresetSelect.options[filamentPresetSelect.selectedIndex];
const presetTemperature = selectedOption.getAttribute("data-temp");
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"
}
});
}
if (presetTemperature) {
temperatureInput.value = presetTemperature; // Auto-fill temperature input
} else {
temperatureInput.value = ""; // Clear the temperature input for "Custom"
}
});
// Update Progress Bar and Time Display
function updateProgressBar(data) {
const progressBar = document.getElementById("dryingProgress");
const dryingTimeEl = document.getElementById("dryingTimeDisplay"); // Total drying time display
const remainingTimeDisplay = document.getElementById("remainingTimeDisplay"); // Remaining time display
// 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
// Reset progress bar if the dryer is turned off
if (!data.dryerOn) {
if (progressBar) {
progressBar.style.width = "0%";
progressBar.setAttribute("aria-valuenow", 0);
}
if (dryingTimeEl) {
dryingTimeEl.innerText = "Total: 0h 0m";
}
if (remainingTimeDisplay) {
remainingTimeDisplay.innerText = "Remaining: 0h 0m";
}
return; // Exit early as no further updates are needed
// 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
}
}
}
}
}
});
}
// Update progress bar when the dryer is on
if (progressBar && dryingTime > 0) {
const progressPercent = ((dryingTime - remainingTime) / dryingTime) * 100;
progressBar.style.width = `${progressPercent}%`;
progressBar.setAttribute("aria-valuenow", progressPercent.toFixed(0));
}
// Update time displays
if (dryingTimeEl) {
dryingTimeEl.innerText = `Total: ${convertToHoursAndMinutes(dryingTime)}`;
}
if (remainingTimeDisplay) {
remainingTimeDisplay.innerText = `Remaining: ${convertToHoursAndMinutes(remainingTime)}`;
// Display drying time below the chart
const timeSetElement = document.getElementById('dryingTimeDisplay');
if (timeSetElement) {
timeSetElement.innerText = `Time Set: ${dryingTimeFormatted}`;
}
}
// Convert minutes to hours and minutes
// 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;
@@ -384,6 +494,7 @@ document.addEventListener("DOMContentLoaded", function () {
series: [{ name: "Temperature", data: tempHistory }],
chart: {
type: 'area',
color: '#1df00a',
height: 150,
toolbar: { show: false },
sparkline: { enabled: true },
@@ -440,7 +551,7 @@ document.addEventListener("DOMContentLoaded", function () {
}
}
// Update Status Cards
// Update Status Cards (Dryer, Heater, Fan)
function updateStatusCards(data) {
const dryerStatusCard = document.querySelector('.card:nth-child(1)');
const heaterStatusCard = document.getElementById('heaterRelayStatus');
@@ -474,7 +585,6 @@ document.addEventListener("DOMContentLoaded", function () {
const dryerStatusCard = document.querySelector('.card:nth-child(1)');
const heaterStatusCard = document.getElementById('heaterRelayStatus');
const fanStatusCard = document.getElementById('fanRelayStatus');
const progressBar = document.getElementById("dryingProgress");
if (dryerStatusCard) {
dryerStatusCard.classList.remove("bg-success");
@@ -493,11 +603,6 @@ document.addEventListener("DOMContentLoaded", function () {
fanStatusCard.classList.add("bg-danger");
fanStatusCard.querySelector("h5").innerText = "Offline";
}
if (progressBar) {
progressBar.style.width = "0%";
progressBar.setAttribute("aria-valuenow", 0);
}
}
// Fetch data every 10 seconds
@@ -505,7 +610,8 @@ document.addEventListener("DOMContentLoaded", function () {
setInterval(fetchDryerStatus, 10000);
});
</script>
</script>
</body>

View File

@@ -137,135 +137,147 @@ document.addEventListener("DOMContentLoaded", function () {
}
fetch('../src/filamentTracker/getFilamentPrices.php')
.then(response => response.json())
.then(data => {
if (data.status === 'success') {
container.innerHTML = ''; // Clear Skeleton
.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);
const latestPrice = prices[prices.length - 1] || 0;
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;
// 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;
// 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
}
}
}
const priceDifference = latestPrice - lastDistinctPrice;
// Ensure price doesn't go below zero
latestPrice = Math.max(latestPrice, 0);
// 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>`;
}
// 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 amazonUrl = filamentData.amazonUrl || '#';
const priceDifference = latestPrice - lastDistinctPrice;
// Extract discount details
const currentDiscount = filamentData.currentDiscount || {};
const voucher = currentDiscount.voucher || {};
const discount = currentDiscount.discount || {};
// 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>`;
}
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 amazonUrl = filamentData.amazonUrl || '#';
const chartId = `chart-${filament.replace(/\s+/g, '-')}`;
const chartColor = getRandomColor();
// 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}`;
}
// 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} ${priceChangeIndicator}
</h5>
</div>
<div>
${
discountText
? `<span class="text-success fw-bold" style="font-size: 1rem;">
${discountText}
</span>`
: ''
}
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 id="${chartId}"></div>
</div>
</div>
`;
`;
container.insertAdjacentHTML('beforeend', cardHTML);
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()}`;
// 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);
});
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();
showSkeletonLoader();
});
</script>
</body>
</html>

View File

@@ -9,35 +9,42 @@ include '../src/session_check.php';
header('Content-Type: application/json');
try {
// Fetch all filament prices with the latest discount
$stmt = $pdo->query("
SELECT ft.filamentName,
// 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.currentDiscount,
fp.recordedAt
FROM filamentTracker ft
JOIN filamentPriceHistory fp ON ft.id = fp.filamentId
JOIN (
SELECT filamentId, recordedAt
FROM (
SELECT filamentId, recordedAt,
ROW_NUMBER() OVER (PARTITION BY filamentId ORDER BY recordedAt DESC) as rn
FROM filamentPriceHistory
) ranked
WHERE rn <= 380
) filtered ON fp.filamentId = filtered.filamentId AND fp.recordedAt = filtered.recordedAt
ORDER BY ft.filamentName, fp.recordedAt ASC
ORDER BY ft.filamentName ASC, fp.recordedAt ASC;
");
$filaments = $stmt1->fetchAll(PDO::FETCH_ASSOC);
$filaments = $stmt->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'];
@@ -48,15 +55,15 @@ try {
'color' => $filament['color'],
'amazonUrl' => $filament['amazonUrl'],
'prices' => [],
'currentDiscount' => $filament['currentDiscount'] ? json_decode($filament['currentDiscount'], true) : [
'currentDiscount' => $discountMap[$filament['filamentId']] ?? [
'discount' => ['value' => 0, 'type' => 'none'],
'voucher' => ['value' => 0, 'type' => 'none']
] // Decode JSON format and provide fallback
]
];
}
$result[$name]['prices'][] = [
'price' => $filament['price'],
'price' => (float)$filament['price'],
'recordedAt' => $filament['recordedAt']
];
}