- Drop `expected_discount_pct`, add `price_uplift DECIMAL(8,4) DEFAULT 1.3` to `qt_competitors` (migration 040); formula: effective_price = price / uplift - Extend `LoadLotMetrics` to return per-PN qty map (`pnQtysByLot`) - Add virtual fields `CompetitorNames`, `PriceSpreadPct`, `PartnumberQtys` to `PricelistItem`; populate via `enrichWarehouseItems` / `enrichCompetitorItems` - Competitor quotes filtered to qty > 0 before lot resolution - New "stock layout" on pricelist detail page for warehouse/competitor: Partnumbers column (PN + qty, only qty>0), Поставщик column, no Настройки/Доступно - Spread badge ±N% shown next to price for competitor rows - Bible updated: pricelist.md, history.md Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
188 lines
13 KiB
HTML
188 lines
13 KiB
HTML
{{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" title="Делитель цены: цена_без_НДС = цена / аплифт">Аплифт</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">×</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" title="Делитель: цена_без_НДС = цена_конкурента / аплифт">Аплифт (делитель цены)</label>
|
||
<input id="comp-uplift" type="number" step="0.0001" min="0.0001" class="w-full px-3 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-orange-500" value="1.3" placeholder="1.3">
|
||
</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">×</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" .}}
|