Add price refresh functionality to configurator

- Add price_updated_at field to qt_configurations table to track when prices were last updated
- Add RefreshPrices() method in configuration service to update all component prices with current values from database
- Add POST /api/configs/:uuid/refresh-prices API endpoint for price updates
- Add "Refresh Prices" button in configurator UI next to Save button
- Display last price update timestamp in human-readable format (e.g., "5 min ago", "2 hours ago")
- Create migration 004_add_price_updated_at.sql for database schema update
- Update CLAUDE.md documentation with new API endpoint and schema changes
- Add MIGRATION_PRICE_REFRESH.md with detailed migration instructions

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-31 10:31:00 +03:00
parent 3132ab2fa2
commit f31ae69233
8 changed files with 310 additions and 18 deletions

View File

@@ -14,10 +14,14 @@
<span id="config-name">Конфигуратор</span>
</h1>
</div>
<div id="save-buttons" class="hidden space-x-2">
<div id="save-buttons" class="hidden flex items-center space-x-2">
<button onclick="refreshPrices()" class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">
Пересчитать цену
</button>
<button onclick="saveConfig()" class="px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700">
Сохранить
</button>
<span id="price-update-date" class="text-sm text-gray-500"></span>
</div>
</div>
@@ -315,6 +319,11 @@ document.addEventListener('DOMContentLoaded', async function() {
if (config.custom_price) {
document.getElementById('custom-price-input').value = config.custom_price.toFixed(2);
}
// Display price update date if available
if (config.price_updated_at) {
updatePriceUpdateDate(config.price_updated_at);
}
} catch(e) {
showToast('Ошибка загрузки конфигурации', 'error');
window.location.href = '/configs';
@@ -1298,6 +1307,86 @@ async function exportCSVWithCustomPrice() {
}
}
async function refreshPrices() {
const token = localStorage.getItem('token');
if (!token || !configUUID) return;
try {
const resp = await fetch('/api/configs/' + configUUID + '/refresh-prices', {
method: 'POST',
headers: {
'Authorization': 'Bearer ' + token,
'Content-Type': 'application/json'
}
});
if (resp.status === 401) {
window.location.href = '/login';
return;
}
if (!resp.ok) {
showToast('Ошибка обновления цен', 'error');
return;
}
const config = await resp.json();
// Update cart with new prices
if (config.items && config.items.length > 0) {
cart = config.items.map(item => ({
lot_name: item.lot_name,
quantity: item.quantity,
unit_price: item.unit_price,
description: item.description || '',
category: item.category || getCategoryFromLotName(item.lot_name)
}));
}
// Update price update date
if (config.price_updated_at) {
updatePriceUpdateDate(config.price_updated_at);
}
// Re-render UI
renderTab();
updateCartUI();
showToast('Цены обновлены', 'success');
} catch(e) {
showToast('Ошибка обновления цен', 'error');
}
}
function updatePriceUpdateDate(dateStr) {
if (!dateStr) {
document.getElementById('price-update-date').textContent = '';
return;
}
const date = new Date(dateStr);
const now = new Date();
const diffMs = now - date;
const diffMins = Math.floor(diffMs / 60000);
const diffHours = Math.floor(diffMs / 3600000);
const diffDays = Math.floor(diffMs / 86400000);
let timeAgo;
if (diffMins < 1) {
timeAgo = 'только что';
} else if (diffMins < 60) {
timeAgo = diffMins + ' мин. назад';
} else if (diffHours < 24) {
timeAgo = diffHours + ' ч. назад';
} else if (diffDays < 7) {
timeAgo = diffDays + ' дн. назад';
} else {
timeAgo = date.toLocaleDateString('ru-RU');
}
document.getElementById('price-update-date').textContent = 'Обновлено: ' + timeAgo;
}
</script>
{{end}}