Files
PriceForge/web/templates/competitors.html
Mikhail Chusavitin f48615e8a9 Modularize Go files, extract JS to static, implement competitor pricelists
Go refactoring:
- Split handlers/pricing.go (2446→291 lines) into 5 focused files
- Split services/stock_import.go (1334→~400 lines) into stock_mappings.go + stock_parse.go
- Split services/sync/service.go (1290→~250 lines) into 3 files

JS extraction:
- Move all inline <script> blocks to web/static/js/ (6 files)
- Templates reduced: admin_pricing 2873→521, lot 1531→304, vendor_mappings 1063→169, etc.

Competitor pricelists (migrations 033-039):
- qt_competitors + partnumber_log_competitors tables
- Excel import with column mapping, dedup, bulk insert
- p/n→lot resolution via weighted_median, discount applied
- Unmapped p/ns written to qt_vendor_partnumber_seen
- Quote counts (unique/total) shown on /admin/competitors
- price_method="weighted_median", price_period_days=0 stored explicitly

Fix price_method/price_period_days for warehouse items:
- warehouse: weighted_avg, period=0
- competitor: weighted_median, period=0
- Removes misleading DB defaults (was: median/90)

Update bible: architecture.md, pricelist.md, history.md

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-13 07:44:10 +03:00

188 lines
13 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{{define "title"}}Конкуренты - PriceForge{{end}}
{{define "content"}}
<div class="space-y-4">
<div class="flex items-center justify-between">
<h1 class="text-2xl font-bold">Конкуренты</h1>
<div class="flex gap-2">
<button onclick="openImportModal()" class="px-3 py-2 border rounded hover:bg-gray-50 text-sm">Импорт прайслиста</button>
<button onclick="openCompetitorModal()" class="px-3 py-2 bg-orange-600 text-white rounded hover:bg-orange-700 text-sm">Добавить конкурента</button>
</div>
</div>
<div class="bg-white rounded-lg shadow p-4">
<div id="comp-error" class="hidden mb-3 p-2 rounded bg-red-50 text-red-700 text-sm"></div>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200 text-sm">
<thead class="bg-gray-50">
<tr>
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Код</th>
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Название</th>
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Базис</th>
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Валюта</th>
<th class="px-4 py-2 text-right text-xs font-medium text-gray-500 uppercase">Скидка %</th>
<th class="px-4 py-2 text-right text-xs font-medium text-gray-500 uppercase" title="Уникальных партномеров в системе">Уник. p/n</th>
<th class="px-4 py-2 text-right text-xs font-medium text-gray-500 uppercase" title="Всего котировок исторически">Всего</th>
<th class="px-4 py-2 text-center text-xs font-medium text-gray-500 uppercase">Активен</th>
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Действия</th>
</tr>
</thead>
<tbody id="comp-body" class="bg-white divide-y divide-gray-200">
<tr><td colspan="9" class="px-4 py-3 text-sm text-gray-500">Загрузка...</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- Competitor Create/Edit Modal -->
<div id="comp-modal" class="fixed inset-0 bg-black bg-opacity-50 hidden items-center justify-center z-50">
<div class="bg-white rounded-lg p-6 w-full mx-4 max-h-[92vh] overflow-y-auto" style="max-width:min(95vw,1200px);">
<div class="flex justify-between items-center mb-4">
<h2 id="comp-modal-title" class="text-xl font-bold">Добавить конкурента</h2>
<button onclick="closeCompetitorModal()" class="text-gray-400 hover:text-gray-600 text-xl">&times;</button>
</div>
<div id="comp-modal-error" class="hidden mb-3 p-2 rounded bg-red-50 text-red-700 text-sm"></div>
<form id="comp-form" class="space-y-4" onsubmit="saveCompetitor(event)">
<input type="hidden" id="comp-id" value="">
<!-- Basic info -->
<div class="grid grid-cols-2 gap-3">
<div class="col-span-2">
<label class="block text-sm font-medium text-gray-700 mb-1">Название <span class="text-red-500">*</span></label>
<input id="comp-name" type="text" required class="w-full px-3 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-orange-500">
</div>
<div id="comp-code-row">
<label class="block text-sm font-medium text-gray-700 mb-1">Код <span class="text-red-500">*</span></label>
<input id="comp-code" type="text" required class="w-full px-3 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-orange-500" placeholder="например: acme">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Базис поставки</label>
<input id="comp-delivery" type="text" class="w-full px-3 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-orange-500" placeholder="DDP">
</div>
<div class="col-span-2 grid grid-cols-2 gap-3">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Ожидаемая скидка %</label>
<input id="comp-discount" type="number" step="0.01" min="0" max="100" class="w-full px-3 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-orange-500" value="0">
</div>
</div>
</div>
<!-- Column mapping -->
<div class="border rounded p-4 bg-gray-50 space-y-3">
<p class="text-sm font-medium text-gray-700">Маппинг колонок Excel</p>
<div class="flex items-end gap-3 flex-wrap">
<div>
<label class="block text-xs text-gray-500 mb-1">Лист (номер)</label>
<input id="comp-sheet" type="number" min="1" value="1" class="w-20 px-2 py-1.5 border rounded text-sm focus:outline-none focus:ring-2 focus:ring-orange-500">
</div>
<div class="flex items-center gap-2">
<input id="comp-sample-file" type="file" accept=".xlsx,.xls" class="hidden" onchange="onSampleFileUpload(this)">
<button type="button" onclick="document.getElementById('comp-sample-file').click()"
class="text-xs px-3 py-1.5 border rounded hover:bg-white text-gray-700 flex items-center gap-1.5 bg-white">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"/></svg>
Загрузить пример файла
</button>
<span id="comp-sample-filename" class="text-xs text-gray-400 italic"></span>
<span id="comp-sample-loading" class="hidden text-xs text-orange-600">Разбор...</span>
</div>
</div>
<!-- Current mapping summary (shown in edit mode before file upload) -->
<div id="comp-mapping-summary" class="hidden text-xs bg-white border rounded p-2 text-gray-600 leading-relaxed"></div>
<!-- Preview: slider + table (shown after file upload) -->
<div id="comp-preview" class="hidden">
<div class="flex gap-3">
<!-- Vertical header-row slider -->
<div class="flex flex-col items-center gap-1 select-none" style="min-width:52px;">
<span class="text-xs text-gray-500 text-center leading-tight">Строка<br>заголовка</span>
<span id="comp-hrow-val" class="text-sm font-bold text-orange-600">1</span>
<input type="range" id="comp-hrow-slider" min="1" max="30" value="1"
orient="vertical"
style="-webkit-appearance:slider-vertical;appearance:slider-vertical;writing-mode:vertical-lr;height:160px;width:20px;cursor:pointer;accent-color:#ea580c;"
oninput="onHeaderRowSlider(this.value)">
<span class="text-xs text-gray-400" id="comp-hrow-max">/ 30</span>
</div>
<!-- Scrollable table -->
<div class="flex-1 min-w-0">
<div class="overflow-x-auto border rounded bg-white">
<table id="comp-preview-table" class="text-xs border-collapse" style="min-width:100%;">
<thead id="comp-preview-thead"></thead>
<tbody id="comp-preview-body"></tbody>
</table>
</div>
<div class="flex items-center justify-between mt-1">
<span id="comp-preview-count" class="text-xs text-gray-400"></span>
<button type="button" id="comp-load-more-btn" onclick="loadMorePreviewRows()"
class="hidden text-xs text-blue-600 hover:underline">
Загрузить ещё 10 строк
</button>
</div>
</div>
</div>
</div>
</div>
<div class="flex justify-end gap-2 pt-2">
<button type="button" onclick="closeCompetitorModal()" class="px-4 py-2 border rounded hover:bg-gray-50">Отмена</button>
<button type="submit" id="comp-save-btn" class="px-4 py-2 bg-orange-600 text-white rounded hover:bg-orange-700">Сохранить</button>
</div>
</form>
</div>
</div>
<!-- Import Modal -->
<div id="import-modal" class="fixed inset-0 bg-black bg-opacity-50 hidden items-center justify-center z-50">
<div class="bg-white rounded-lg p-6 max-w-lg w-full mx-4 space-y-4">
<div class="flex justify-between items-center">
<h2 class="text-xl font-bold">Импорт прайслиста конкурента</h2>
<button onclick="closeImportModal()" class="text-gray-400 hover:text-gray-600 text-xl">&times;</button>
</div>
<div id="import-error" class="hidden p-2 rounded bg-red-50 text-red-700 text-sm"></div>
<div id="import-progress-container" class="hidden p-3 bg-orange-50 rounded border border-orange-200">
<div class="flex justify-between text-sm text-gray-700 mb-1">
<span id="import-progress-text" class="font-medium">Импорт...</span>
<span id="import-progress-percent" class="font-bold">0%</span>
</div>
<div class="w-full bg-gray-200 rounded-full h-3">
<div id="import-progress-bar" class="bg-orange-600 h-3 rounded-full transition-all duration-300" style="width:0%"></div>
</div>
</div>
<form id="import-form" class="space-y-3" onsubmit="submitImport(event)">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Конкурент <span class="text-red-500">*</span></label>
<select id="import-competitor" required class="w-full px-3 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-orange-500">
<option value="">Выберите конкурента...</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Дата котировки</label>
<input id="import-date" type="date" class="w-full px-3 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-orange-500">
</div>
<div id="import-rate-row" class="hidden">
<label class="block text-sm font-medium text-gray-700 mb-1">Курс к USD (для локальной валюты)</label>
<div class="flex items-center gap-2">
<input id="import-rate" type="number" step="0.0001" min="0.0001" placeholder="89.50" class="flex-1 px-3 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-orange-500">
<span class="text-sm text-gray-400 whitespace-nowrap">ед. = 1 USD</span>
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Файл Excel (.xlsx) <span class="text-red-500">*</span></label>
<input id="import-file" type="file" accept=".xlsx,.xls" required class="w-full px-3 py-2 border rounded">
</div>
<div class="flex justify-end gap-2 pt-2">
<button type="button" onclick="closeImportModal()" class="px-4 py-2 border rounded hover:bg-gray-50">Отмена</button>
<button type="submit" id="import-submit-btn" class="px-4 py-2 bg-orange-600 text-white rounded hover:bg-orange-700">Импортировать</button>
</div>
</form>
</div>
</div>
<script src="/static/js/competitors.js"></script>
{{end}}
{{template "base" .}}