From 030d72c2020068742994a500525705d9b1688f69 Mon Sep 17 00:00:00 2001 From: Hickmeister <35031453+Hickmeister@users.noreply.github.com> Date: Tue, 14 Jan 2025 22:49:09 +0000 Subject: [PATCH] Price tracking Changes --- public/viewFilament.php | 209 ++++++++++--------- src/filamentTracker/addFilament.php | 32 ++- src/filamentTracker/getFilamentPrices.php | 4 +- src/filamentTracker/scraper.php | 57 +++-- src/filamentTracker/updateFilamentPrices.php | 6 +- 5 files changed, 178 insertions(+), 130 deletions(-) diff --git a/public/viewFilament.php b/public/viewFilament.php index 6ac14fc..38be033 100644 --- a/public/viewFilament.php +++ b/public/viewFilament.php @@ -67,9 +67,6 @@ - @@ -140,121 +137,135 @@ 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 prices = data.data[filament].prices.map(entry => entry.price); - const timestamps = data.data[filament].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); + const 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; - } + // 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; + const priceDifference = latestPrice - lastDistinctPrice; - // Price Change Indicator - let priceChangeIndicator; - if (priceDifference > 0) { - priceChangeIndicator = ` - +£${priceDifference.toFixed(2)}`; - } else if (priceDifference < 0) { - priceChangeIndicator = ` - -£${Math.abs(priceDifference).toFixed(2)}`; - } else { - priceChangeIndicator = ` - £0.00`; - } + // Price Change Indicator + let priceChangeIndicator; + if (priceDifference > 0) { + priceChangeIndicator = ` + +£${priceDifference.toFixed(2)}`; + } else if (priceDifference < 0) { + priceChangeIndicator = ` + -£${Math.abs(priceDifference).toFixed(2)}`; + } else { + priceChangeIndicator = ` + £0.00`; + } - const amazonUrl = data.data[filament].amazonUrl || '#'; - const discount = data.data[filament].currentDiscount || 0; // Get discount percentage - const chartId = `chart-${filament.replace(/\s+/g, '-')}`; - const chartColor = getRandomColor(); + const amazonUrl = filamentData.amazonUrl || '#'; - // Create Card HTML - const cardHTML = ` -
-
-
-
- ${filament} -
- £${latestPrice} ${priceChangeIndicator} -
-
-
- ${ - discount > 0 - ? ` - -${discount}% - ` - : '' - } -
+ // Extract discount details + const currentDiscount = filamentData.currentDiscount || {}; + const voucher = currentDiscount.voucher || {}; + const discount = currentDiscount.discount || {}; + + let discountText = ''; + if (voucher.value > 0) { + discountText = voucher.type === 'percentage' + ? `-${voucher.value}% Voucher` + : `-£${voucher.value} Voucher`; + } else if (discount.value > 0) { + discountText = discount.type === 'percentage' + ? `-${discount.value}%` + : `-£${discount.value}`; + } + + const chartId = `chart-${filament.replace(/\s+/g, '-')}`; + const chartColor = getRandomColor(); + + // Create Card HTML + const cardHTML = ` +
+
+
+
+ ${filament} +
+ £${latestPrice} ${priceChangeIndicator} +
+
+
+ ${ + discountText + ? ` + ${discountText} + ` + : '' + }
-
+
- `; +
+ `; - 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 = '

No filament data available.

'; - } - }) - .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 = '

No filament data available.

'; + } + }) + .catch(error => { + console.error("Error fetching filament data:", error); + }); - showSkeletonLoader(); +showSkeletonLoader(); }); - - - \ No newline at end of file diff --git a/src/filamentTracker/addFilament.php b/src/filamentTracker/addFilament.php index ef433ee..c21bdf8 100644 --- a/src/filamentTracker/addFilament.php +++ b/src/filamentTracker/addFilament.php @@ -7,22 +7,25 @@ error_reporting(E_ALL); require '../../vendor/autoload.php'; require '../db.php'; require_once '../envLoader.php'; -require 'scraper.php'; +require 'scraper.php'; // Include the scraper file loadEnv(__DIR__ . '/../../.env'); include '../session_check.php'; checkUserRole(['admin']); +// Start session to get user ID if (!isset($_SESSION['userId'])) { echo json_encode(['status' => 'error', 'message' => 'User not authenticated.']); exit; } +// Check if the request method is POST if ($_SERVER['REQUEST_METHOD'] === 'POST') { $filamentName = $_POST['productName'] ?? ''; $amazonUrl = $_POST['productUrl'] ?? ''; $userId = $_SESSION['userId']; + // Validate the input if (empty($filamentName) || empty($amazonUrl)) { echo json_encode(['status' => 'error', 'message' => 'Filament name and URL are required.']); exit; @@ -36,26 +39,29 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { } $price = $scrapedData['price']; - $discount = $scrapedData['discount']; - $details = $scrapedData['details']; + $currentDiscount = json_encode($scrapedData['currentDiscount']); // Convert discount to JSON + $details = $scrapedData['details'] ?? []; // Extract details or use defaults $brand = $details['Brand'] ?? 'Unknown'; $material = $details['Material'] ?? 'Unknown'; $color = $details['Colour'] ?? 'Unknown'; $filamentWeight = isset($details['Item weight']) ? preg_replace('/[^0-9.]/', '', $details['Item weight']) : 1; - if (stripos($details['Item weight'], 'kilograms') !== false || stripos($details['Item weight'], 'kg') !== false) { - $filamentWeight = $filamentWeight * 1000; // Convert KG to G + if (stripos($details['Item weight'] ?? '', 'kilograms') !== false || stripos($details['Item weight'] ?? '', 'kg') !== false) { + $filamentWeight *= 1000; // Convert KG to G } $itemDiameter = isset($details['Item diameter']) ? preg_replace('/[^0-9.]/', '', $details['Item diameter']) : 1.75; - // Database operations + // Begin transaction for database operations $pdo->beginTransaction(); + + // Check if the filament already exists in the database $stmt = $pdo->prepare("SELECT id FROM filamentTracker WHERE amazonUrl = :amazonUrl"); $stmt->execute([':amazonUrl' => $amazonUrl]); $existingFilament = $stmt->fetch(PDO::FETCH_ASSOC); if ($existingFilament) { + // If the filament exists, update the price and discount $filamentId = $existingFilament['id']; $stmt = $pdo->prepare(" INSERT INTO filamentPriceHistory (filamentId, price, currentDiscount) @@ -64,10 +70,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $stmt->execute([ ':filamentId' => $filamentId, ':price' => $price, - ':currentDiscount' => $discount + ':currentDiscount' => $currentDiscount ]); $message = 'Filament price and discount updated successfully.'; } else { + // If the filament does not exist, insert it into filamentTracker $stmt = $pdo->prepare(" INSERT INTO filamentTracker (userId, filamentName, amazonUrl, filamentWeight, brand, material, color, itemDiameter) VALUES (:userId, :filamentName, :amazonUrl, :filamentWeight, :brand, :material, :color, :itemDiameter) @@ -83,7 +90,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { ':itemDiameter' => $itemDiameter ]); + // Get the ID of the newly inserted filament $filamentId = $pdo->lastInsertId(); + + // Insert the initial price and discount into filamentPriceHistory $stmt = $pdo->prepare(" INSERT INTO filamentPriceHistory (filamentId, price, currentDiscount) VALUES (:filamentId, :price, :currentDiscount) @@ -91,15 +101,19 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $stmt->execute([ ':filamentId' => $filamentId, ':price' => $price, - ':currentDiscount' => $discount + ':currentDiscount' => $currentDiscount ]); $message = "$filamentName added successfully and price with discount tracked."; } + // Commit the transaction $pdo->commit(); echo json_encode(['status' => 'success', 'message' => $message]); } catch (Exception $e) { - $pdo->rollBack(); + // Rollback transaction if any exception occurs + if ($pdo->inTransaction()) { + $pdo->rollBack(); + } echo json_encode(['status' => 'error', 'message' => $e->getMessage()]); } } else { diff --git a/src/filamentTracker/getFilamentPrices.php b/src/filamentTracker/getFilamentPrices.php index cd269dd..d81ce55 100644 --- a/src/filamentTracker/getFilamentPrices.php +++ b/src/filamentTracker/getFilamentPrices.php @@ -28,7 +28,7 @@ try { ROW_NUMBER() OVER (PARTITION BY filamentId ORDER BY recordedAt DESC) as rn FROM filamentPriceHistory ) ranked - WHERE rn <= 280 + WHERE rn <= 380 ) filtered ON fp.filamentId = filtered.filamentId AND fp.recordedAt = filtered.recordedAt ORDER BY ft.filamentName, fp.recordedAt ASC "); @@ -48,7 +48,7 @@ try { 'color' => $filament['color'], 'amazonUrl' => $filament['amazonUrl'], 'prices' => [], - 'currentDiscount' => $filament['currentDiscount'] ?? 0 // Include the latest discount + 'currentDiscount' => $filament['currentDiscount'] ? json_decode($filament['currentDiscount'], true) : null // Decode JSON format ]; } diff --git a/src/filamentTracker/scraper.php b/src/filamentTracker/scraper.php index 43b3bb0..290d198 100644 --- a/src/filamentTracker/scraper.php +++ b/src/filamentTracker/scraper.php @@ -9,36 +9,57 @@ function scrapeAmazonData($url) { try { $crawler = $client->request('GET', $url); + // Filter by `centerCol` + $centerCol = $crawler->filter('#centerCol'); + // 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 = $centerCol->filter('.a-price-whole')->count() ? $centerCol->filter('.a-price-whole')->text() : '0'; + $fraction = $centerCol->filter('.a-price-fraction')->count() ? $centerCol->filter('.a-price-fraction')->text() : '00'; $whole = preg_replace('/[^0-9]/', '', $whole); $fraction = preg_replace('/[^0-9]/', '', $fraction); $fraction = strlen($fraction) === 1 ? $fraction . '0' : substr($fraction, 0, 2); $totalPrice = floatval($whole . '.' . $fraction); // Scrape discount - $discount = $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 - } + $discount = $centerCol->filter('.savingsPercentage')->count() + ? preg_replace('/[^0-9]/', '', $centerCol->filter('.savingsPercentage')->text()) + : 0; - // 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; + // Scrape voucher + $voucherText = ''; + $centerCol->filter('.promoPriceBlockMessage')->each(function ($node) use (&$voucherText) { + $text = $node->text(); + if (preg_match('/Apply (\d+%|£\d+) voucher/', $text, $voucherMatch)) { + $voucherText = $voucherMatch[1]; + } }); + // Parse voucher + $voucherValue = 0; + $voucherType = null; + if (!empty($voucherText)) { + if (strpos($voucherText, '%') !== false) { + $voucherValue = (int) preg_replace('/[^0-9]/', '', $voucherText); // Extract percentage + $voucherType = 'percentage'; + } elseif (strpos($voucherText, '£') !== false) { + $voucherValue = (float) preg_replace('/[^0-9.]/', '', $voucherText); // Extract £ value + $voucherType = 'fixed'; + } + } + return [ 'price' => $totalPrice, - 'discount' => $discount, - 'details' => $details + 'currentDiscount' => [ + 'discount' => [ + 'value' => $discount, + 'type' => 'percentage' + ], + 'voucher' => [ + 'value' => $voucherValue, + 'type' => $voucherType + ] + ], + 'details' => [] // Placeholder for additional details if needed ]; } catch (Exception $e) { return null; // Return null if scraping fails diff --git a/src/filamentTracker/updateFilamentPrices.php b/src/filamentTracker/updateFilamentPrices.php index 3ef4336..438ffaf 100644 --- a/src/filamentTracker/updateFilamentPrices.php +++ b/src/filamentTracker/updateFilamentPrices.php @@ -18,6 +18,8 @@ try { $scrapedData = scrapeAmazonData($amazonUrl); if ($scrapedData && $scrapedData['price'] > 0) { + $currentDiscount = json_encode($scrapedData['voucher']); // Encode discount as JSON + $stmt = $pdo->prepare(" INSERT INTO filamentPriceHistory (filamentId, price, currentDiscount) VALUES (:filamentId, :price, :currentDiscount) @@ -25,10 +27,10 @@ try { $stmt->execute([ ':filamentId' => $filamentId, ':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 { echo "Failed to update {$filament['filamentName']} (no price found or £0).\n"; }