feat: авто-загрузка из папки, алерты об изменении цен, экспорт каталога в Excel
- POST /scan: сервис забирает новые прайсы из папки (настройка watch_folder), журнал ingested_file исключает повторную обработку; вызывается кроном. - GET /price-changes: услуги, где цена изменилась не меньше порога между прайсами (детектор аномалий из ТЗ §4.4, вынесенный в интерфейс). - GET /export.xlsx: единый сравнительный каталог услуг и цен (openpyxl). - GET/POST /settings: папка авто-загрузки. - админка: карточки авто-загрузки и изменений цен + кнопка экспорта. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+72
-2
@@ -10,7 +10,7 @@
|
||||
<script>
|
||||
tailwind.config = { theme: { extend: {
|
||||
colors: { brand: { DEFAULT: '#ff4713', soft: '#ffede7', dark: '#e63e0f' },
|
||||
tg: '#6b788e', lg: '#f5f7f9', ink: '#111827' },
|
||||
tg: '#6b788e', lg: '#f5f7f9', ink: '#111827', ok: '#0f973d' },
|
||||
fontFamily: { sans: ['Geologica', 'ui-sans-serif', 'system-ui', 'sans-serif'] } } } };
|
||||
</script>
|
||||
<style>body{font-family:'Geologica',system-ui,sans-serif}</style>
|
||||
@@ -48,7 +48,10 @@
|
||||
|
||||
<!-- Админка + верификация -->
|
||||
<section id="view-admin" class="hidden">
|
||||
<h2 class="text-2xl font-bold mt-6">Дашборд обработки</h2>
|
||||
<div class="flex items-center justify-between mt-6 gap-4 flex-wrap">
|
||||
<h2 class="text-2xl font-bold">Дашборд обработки</h2>
|
||||
<button onclick="exportXlsx()" class="bg-white border border-[#d1d5db] text-sm rounded-lg px-4 py-2 font-medium hover:bg-lg">📥 Экспорт в Excel</button>
|
||||
</div>
|
||||
<div id="stats" class="grid grid-cols-2 sm:grid-cols-4 gap-3 mt-4"></div>
|
||||
|
||||
<!-- Загрузка нового архива (ТЗ §4.1) -->
|
||||
@@ -66,6 +69,29 @@
|
||||
<div id="ingest-result" class="mt-3 text-sm hidden"></div>
|
||||
</div>
|
||||
|
||||
<div class="grid md:grid-cols-2 gap-4 mt-4">
|
||||
<!-- Авто-загрузка из папки (watcher + крон) -->
|
||||
<div class="bg-white border border-[#ebedf0] rounded-2xl shadow-sm p-5">
|
||||
<h3 class="text-lg font-semibold">Авто-загрузка из папки</h3>
|
||||
<p class="text-sm text-tg mt-1">Укажите папку — система в кроне сама заберёт и обработает свежие прайсы, без участия человека.</p>
|
||||
<div class="flex gap-2 mt-3">
|
||||
<input id="watchfolder" placeholder="например: /app/inbox" class="flex-1 bg-white border border-[#d1d5db] rounded-lg px-3 py-2 text-sm outline-none focus:border-brand">
|
||||
<button onclick="saveFolder()" class="border border-[#e5e7eb] text-sm rounded-lg px-3 py-2 hover:bg-lg shrink-0">Сохранить</button>
|
||||
</div>
|
||||
<button onclick="scanNow()" class="bg-brand text-white text-sm rounded-lg px-4 py-2 mt-2 font-medium hover:bg-brand-dark">Проверить сейчас</button>
|
||||
<div id="scan-result" class="text-sm mt-2"></div>
|
||||
</div>
|
||||
<!-- Изменения цен (детектор аномалий, ТЗ §4.4) -->
|
||||
<div class="bg-white border border-[#ebedf0] rounded-2xl shadow-sm p-5">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-lg font-semibold">Изменения цен</h3>
|
||||
<span id="pc-count" class="text-xs text-tg"></span>
|
||||
</div>
|
||||
<p class="text-sm text-tg mt-1">Услуги, где новый прайс изменил цену на ≥ 10% относительно прошлого.</p>
|
||||
<div id="price-changes" class="mt-3 space-y-1 max-h-64 overflow-auto"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between mt-8">
|
||||
<h3 class="text-lg font-semibold">Очередь верификации</h3>
|
||||
<div id="vprogress" class="text-sm text-tg"></div>
|
||||
@@ -158,6 +184,50 @@ async function loadAdmin() {
|
||||
<div class="text-2xl font-bold text-brand">${v}</div>
|
||||
<div class="text-xs text-tg mt-1">${l}</div></div>`).join('');
|
||||
loadQueue();
|
||||
loadSettings();
|
||||
loadPriceChanges();
|
||||
}
|
||||
|
||||
function exportXlsx() { window.location = `${API}/export.xlsx`; }
|
||||
|
||||
async function loadSettings() {
|
||||
const s = await fetch(`${API}/settings`).then(r => r.json());
|
||||
$('watchfolder').value = s.watch_folder || '';
|
||||
}
|
||||
|
||||
async function saveFolder() {
|
||||
const folder = $('watchfolder').value.trim();
|
||||
await fetch(`${API}/settings?watch_folder=${encodeURIComponent(folder)}`, { method: 'POST' });
|
||||
$('scan-result').innerHTML = '<span class="text-tg">Папка сохранена.</span>';
|
||||
}
|
||||
|
||||
async function scanNow() {
|
||||
const box = $('scan-result');
|
||||
box.innerHTML = '<span class="text-tg">⏳ Проверяю папку…</span>';
|
||||
const res = await fetch(`${API}/scan`, { method: 'POST' });
|
||||
const r = await res.json().catch(() => ({}));
|
||||
if (!res.ok) { box.innerHTML = `<span class="text-[#dc2626]">${r.detail || 'ошибка'}</span>`; return; }
|
||||
if (!r.new_files) { box.innerHTML = '<span class="text-tg">Новых прайсов нет.</span>'; return; }
|
||||
box.innerHTML = `<span class="text-ok">✓ Обработано файлов: <b>${r.new_files}</b>, позиций: <b>${r.items}</b>, нормализовано <b>${r.auto_matched_pct}%</b>.</span>`;
|
||||
loadAdmin();
|
||||
}
|
||||
|
||||
async function loadPriceChanges() {
|
||||
const d = await fetch(`${API}/price-changes?threshold=10&limit=40`).then(r => r.json());
|
||||
$('pc-count').textContent = d.count ? `${d.count} услуг` : '';
|
||||
if (!d.changes || !d.changes.length) {
|
||||
$('price-changes').innerHTML = '<div class="text-tg text-sm">Изменений не найдено.</div>';
|
||||
return;
|
||||
}
|
||||
$('price-changes').innerHTML = d.changes.map(c => {
|
||||
const up = c.delta_pct > 0;
|
||||
const badge = `<span class="${up ? 'text-[#dc2626]' : 'text-ok'} font-semibold whitespace-nowrap">${up ? '▲ +' : '▼ '}${c.delta_pct}%</span>`;
|
||||
return `<div class="flex items-center justify-between gap-3 border-b border-[#f0f0f0] py-1.5">
|
||||
<div class="min-w-0">
|
||||
<div class="text-sm truncate">${c.service}</div>
|
||||
<div class="text-xs text-tg">${c.partner_name} · ${money(c.old_price)} → ${money(c.new_price)}</div>
|
||||
</div>${badge}</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
async function uploadArchive(input) {
|
||||
|
||||
Reference in New Issue
Block a user