290 lines
13 KiB
HTML
290 lines
13 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}{{ product.name }} - Price Tracker{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<div>
|
|
<h1 class="display-5">{{ product.name }}</h1>
|
|
{% if product.description %}
|
|
<p class="text-muted">{{ product.description }}</p>
|
|
{% endif %}
|
|
</div>
|
|
<div>
|
|
<button class="btn btn-success me-2" onclick="scrapeProduct({{ product.id }})">
|
|
<i class="fas fa-sync-alt me-1"></i>Scrape Now
|
|
</button>
|
|
<a href="{{ url_for('edit_product', product_id=product.id) }}" class="btn btn-outline-primary me-2">
|
|
<i class="fas fa-edit me-1"></i>Edit
|
|
</a>
|
|
<button class="btn btn-outline-danger me-2 delete-product-btn"
|
|
data-product-id="{{ product.id }}"
|
|
data-product-name="{{ product.name }}"
|
|
data-bs-toggle="modal"
|
|
data-bs-target="#deleteModal">
|
|
<i class="fas fa-trash me-1"></i>Delete
|
|
</button>
|
|
<a href="{{ url_for('index') }}" class="btn btn-outline-secondary">
|
|
<i class="fas fa-arrow-left me-1"></i>Back to Dashboard
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<!-- Price Overview -->
|
|
<div class="col-lg-4">
|
|
<div class="card mb-4">
|
|
<div class="card-header">
|
|
<h5 class="mb-0">
|
|
<i class="fas fa-tags me-2"></i>Current Prices
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
{% if latest_prices %}
|
|
{% set price_list = latest_prices.values() | list %}
|
|
{% set min_price = price_list | min(attribute='price') %}
|
|
{% set max_price = price_list | max(attribute='price') %}
|
|
|
|
{% for site_name, price_data in latest_prices.items() %}
|
|
<div class="d-flex justify-content-between align-items-center mb-3 p-3 rounded
|
|
{% if price_data.price == min_price.price %}bg-success bg-opacity-10{% endif %}">
|
|
<div>
|
|
<span class="site-badge {{ site_name }}">{{ site_name.title() }}</span>
|
|
{% if not price_data.availability %}
|
|
<span class="badge bg-warning ms-2">Out of Stock</span>
|
|
{% endif %}
|
|
{% if price_data.price == min_price.price %}
|
|
<span class="badge bg-success ms-2">
|
|
<i class="fas fa-trophy"></i> Best Price
|
|
</span>
|
|
{% endif %}
|
|
</div>
|
|
<div class="text-end">
|
|
<div class="h5 mb-0">£{{ "%.2f"|format(price_data.price) }}</div>
|
|
<small class="text-muted">{{ price_data.timestamp[:10] }}</small>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
|
|
{% if product.target_price %}
|
|
<hr>
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<span class="fw-bold">Target Price:</span>
|
|
<span class="h5 mb-0 text-info">£{{ "%.2f"|format(product.target_price) }}</span>
|
|
</div>
|
|
|
|
{% if min_price.price <= product.target_price %}
|
|
<div class="alert alert-success mt-3 py-2">
|
|
<i class="fas fa-bell me-2"></i>
|
|
<strong>Target Reached!</strong> Best price is at or below your target.
|
|
</div>
|
|
{% else %}
|
|
<div class="alert alert-info mt-3 py-2">
|
|
<i class="fas fa-info-circle me-2"></i>
|
|
You could save <strong>£{{ "%.2f"|format(min_price.price - product.target_price) }}</strong>
|
|
when price drops to target.
|
|
</div>
|
|
{% endif %}
|
|
{% endif %}
|
|
|
|
{% else %}
|
|
<p class="text-muted text-center py-4">
|
|
<i class="fas fa-exclamation-triangle fa-2x mb-3"></i><br>
|
|
No price data available yet.<br>
|
|
<button class="btn btn-primary mt-2" onclick="scrapeProduct({{ product.id }})">
|
|
Get Prices Now
|
|
</button>
|
|
</p>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Product URLs -->
|
|
<div class="card mb-4">
|
|
<div class="card-header">
|
|
<h5 class="mb-0">
|
|
<i class="fas fa-link me-2"></i>Tracked URLs
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
{% for site_name, url in product.urls.items() %}
|
|
<div class="mb-2">
|
|
<span class="site-badge {{ site_name }}">{{ site_name.title() }}</span>
|
|
<a href="{{ url }}" target="_blank" class="btn btn-sm btn-outline-primary ms-2">
|
|
<i class="fas fa-external-link-alt"></i> View
|
|
</a>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Price Chart -->
|
|
<div class="col-lg-8">
|
|
<div class="card mb-4">
|
|
<div class="card-header">
|
|
<h5 class="mb-0">
|
|
<i class="fas fa-chart-line me-2"></i>Price History (Last 30 Days)
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
{% if price_history %}
|
|
<div id="priceChart" style="height: 400px;"></div>
|
|
{% else %}
|
|
<p class="text-muted text-center py-5">
|
|
<i class="fas fa-chart-line fa-3x mb-3"></i><br>
|
|
No price history available yet. Price data will appear here after scraping.
|
|
</p>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Price Statistics -->
|
|
{% if price_stats %}
|
|
<div class="card mb-4">
|
|
<div class="card-header">
|
|
<h5 class="mb-0">
|
|
<i class="fas fa-calculator me-2"></i>Price Statistics (Last 30 Days)
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row">
|
|
{% for site_name, stats in price_stats.items() %}
|
|
<div class="col-md-6 mb-3">
|
|
<div class="card border-0 bg-light">
|
|
<div class="card-body">
|
|
<h6 class="card-title">
|
|
<span class="site-badge {{ site_name }}">{{ site_name.title() }}</span>
|
|
</h6>
|
|
<div class="row">
|
|
<div class="col-6">
|
|
<small class="text-muted">Min Price</small>
|
|
<div class="fw-bold text-success">£{{ "%.2f"|format(stats.min_price) }}</div>
|
|
</div>
|
|
<div class="col-6">
|
|
<small class="text-muted">Max Price</small>
|
|
<div class="fw-bold text-danger">£{{ "%.2f"|format(stats.max_price) }}</div>
|
|
</div>
|
|
<div class="col-6 mt-2">
|
|
<small class="text-muted">Avg Price</small>
|
|
<div class="fw-bold">£{{ "%.2f"|format(stats.avg_price) }}</div>
|
|
</div>
|
|
<div class="col-6 mt-2">
|
|
<small class="text-muted">Data Points</small>
|
|
<div class="fw-bold">{{ stats.data_points }}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Recent Price History -->
|
|
{% if price_history %}
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h5 class="mb-0">
|
|
<i class="fas fa-history me-2"></i>Recent Price Updates
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="table-responsive">
|
|
<table class="table table-hover">
|
|
<thead>
|
|
<tr>
|
|
<th>Site</th>
|
|
<th>Price</th>
|
|
<th>Available</th>
|
|
<th>Date</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for entry in price_history[:20] %}
|
|
<tr>
|
|
<td>
|
|
<span class="site-badge {{ entry.site_name }}">{{ entry.site_name.title() }}</span>
|
|
</td>
|
|
<td class="fw-bold">£{{ "%.2f"|format(entry.price) }}</td>
|
|
<td>
|
|
{% if entry.availability %}
|
|
<span class="badge bg-success">Available</span>
|
|
{% else %}
|
|
<span class="badge bg-warning">Out of Stock</span>
|
|
{% endif %}
|
|
</td>
|
|
<td class="text-muted">{{ entry.timestamp[:16] }}</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{% if price_history|length > 20 %}
|
|
<p class="text-muted text-center mt-3">
|
|
Showing 20 most recent entries of {{ price_history|length }} total.
|
|
</p>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Delete Confirmation Modal -->
|
|
<div class="modal fade" id="deleteModal" tabindex="-1" aria-labelledby="deleteModalLabel" aria-hidden="true">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" id="deleteModalLabel">
|
|
<i class="fas fa-exclamation-triangle me-2 text-warning"></i>Confirm Delete
|
|
</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 delete <strong>"{{ product.name }}"</strong>?</p>
|
|
<div class="alert alert-warning">
|
|
<i class="fas fa-warning me-2"></i>
|
|
<strong>Warning:</strong> This action cannot be undone. All price history for this product will be permanently deleted.
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
<form method="POST" action="{{ url_for('delete_product', product_id=product.id) }}" style="display: inline;">
|
|
<button type="submit" class="btn btn-danger">
|
|
<i class="fas fa-trash me-2"></i>Delete Product
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
{% if chart_json %}
|
|
<script>
|
|
var chartData = {{ chart_json|safe }};
|
|
Plotly.newPlot('priceChart', chartData.data, chartData.layout, {responsive: true});
|
|
</script>
|
|
{% endif %}
|
|
|
|
<script>
|
|
// Handle delete product button
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
const deleteButton = document.querySelector('.delete-product-btn');
|
|
const deleteModal = document.getElementById('deleteModal');
|
|
|
|
if (deleteButton) {
|
|
deleteButton.addEventListener('click', function() {
|
|
// Show modal
|
|
const modal = new bootstrap.Modal(deleteModal);
|
|
modal.show();
|
|
});
|
|
}
|
|
});
|
|
</script>
|
|
{% endblock %}
|