Price tracking Changes

This commit is contained in:
Hickmeister
2025-01-14 22:49:09 +00:00
parent a08c4eba3b
commit 030d72c202
5 changed files with 178 additions and 130 deletions

View File

@@ -67,9 +67,6 @@
<!--Start Back To Top Button--> <!--Start Back To Top Button-->
<a href="javaScript:;" class="back-to-top"><i class='bx bxs-up-arrow-alt'></i></a> <a href="javaScript:;" class="back-to-top"><i class='bx bxs-up-arrow-alt'></i></a>
<!--End Back To Top Button--> <!--End Back To Top Button-->
<footer class="page-footer">
<p class="mb-0">Copyright © 2024. All right reserved.</p>
</footer>
</div> </div>
<!--end wrapper--> <!--end wrapper-->
@@ -140,121 +137,135 @@ document.addEventListener("DOMContentLoaded", function () {
} }
fetch('../src/filamentTracker/getFilamentPrices.php') fetch('../src/filamentTracker/getFilamentPrices.php')
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
if (data.status === 'success') { if (data.status === 'success') {
container.innerHTML = ''; // Clear Skeleton container.innerHTML = ''; // Clear Skeleton
Object.keys(data.data).forEach(filament => { Object.keys(data.data).forEach(filament => {
const prices = data.data[filament].prices.map(entry => entry.price); const filamentData = data.data[filament];
const timestamps = data.data[filament].prices.map(entry => entry.recordedAt); const prices = filamentData.prices.map(entry => parseFloat(entry.price));
const latestPrice = prices[prices.length - 1] || 0; const timestamps = filamentData.prices.map(entry => entry.recordedAt);
const latestPrice = prices[prices.length - 1] || 0;
// Find the last distinct price (where the price actually changed) // Find the last distinct price (where the price actually changed)
let lastDistinctPrice = latestPrice; let lastDistinctPrice = latestPrice;
for (let i = prices.length - 2; i >= 0; i--) { for (let i = prices.length - 2; i >= 0; i--) {
if (prices[i] !== latestPrice) { if (prices[i] !== latestPrice) {
lastDistinctPrice = prices[i]; lastDistinctPrice = prices[i];
break; break;
}
} }
}
const priceDifference = latestPrice - lastDistinctPrice; const priceDifference = latestPrice - lastDistinctPrice;
// Price Change Indicator // 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 = filamentData.amazonUrl || '#';
const discount = data.data[filament].currentDiscount || 0; // Get discount percentage
const chartId = `chart-${filament.replace(/\s+/g, '-')}`;
const chartColor = getRandomColor();
// Create Card HTML // Extract discount details
const cardHTML = ` const currentDiscount = filamentData.currentDiscount || {};
<div class="col-lg-4 mb-1"> const voucher = currentDiscount.voucher || {};
<div class="card radius-10 overflow-hidden"> const discount = currentDiscount.discount || {};
<div class="card-body d-flex align-items-center justify-content-between">
<div> let discountText = '';
<a href="${amazonUrl}" target="_blank" class="mb-0 filament-name">${filament}</a> if (voucher.value > 0) {
<h5 class="mb-0"> discountText = voucher.type === 'percentage'
£${latestPrice} ${priceChangeIndicator} ? `-${voucher.value}% Voucher`
</h5> : `-£${voucher.value} Voucher`;
</div> } else if (discount.value > 0) {
<div> discountText = discount.type === 'percentage'
${ ? `-${discount.value}%`
discount > 0 : `-£${discount.value}`;
? `<span class="text-success fw-bold" style="font-size: 1rem;"> }
-${discount}%
</span>` const chartId = `chart-${filament.replace(/\s+/g, '-')}`;
: '' const chartColor = getRandomColor();
}
</div> // 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>`
: ''
}
</div> </div>
<div id="${chartId}"></div>
</div> </div>
<div id="${chartId}"></div>
</div> </div>
`; </div>
`;
container.insertAdjacentHTML('beforeend', cardHTML); container.insertAdjacentHTML('beforeend', cardHTML);
// Chart Rendering // Chart Rendering
const chartOptions = { const chartOptions = {
series: [{ name: "Price", data: prices }], series: [{ name: "Price", data: prices }],
chart: { chart: {
type: "area", type: "area",
height: 110, height: 110,
toolbar: { show: false }, toolbar: { show: false },
zoom: { enabled: false }, zoom: { enabled: false },
sparkline: { enabled: true }, sparkline: { enabled: true },
}, },
markers: { size: 0 }, markers: { size: 0 },
dataLabels: { enabled: false }, dataLabels: { enabled: false },
stroke: { show: true, width: 2.4, curve: "smooth" }, stroke: { show: true, width: 2.4, curve: "smooth" },
colors: [chartColor], colors: [chartColor],
xaxis: { categories: timestamps, labels: { show: false } }, xaxis: { categories: timestamps, labels: { show: false } },
fill: { opacity: 1 }, fill: { opacity: 1 },
tooltip: { tooltip: {
theme: "dark", theme: "dark",
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); const chartElement = document.getElementById(chartId);
if (chartElement) { if (chartElement) {
const chart = new ApexCharts(chartElement, chartOptions); const chart = new ApexCharts(chartElement, chartOptions);
chart.render(); chart.render();
} }
}); });
} else { } else {
container.innerHTML = '<p class="text-center">No filament data available.</p>'; container.innerHTML = '<p class="text-center">No filament data available.</p>';
} }
}) })
.catch(error => { .catch(error => {
console.error("Error fetching filament data:", error); console.error("Error fetching filament data:", error);
}); });
showSkeletonLoader(); showSkeletonLoader();
}); });
</script> </script>
</body> </body>
</html> </html>

View File

@@ -7,22 +7,25 @@ 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'; require 'scraper.php'; // Include the scraper file
loadEnv(__DIR__ . '/../../.env'); loadEnv(__DIR__ . '/../../.env');
include '../session_check.php'; include '../session_check.php';
checkUserRole(['admin']); checkUserRole(['admin']);
// Start session to get user ID
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 if the request method is POST
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'];
// Validate the input
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;
@@ -36,26 +39,29 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
} }
$price = $scrapedData['price']; $price = $scrapedData['price'];
$discount = $scrapedData['discount']; $currentDiscount = json_encode($scrapedData['currentDiscount']); // Convert discount to JSON
$details = $scrapedData['details']; $details = $scrapedData['details'] ?? [];
// Extract details or use defaults // Extract details or use defaults
$brand = $details['Brand'] ?? 'Unknown'; $brand = $details['Brand'] ?? 'Unknown';
$material = $details['Material'] ?? 'Unknown'; $material = $details['Material'] ?? 'Unknown';
$color = $details['Colour'] ?? 'Unknown'; $color = $details['Colour'] ?? 'Unknown';
$filamentWeight = isset($details['Item weight']) ? preg_replace('/[^0-9.]/', '', $details['Item weight']) : 1; $filamentWeight = isset($details['Item weight']) ? preg_replace('/[^0-9.]/', '', $details['Item weight']) : 1;
if (stripos($details['Item weight'], 'kilograms') !== false || stripos($details['Item weight'], 'kg') !== false) { if (stripos($details['Item weight'] ?? '', 'kilograms') !== false || stripos($details['Item weight'] ?? '', 'kg') !== false) {
$filamentWeight = $filamentWeight * 1000; // Convert KG to G $filamentWeight *= 1000; // Convert KG to G
} }
$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;
// Database operations // Begin transaction for database operations
$pdo->beginTransaction(); $pdo->beginTransaction();
// Check if the filament already exists in the database
$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) {
// If the filament exists, update the price and discount
$filamentId = $existingFilament['id']; $filamentId = $existingFilament['id'];
$stmt = $pdo->prepare(" $stmt = $pdo->prepare("
INSERT INTO filamentPriceHistory (filamentId, price, currentDiscount) INSERT INTO filamentPriceHistory (filamentId, price, currentDiscount)
@@ -64,10 +70,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$stmt->execute([ $stmt->execute([
':filamentId' => $filamentId, ':filamentId' => $filamentId,
':price' => $price, ':price' => $price,
':currentDiscount' => $discount ':currentDiscount' => $currentDiscount
]); ]);
$message = 'Filament price and discount updated successfully.'; $message = 'Filament price and discount updated successfully.';
} else { } else {
// If the filament does not exist, insert it 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)
@@ -83,7 +90,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
':itemDiameter' => $itemDiameter ':itemDiameter' => $itemDiameter
]); ]);
// Get the ID of the newly inserted filament
$filamentId = $pdo->lastInsertId(); $filamentId = $pdo->lastInsertId();
// Insert the initial price and discount into filamentPriceHistory
$stmt = $pdo->prepare(" $stmt = $pdo->prepare("
INSERT INTO filamentPriceHistory (filamentId, price, currentDiscount) INSERT INTO filamentPriceHistory (filamentId, price, currentDiscount)
VALUES (:filamentId, :price, :currentDiscount) VALUES (:filamentId, :price, :currentDiscount)
@@ -91,15 +101,19 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$stmt->execute([ $stmt->execute([
':filamentId' => $filamentId, ':filamentId' => $filamentId,
':price' => $price, ':price' => $price,
':currentDiscount' => $discount ':currentDiscount' => $currentDiscount
]); ]);
$message = "$filamentName added successfully and price with discount tracked."; $message = "$filamentName added successfully and price with discount 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(); // Rollback transaction if any exception occurs
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
echo json_encode(['status' => 'error', 'message' => $e->getMessage()]); echo json_encode(['status' => 'error', 'message' => $e->getMessage()]);
} }
} else { } else {

View File

@@ -28,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 <= 280 WHERE rn <= 380
) 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
"); ");
@@ -48,7 +48,7 @@ try {
'color' => $filament['color'], 'color' => $filament['color'],
'amazonUrl' => $filament['amazonUrl'], 'amazonUrl' => $filament['amazonUrl'],
'prices' => [], 'prices' => [],
'currentDiscount' => $filament['currentDiscount'] ?? 0 // Include the latest discount 'currentDiscount' => $filament['currentDiscount'] ? json_decode($filament['currentDiscount'], true) : null // Decode JSON format
]; ];
} }

View File

@@ -9,36 +9,57 @@ function scrapeAmazonData($url) {
try { try {
$crawler = $client->request('GET', $url); $crawler = $client->request('GET', $url);
// Filter by `centerCol`
$centerCol = $crawler->filter('#centerCol');
// Scrape price // Scrape price
$whole = $crawler->filter('.a-price-whole')->count() ? $crawler->filter('.a-price-whole')->text() : '0'; $whole = $centerCol->filter('.a-price-whole')->count() ? $centerCol->filter('.a-price-whole')->text() : '0';
$fraction = $crawler->filter('.a-price-fraction')->count() ? $crawler->filter('.a-price-fraction')->text() : '00'; $fraction = $centerCol->filter('.a-price-fraction')->count() ? $centerCol->filter('.a-price-fraction')->text() : '00';
$whole = preg_replace('/[^0-9]/', '', $whole); $whole = preg_replace('/[^0-9]/', '', $whole);
$fraction = preg_replace('/[^0-9]/', '', $fraction); $fraction = preg_replace('/[^0-9]/', '', $fraction);
$fraction = strlen($fraction) === 1 ? $fraction . '0' : substr($fraction, 0, 2); $fraction = strlen($fraction) === 1 ? $fraction . '0' : substr($fraction, 0, 2);
$totalPrice = floatval($whole . '.' . $fraction); $totalPrice = floatval($whole . '.' . $fraction);
// Scrape discount // Scrape discount
$discount = $crawler->filter('.savingsPercentage')->count() $discount = $centerCol->filter('.savingsPercentage')->count()
? $crawler->filter('.savingsPercentage')->text() ? preg_replace('/[^0-9]/', '', $centerCol->filter('.savingsPercentage')->text())
: null; : 0;
if ($discount) {
$discount = preg_replace('/[^0-9]/', '', $discount);
} else {
$discount = 0; // Default to 0 if no discount is found
}
// Scrape additional details // Scrape voucher
$details = []; $voucherText = '';
$crawler->filter('table.a-normal tr')->each(function ($node) use (&$details) { $centerCol->filter('.promoPriceBlockMessage')->each(function ($node) use (&$voucherText) {
$label = trim($node->filter('td.a-span3')->text()); $text = $node->text();
$value = trim($node->filter('td.a-span9')->text()); if (preg_match('/Apply (\d+%|£\d+) voucher/', $text, $voucherMatch)) {
$details[$label] = $value; $voucherText = $voucherMatch[1];
}
}); });
// Parse voucher
$voucherValue = 0;
$voucherType = null;
if (!empty($voucherText)) {
if (strpos($voucherText, '%') !== false) {
$voucherValue = (int) preg_replace('/[^0-9]/', '', $voucherText); // Extract percentage
$voucherType = 'percentage';
} elseif (strpos($voucherText, '£') !== false) {
$voucherValue = (float) preg_replace('/[^0-9.]/', '', $voucherText); // Extract £ value
$voucherType = 'fixed';
}
}
return [ return [
'price' => $totalPrice, 'price' => $totalPrice,
'discount' => $discount, 'currentDiscount' => [
'details' => $details 'discount' => [
'value' => $discount,
'type' => 'percentage'
],
'voucher' => [
'value' => $voucherValue,
'type' => $voucherType
]
],
'details' => [] // Placeholder for additional details if needed
]; ];
} catch (Exception $e) { } catch (Exception $e) {
return null; // Return null if scraping fails return null; // Return null if scraping fails

View File

@@ -18,6 +18,8 @@ try {
$scrapedData = scrapeAmazonData($amazonUrl); $scrapedData = scrapeAmazonData($amazonUrl);
if ($scrapedData && $scrapedData['price'] > 0) { if ($scrapedData && $scrapedData['price'] > 0) {
$currentDiscount = json_encode($scrapedData['voucher']); // Encode discount as JSON
$stmt = $pdo->prepare(" $stmt = $pdo->prepare("
INSERT INTO filamentPriceHistory (filamentId, price, currentDiscount) INSERT INTO filamentPriceHistory (filamentId, price, currentDiscount)
VALUES (:filamentId, :price, :currentDiscount) VALUES (:filamentId, :price, :currentDiscount)
@@ -25,10 +27,10 @@ try {
$stmt->execute([ $stmt->execute([
':filamentId' => $filamentId, ':filamentId' => $filamentId,
':price' => $scrapedData['price'], ':price' => $scrapedData['price'],
':currentDiscount' => $scrapedData['discount'] ':currentDiscount' => $currentDiscount
]); ]);
echo "Updated price for {$filament['filamentName']}: £{$scrapedData['price']}, Discount: {$scrapedData['discount']}%\n"; echo "Updated price for {$filament['filamentName']}: £{$scrapedData['price']}, Discount: " . json_encode($scrapedData['voucher']) . "\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";
} }