Files
PriceForge/web/templates/lot.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

305 lines
18 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"}}LOT - PriceForge{{end}}
{{define "content"}}
<div class="space-y-4">
<h1 class="text-2xl font-bold">LOT</h1>
<div class="bg-white rounded-lg shadow p-4">
<div class="flex justify-between items-center border-b pb-4 mb-4">
<div class="flex gap-4">
<button onclick="loadLotTab('components')" id="btn-lot-components" class="text-orange-600 font-medium">LOT</button>
<button onclick="loadLotTab('mappings')" id="btn-lot-mappings" class="text-gray-600">Сопоставления</button>
</div>
<button onclick="recalculateAll()" id="btn-recalc" class="px-3 py-1 bg-green-600 text-white text-sm rounded hover:bg-green-700">
Обновить цены
</button>
</div>
<!-- Search and sort (for LOT) -->
<div id="lot-search-bar" class="mb-4">
<div class="flex gap-4 items-center">
<input type="text" id="lot-search-input" placeholder="Поиск..."
class="flex-1 px-3 py-2 border rounded"
onkeyup="debounceSearch()">
<div class="flex items-center gap-2">
<span class="text-sm text-gray-500">Сортировка:</span>
<select id="lot-sort-field" class="px-2 py-1 border rounded text-sm" onchange="changeSort()">
<option value="lot_name">Артикул</option>
<option value="category">Категория</option>
<option value="popularity_score">Популярность</option>
<option value="estimate_count">Котировок</option>
<option value="stock_qty">На складе</option>
</select>
<button onclick="toggleSortDir()" id="lot-sort-dir-btn" class="px-2 py-1 border rounded text-sm"></button>
</div>
</div>
</div>
<!-- LOT Components Tab -->
<div id="lot-components-tab" class="">
<div id="lot-tab-content">
<div class="text-center py-8 text-gray-500">Загрузка...</div>
</div>
<!-- Create LOT Modal -->
<div id="create-lot-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-md w-full mx-4">
<h2 class="text-xl font-bold mb-4">Создать новый LOT</h2>
<form id="create-lot-form" class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">
LOT Name (артикул) <span class="text-red-500">*</span>
</label>
<input type="text" id="lot-name-input" name="lot_name" required
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-orange-500"
placeholder="Например: STM32F103C8T6">
<p class="text-xs text-gray-500 mt-1">Без пробелов, обычно это парт-номер компонента</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">
Описание
</label>
<input type="text" id="lot-description-input" name="lot_description"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-orange-500"
placeholder="Описание компонента">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">
Категория
</label>
<input type="text" id="lot-category-input" name="category"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-orange-500"
placeholder="Будет определена автоматически, если не указано">
<p class="text-xs text-gray-500 mt-1">Например: STM32, ATMEGA, резисторы и т.д.</p>
</div>
<div id="create-lot-error" class="hidden p-3 bg-red-50 border border-red-200 rounded text-sm text-red-600"></div>
<div class="flex justify-end space-x-3">
<button type="button" onclick="closeCreateLotModal()"
class="px-4 py-2 text-gray-700 border border-gray-300 rounded-md hover:bg-gray-50">
Отмена
</button>
<button type="submit" id="create-lot-submit-btn"
class="px-4 py-2 bg-orange-600 text-white rounded-md hover:bg-orange-700">
Создать
</button>
</div>
</form>
</div>
</div>
<!-- Pagination -->
<div id="lot-pagination" class="flex justify-between items-center mt-4 pt-4 border-t hidden">
<span id="lot-page-info" class="text-sm text-gray-600"></span>
<div class="flex gap-2">
<button onclick="prevPage()" id="lot-btn-prev" class="px-3 py-1 border rounded text-sm disabled:opacity-50">Назад</button>
<button onclick="nextPage()" id="lot-btn-next" class="px-3 py-1 border rounded text-sm disabled:opacity-50">Вперед</button>
</div>
</div>
</div>
<!-- Mappings Tab -->
<div id="lot-mappings-tab" class="hidden">
<div class="space-y-6">
<div class="border rounded-lg p-4">
<h3 class="text-lg font-semibold mb-3">Сопоставление partnumber → LOT</h3>
<div class="flex items-center gap-2 mb-3">
<input id="stock-mappings-search" type="text" placeholder="Поиск по partnumber / описанию / LOT" class="px-3 py-2 border rounded w-full">
<datalist id="stock-lot-options"></datalist>
<button onclick="openCreateMappingModal()" class="inline-flex items-center gap-1 px-3 py-2 bg-orange-600 text-white rounded hover:bg-orange-700">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path>
</svg>
Создать
</button>
<button onclick="loadStockMappings(1)" class="px-3 py-2 border rounded hover:bg-gray-50">Обновить</button>
</div>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-4 py-2 w-1/4 text-left text-xs font-medium text-gray-500 uppercase">LOT</th>
<th class="px-4 py-2 w-1/4 text-left text-xs font-medium text-gray-500 uppercase">Partnumber</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>
</tr>
</thead>
<tbody id="stock-mappings-body" class="bg-white divide-y divide-gray-200">
<tr><td colspan="4" class="px-4 py-3 text-sm text-gray-500">Загрузка...</td></tr>
</tbody>
</table>
</div>
<div id="stock-mappings-pagination" class="flex justify-center space-x-2 mt-3"></div>
</div>
</div>
</div>
</div>
</div>
<!-- LOT Mapping Modal -->
<div id="create-mapping-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-3xl w-full mx-4">
<div class="flex justify-between items-center mb-4">
<div>
<h2 id="mapping-modal-title" class="text-xl font-bold">LOT</h2>
<p id="mapping-modal-lot-name" class="text-sm text-gray-600 mt-1"></p>
</div>
<button onclick="closeCreateMappingModal()" class="text-gray-400 hover:text-gray-600">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Partnumber</label>
<input type="text" id="mapping-partnumber-input" autocomplete="off"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-orange-500 font-mono"
placeholder="Добавьте partnumber"
oninput="onMappingPartnumberChange()">
<div id="mapping-partnumber-suggestions" class="mt-1 border border-gray-200 rounded-md hidden bg-white"></div>
<div id="mapping-selected-partnumbers" class="mt-2 flex flex-wrap gap-2"></div>
<p id="mapping-partnumber-hint" class="text-xs text-gray-500 mt-1">Несопоставленных partnumbers</p>
<p id="mapping-partnumber-description" class="text-xs text-gray-600 mt-1"></p>
</div>
<p id="mapping-lot-description" class="text-xs text-gray-600"></p>
<div id="mapping-stats" class="hidden border rounded-lg p-4 bg-gray-50 space-y-3">
<h3 class="text-sm font-semibold text-gray-700">Статистика</h3>
<div class="grid grid-cols-2 gap-4">
<div>
<div class="text-xs font-medium text-gray-500 uppercase mb-2">Estimate</div>
<div class="space-y-1 text-sm">
<div class="flex justify-between">
<span class="text-gray-600">Цена:</span>
<span id="mapping-estimate-price" class="font-medium"></span>
</div>
<div class="flex justify-between">
<span class="text-gray-600">Котировок:</span>
<span id="mapping-estimate-count" class="font-medium"></span>
</div>
</div>
</div>
<div>
<div class="text-xs font-medium text-gray-500 uppercase mb-2">Склад</div>
<div class="space-y-1 text-sm">
<div class="flex justify-between">
<span class="text-gray-600">Цена:</span>
<span id="mapping-warehouse-price" class="font-medium"></span>
</div>
<div class="flex justify-between">
<span class="text-gray-600">На складе:</span>
<span id="mapping-warehouse-qty" class="font-medium"></span>
</div>
</div>
</div>
</div>
<div class="pt-2 border-t space-y-2">
<div class="text-xs font-medium text-gray-500 uppercase">График цены</div>
<div id="mapping-chart-container" class="rounded border bg-white p-2">
<svg id="mapping-price-chart" class="w-full h-48" viewBox="0 0 700 240" preserveAspectRatio="none"></svg>
<p id="mapping-chart-empty" class="hidden text-xs text-gray-500 px-2 py-3">Недостаточно данных для графика</p>
</div>
</div>
<div class="pt-2 border-t">
<div class="text-xs text-gray-500">
Несопоставленных partnumbers: <span id="mapping-unmapped-count" class="font-medium"></span>
</div>
</div>
</div>
<div id="create-mapping-error" class="hidden p-3 bg-red-50 border border-red-200 rounded text-sm text-red-600"></div>
<div class="flex justify-end space-x-3 pt-2">
<button type="button" onclick="closeCreateMappingModal()"
class="px-4 py-2 text-gray-700 border border-gray-300 rounded-md hover:bg-gray-50">
Отмена
</button>
<button type="button" onclick="submitCreateMapping()" id="create-mapping-submit-btn"
class="px-4 py-2 bg-orange-600 text-white rounded-md hover:bg-orange-700">
Сохранить сопоставления
</button>
</div>
</div>
</div>
</div>
<!-- Price Settings Modal (from admin_pricing) -->
<div id="price-modal" class="fixed inset-0 bg-black bg-opacity-50 hidden items-center justify-center z-50">
<div class="bg-white rounded-lg shadow-xl w-full max-w-md mx-4">
<div class="flex justify-between items-center p-4 border-b">
<h3 class="text-lg font-semibold">Настройка цены</h3>
<button onclick="closeModal()" class="text-gray-500 hover:text-gray-700">&times;</button>
</div>
<div class="p-4 space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Артикул</label>
<input type="text" id="modal-lot-name" readonly class="w-full px-3 py-2 border rounded bg-gray-100">
</div>
<div class="flex items-center mb-2">
<input type="checkbox" id="modal-meta-enabled" class="mr-2" onchange="toggleMetaPrice()">
<span class="text-sm font-medium text-gray-700">Мета артикул</span>
</div>
<div id="meta-price-fields" class="hidden mt-2">
<label class="block text-sm font-medium text-gray-700 mb-1">Источники цен</label>
<input type="text" id="modal-meta-prices" class="w-full px-3 py-2 border rounded" placeholder="Артикулы через запятую (например: CPU_AMD_9654, MB_INTEL_4.Sapphire_2S)">
<p class="text-xs text-gray-500 mt-1">Артикулы, чьи цены будут использоваться в расчете. Для автоматического подбора используйте * в конце (например: CPU_AMD_9654*)</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Метод расчёта</label>
<select id="modal-method" class="w-full px-3 py-2 border rounded" onchange="onMethodChange()">
<option value="median">Медиана</option>
<option value="average">Среднее</option>
<option value="manual">Установить цену вручную</option>
</select>
</div>
<div id="manual-price-field" class="hidden">
<label class="block text-sm font-medium text-gray-700 mb-1">Цена (USD)</label>
<input type="number" id="modal-manual-price" step="0.01" min="0" class="w-full px-3 py-2 border rounded">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Коэффициент</label>
<input type="number" id="modal-coefficient" step="0.01" value="1" min="0" class="w-full px-3 py-2 border rounded">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Период (дней)</label>
<select id="modal-period" class="w-full px-3 py-2 border rounded">
<option value="1">1</option>
<option value="7" selected>7</option>
<option value="14">14</option>
<option value="30">30</option>
<option value="90">90</option>
</select>
</div>
<div id="preview-section" class="hidden p-3 bg-blue-50 rounded border border-blue-200">
<p class="text-sm text-blue-800">Предпросмотр: <span id="preview-price" class="font-semibold">$0.00</span></p>
</div>
<div class="flex justify-end space-x-2 pt-4 border-t">
<button type="button" onclick="closeModal()" class="px-4 py-2 border border-gray-300 rounded text-sm hover:bg-gray-50">
Отмена
</button>
<button type="button" onclick="submitPriceChange()" class="px-4 py-2 bg-orange-600 text-white rounded text-sm hover:bg-orange-700">
Сохранить
</button>
</div>
</div>
</div>
</div>
<script src="/static/js/lot.js"></script>
{{end}}
{{template "base" .}}