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>
305 lines
18 KiB
HTML
305 lines
18 KiB
HTML
{{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">×</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" .}}
|