Update Filament Dryer
This commit is contained in:
@@ -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>
|
||||
<!-- Start Page Content -->
|
||||
|
||||
<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>
|
||||
<!-- 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="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="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="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>
|
||||
<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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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']
|
||||
];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user