feat: price_quality индикатор для прайслиста; убраны неиспользуемые price_method/period/coefficient/manual_price/meta_prices
Прайслисты: добавлено поле price_quality (0-9, выставляется внешней утилитой ценообразования на основе количества/свежести котировок за период — QF только отображает, не считает сам). В карточке прайслиста рядом с ценой показывается предупреждение (жёлтый/красный «!») при quality < 7. price_method/price_period_days/price_coefficient/manual_price/meta_prices убраны из контракта QF (models.PricelistItem, local_pricelist_items, API, колонка «Настройки» на странице прайслиста) — эти поля принадлежат внешнему движку ценообразования и нигде не использовались в клиенте. Сами колонки в qt_pricelist_items не трогаем — оставлены для обратной совместимости с внешней системой. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
39e6fd5642
commit
bea8d9453e
@@ -259,12 +259,9 @@ PK: lot_name
|
|||||||
| lot_category | varchar(50) | |
|
| lot_category | varchar(50) | |
|
||||||
| lot_description | varchar(10000) | added by migration 032; backfilled once from `lot.lot_description`. QF syncs this column directly — do not join `lot` per-row to get a description |
|
| lot_description | varchar(10000) | added by migration 032; backfilled once from `lot.lot_description`. QF syncs this column directly — do not join `lot` per-row to get a description |
|
||||||
| price | decimal(12,2) NOT NULL | |
|
| price | decimal(12,2) NOT NULL | |
|
||||||
| price_method | varchar(20) | synced to local and shown in the pricelist detail "Настройки" column |
|
| price_quality | tinyint unsigned, nullable | added by migration 033. Set by the external pricelist-building tool, 0-9, based on quote recency/count per its own per-lot pricing-period settings — QF only reads and displays it, never computes it. UI: 7-9 = no warning, 4-6 = amber "!", 0-3 = red "!" (`formatPriceQualityWarning` in `pricelist_detail.html`) |
|
||||||
| price_period_days | bigint DEFAULT 90 | synced to local, same as above |
|
|
||||||
| price_coefficient | decimal(5,2) DEFAULT 0 | synced to local, same as above |
|
The real table also has `price_method`, `price_period_days`, `price_coefficient`, `manual_price`, `meta_prices` and `lead_time_weeks` columns, owned and written by the external pricing engine that maintains `qt_pricelist_items` — **keep them in the table for backward compatibility with that system; QF must never drop, rename, or write to them.** QF itself does not model, sync, or display any of them (tried once, removed as unused/dead in the client). Do not re-add them to `models.PricelistItem`/`LocalPricelistItem` without a concrete UI need.
|
||||||
| manual_price | decimal(12,2) | synced to local, same as above |
|
|
||||||
| meta_prices | varchar(1000) | synced to local, same as above |
|
|
||||||
| lead_time_weeks | int | exists in the real table; not read by QF — no corresponding field in `models.PricelistItem` |
|
|
||||||
|
|
||||||
Fields QF never populates and does not model: `AvailableQty`/`Partnumbers` (removed from `models.PricelistItem` and `LocalPricelistItem` — no writer ever set them; always empty).
|
Fields QF never populates and does not model: `AvailableQty`/`Partnumbers` (removed from `models.PricelistItem` and `LocalPricelistItem` — no writer ever set them; always empty).
|
||||||
|
|
||||||
|
|||||||
@@ -185,11 +185,7 @@ func (h *PricelistHandler) GetItems(c *gin.Context) {
|
|||||||
"lot_description": item.LotDescription,
|
"lot_description": item.LotDescription,
|
||||||
"price": item.Price,
|
"price": item.Price,
|
||||||
"category": item.LotCategory,
|
"category": item.LotCategory,
|
||||||
"price_method": item.PriceMethod,
|
"price_quality": item.PriceQuality,
|
||||||
"price_period_days": item.PricePeriodDays,
|
|
||||||
"price_coefficient": item.PriceCoefficient,
|
|
||||||
"manual_price": item.ManualPrice,
|
|
||||||
"meta_prices": item.MetaPrices,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -337,11 +337,7 @@ func PricelistItemToLocal(item *models.PricelistItem, localPricelistID uint) *Lo
|
|||||||
LotCategory: item.LotCategory,
|
LotCategory: item.LotCategory,
|
||||||
LotDescription: item.LotDescription,
|
LotDescription: item.LotDescription,
|
||||||
Price: item.Price,
|
Price: item.Price,
|
||||||
PriceMethod: item.PriceMethod,
|
PriceQuality: item.PriceQuality,
|
||||||
PricePeriodDays: item.PricePeriodDays,
|
|
||||||
PriceCoefficient: item.PriceCoefficient,
|
|
||||||
ManualPrice: item.ManualPrice,
|
|
||||||
MetaPrices: item.MetaPrices,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -354,10 +350,6 @@ func LocalToPricelistItem(local *LocalPricelistItem, serverPricelistID uint) *mo
|
|||||||
LotCategory: local.LotCategory,
|
LotCategory: local.LotCategory,
|
||||||
LotDescription: local.LotDescription,
|
LotDescription: local.LotDescription,
|
||||||
Price: local.Price,
|
Price: local.Price,
|
||||||
PriceMethod: local.PriceMethod,
|
PriceQuality: local.PriceQuality,
|
||||||
PricePeriodDays: local.PricePeriodDays,
|
|
||||||
PriceCoefficient: local.PriceCoefficient,
|
|
||||||
ManualPrice: local.ManualPrice,
|
|
||||||
MetaPrices: local.MetaPrices,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -130,13 +130,18 @@ var localMigrations = []localMigration{
|
|||||||
run: addLocalProjectRentalEnabled,
|
run: addLocalProjectRentalEnabled,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "2026_07_22_pricelist_items_price_settings",
|
id: "2026_07_22_pricelist_items_add_description_drop_unused",
|
||||||
name: "Add lot_description and price settings columns to local_pricelist_items, drop unused available_qty/partnumbers",
|
name: "Add lot_description to local_pricelist_items, drop unused available_qty/partnumbers",
|
||||||
run: addPricelistItemDescriptionAndPriceSettings,
|
run: addPricelistItemDescriptionDropUnused,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "2026_07_22_pricelist_items_price_quality",
|
||||||
|
name: "Add price_quality to local_pricelist_items",
|
||||||
|
run: addLocalPricelistItemPriceQuality,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func addPricelistItemDescriptionAndPriceSettings(tx *gorm.DB) error {
|
func addLocalPricelistItemPriceQuality(tx *gorm.DB) error {
|
||||||
type columnInfo struct {
|
type columnInfo struct {
|
||||||
Name string `gorm:"column:name"`
|
Name string `gorm:"column:name"`
|
||||||
}
|
}
|
||||||
@@ -144,7 +149,33 @@ func addPricelistItemDescriptionAndPriceSettings(tx *gorm.DB) error {
|
|||||||
var columns []columnInfo
|
var columns []columnInfo
|
||||||
if err := tx.Raw(`
|
if err := tx.Raw(`
|
||||||
SELECT name FROM pragma_table_info('local_pricelist_items')
|
SELECT name FROM pragma_table_info('local_pricelist_items')
|
||||||
WHERE name IN ('lot_description', 'price_method', 'price_period_days', 'price_coefficient', 'manual_price', 'meta_prices', 'available_qty', 'partnumbers')
|
WHERE name = 'price_quality'
|
||||||
|
`).Scan(&columns).Error; err != nil {
|
||||||
|
return fmt.Errorf("check local_pricelist_items(price_quality) existence: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(columns) > 0 {
|
||||||
|
slog.Info("price_quality already present on local_pricelist_items")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Exec(`ALTER TABLE local_pricelist_items ADD COLUMN price_quality INTEGER`).Error; err != nil {
|
||||||
|
return fmt.Errorf("add local_pricelist_items.price_quality: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Info("added price_quality to local_pricelist_items")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func addPricelistItemDescriptionDropUnused(tx *gorm.DB) error {
|
||||||
|
type columnInfo struct {
|
||||||
|
Name string `gorm:"column:name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var columns []columnInfo
|
||||||
|
if err := tx.Raw(`
|
||||||
|
SELECT name FROM pragma_table_info('local_pricelist_items')
|
||||||
|
WHERE name IN ('lot_description', 'available_qty', 'partnumbers')
|
||||||
`).Scan(&columns).Error; err != nil {
|
`).Scan(&columns).Error; err != nil {
|
||||||
return fmt.Errorf("check local_pricelist_items columns: %w", err)
|
return fmt.Errorf("check local_pricelist_items columns: %w", err)
|
||||||
}
|
}
|
||||||
@@ -154,16 +185,13 @@ func addPricelistItemDescriptionAndPriceSettings(tx *gorm.DB) error {
|
|||||||
have[c.Name] = true
|
have[c.Name] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if have["lot_description"] && have["price_method"] && have["price_period_days"] &&
|
if have["lot_description"] && !have["available_qty"] && !have["partnumbers"] {
|
||||||
have["price_coefficient"] && have["manual_price"] && have["meta_prices"] &&
|
slog.Info("local_pricelist_items already migrated to description-only schema")
|
||||||
!have["available_qty"] && !have["partnumbers"] {
|
|
||||||
slog.Info("local_pricelist_items already migrated to description/price-settings schema")
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SQLite: recreate table without the never-populated available_qty/partnumbers
|
// SQLite: recreate table without the never-populated available_qty/partnumbers
|
||||||
// columns, adding lot_description and the price settings columns synced from
|
// columns, adding lot_description (synced from qt_pricelist_items.lot_description).
|
||||||
// qt_pricelist_items (needed to render the pricelist detail UI correctly).
|
|
||||||
if err := tx.Exec(`
|
if err := tx.Exec(`
|
||||||
CREATE TABLE local_pricelist_items_new (
|
CREATE TABLE local_pricelist_items_new (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
@@ -171,12 +199,7 @@ func addPricelistItemDescriptionAndPriceSettings(tx *gorm.DB) error {
|
|||||||
lot_name TEXT NOT NULL,
|
lot_name TEXT NOT NULL,
|
||||||
lot_category TEXT,
|
lot_category TEXT,
|
||||||
lot_description TEXT,
|
lot_description TEXT,
|
||||||
price REAL NOT NULL,
|
price REAL NOT NULL
|
||||||
price_method TEXT,
|
|
||||||
price_period_days INTEGER DEFAULT 90,
|
|
||||||
price_coefficient REAL DEFAULT 0,
|
|
||||||
manual_price REAL,
|
|
||||||
meta_prices TEXT
|
|
||||||
)
|
)
|
||||||
`).Error; err != nil {
|
`).Error; err != nil {
|
||||||
return fmt.Errorf("create new local_pricelist_items table: %w", err)
|
return fmt.Errorf("create new local_pricelist_items table: %w", err)
|
||||||
@@ -198,13 +221,6 @@ func addPricelistItemDescriptionAndPriceSettings(tx *gorm.DB) error {
|
|||||||
return fmt.Errorf("rename new local_pricelist_items table: %w", err)
|
return fmt.Errorf("rename new local_pricelist_items table: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := tx.Exec(`
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_local_pricelist_items_pricelist_lot
|
|
||||||
ON local_pricelist_items(pricelist_id, lot_name)
|
|
||||||
`).Error; err != nil {
|
|
||||||
return fmt.Errorf("recreate idx_local_pricelist_items_pricelist_lot: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := tx.Exec(`
|
if err := tx.Exec(`
|
||||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_local_pricelist_items_pricelist_lot_unique
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_local_pricelist_items_pricelist_lot_unique
|
||||||
ON local_pricelist_items(pricelist_id, lot_name)
|
ON local_pricelist_items(pricelist_id, lot_name)
|
||||||
@@ -219,7 +235,7 @@ func addPricelistItemDescriptionAndPriceSettings(tx *gorm.DB) error {
|
|||||||
return fmt.Errorf("recreate idx_local_pricelist_items_lot_category: %w", err)
|
return fmt.Errorf("recreate idx_local_pricelist_items_lot_category: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
slog.Info("added lot_description/price settings columns to local_pricelist_items and dropped available_qty/partnumbers")
|
slog.Info("added lot_description to local_pricelist_items and dropped available_qty/partnumbers")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1227,4 +1243,3 @@ func deduplicatePricelistItemsAndAddUniqueIndex(tx *gorm.DB) error {
|
|||||||
slog.Info("deduplicated local_pricelist_items and added unique index")
|
slog.Info("deduplicated local_pricelist_items and added unique index")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -197,13 +197,9 @@ type LocalPricelistItem struct {
|
|||||||
LotDescription string `gorm:"column:lot_description" json:"lot_description,omitempty"`
|
LotDescription string `gorm:"column:lot_description" json:"lot_description,omitempty"`
|
||||||
Price float64 `gorm:"not null" json:"price"`
|
Price float64 `gorm:"not null" json:"price"`
|
||||||
|
|
||||||
// Price calculation settings, mirrored from qt_pricelist_items for display
|
// PriceQuality mirrors qt_pricelist_items.price_quality: a 0-9 score set by
|
||||||
// in the pricelist detail UI (formatPriceSettings in pricelist_detail.html).
|
// the external pricing tool. 7-9 good, 4-6 warning, 0-3 poor.
|
||||||
PriceMethod string `gorm:"column:price_method" json:"price_method,omitempty"`
|
PriceQuality *int `gorm:"column:price_quality" json:"price_quality,omitempty"`
|
||||||
PricePeriodDays int `gorm:"column:price_period_days;default:90" json:"price_period_days"`
|
|
||||||
PriceCoefficient float64 `gorm:"column:price_coefficient;default:0" json:"price_coefficient"`
|
|
||||||
ManualPrice *float64 `gorm:"column:manual_price" json:"manual_price,omitempty"`
|
|
||||||
MetaPrices string `gorm:"column:meta_prices" json:"meta_prices,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (LocalPricelistItem) TableName() string {
|
func (LocalPricelistItem) TableName() string {
|
||||||
|
|||||||
@@ -61,14 +61,11 @@ type PricelistItem struct {
|
|||||||
LotCategory string `gorm:"column:lot_category;size:50" json:"lot_category,omitempty"`
|
LotCategory string `gorm:"column:lot_category;size:50" json:"lot_category,omitempty"`
|
||||||
LotDescription string `gorm:"column:lot_description;size:10000" json:"lot_description,omitempty"`
|
LotDescription string `gorm:"column:lot_description;size:10000" json:"lot_description,omitempty"`
|
||||||
Price float64 `gorm:"type:decimal(12,2);not null" json:"price"`
|
Price float64 `gorm:"type:decimal(12,2);not null" json:"price"`
|
||||||
PriceMethod string `gorm:"size:20" json:"price_method,omitempty"`
|
|
||||||
|
|
||||||
// Price calculation settings (snapshot from qt_lot_metadata), shown in the
|
// PriceQuality is a 0-9 score set by the external pricing tool that builds
|
||||||
// pricelist detail UI as a compact "settings" summary next to each item.
|
// the pricelist (based on quote recency/count per its own per-lot settings).
|
||||||
PricePeriodDays int `gorm:"default:90" json:"price_period_days"`
|
// 7-9 = good, 4-6 = warning, 0-3 = poor. QF only displays it, never computes it.
|
||||||
PriceCoefficient float64 `gorm:"type:decimal(5,2);default:0" json:"price_coefficient"`
|
PriceQuality *int `gorm:"column:price_quality" json:"price_quality,omitempty"`
|
||||||
ManualPrice *float64 `gorm:"type:decimal(12,2)" json:"manual_price,omitempty"`
|
|
||||||
MetaPrices string `gorm:"size:1000" json:"meta_prices,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (PricelistItem) TableName() string {
|
func (PricelistItem) TableName() string {
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
-- Tables affected: qt_pricelist_items
|
||||||
|
-- recovery.not-started: safe to re-run; ADD COLUMN IF NOT EXISTS
|
||||||
|
-- recovery.partial: ALTER TABLE qt_pricelist_items DROP COLUMN price_quality;
|
||||||
|
-- recovery.completed: no action needed
|
||||||
|
-- verify: price_quality column missing | SELECT 1 FROM information_schema.COLUMNS WHERE table_schema=DATABASE() AND table_name='qt_pricelist_items' AND column_name='price_quality' HAVING COUNT(*)=0
|
||||||
|
|
||||||
|
-- price_quality is a 0-9 score set by the external pricelist-building tool,
|
||||||
|
-- based on quote recency/count per its own per-lot pricing-period settings.
|
||||||
|
-- QuoteForge only reads and displays it (7-9 good, 4-6 warning, 0-3 poor);
|
||||||
|
-- it never computes or writes this value.
|
||||||
|
ALTER TABLE qt_pricelist_items
|
||||||
|
ADD COLUMN IF NOT EXISTS price_quality TINYINT UNSIGNED NULL;
|
||||||
@@ -59,7 +59,6 @@
|
|||||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Категория</th>
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Категория</th>
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Описание</th>
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Описание</th>
|
||||||
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase">Цена, $</th>
|
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase">Цена, $</th>
|
||||||
<th id="th-settings" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Настройки</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="items-body" class="bg-white divide-y divide-gray-200">
|
<tbody id="items-body" class="bg-white divide-y divide-gray-200">
|
||||||
@@ -90,7 +89,6 @@
|
|||||||
|
|
||||||
const pl = await resp.json();
|
const pl = await resp.json();
|
||||||
currentSource = pl.source || '';
|
currentSource = pl.source || '';
|
||||||
toggleWarehouseColumns();
|
|
||||||
|
|
||||||
document.getElementById('page-title').textContent = `Прайслист ${pl.version}`;
|
document.getElementById('page-title').textContent = `Прайслист ${pl.version}`;
|
||||||
document.getElementById('pl-version').textContent = pl.version;
|
document.getElementById('pl-version').textContent = pl.version;
|
||||||
@@ -133,7 +131,6 @@
|
|||||||
const resp = await fetch(url);
|
const resp = await fetch(url);
|
||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
currentSource = data.source || currentSource;
|
currentSource = data.source || currentSource;
|
||||||
toggleWarehouseColumns();
|
|
||||||
|
|
||||||
renderItems(data.items || []);
|
renderItems(data.items || []);
|
||||||
renderItemsPagination(data.total_count, data.page, data.per_page);
|
renderItemsPagination(data.total_count, data.page, data.per_page);
|
||||||
@@ -158,12 +155,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function itemsColspan() {
|
function itemsColspan() {
|
||||||
return isStockSource() ? 4 : 5;
|
return 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleWarehouseColumns() {
|
function formatPriceQualityWarning(quality) {
|
||||||
const stock = isStockSource();
|
if (typeof quality !== 'number') return '';
|
||||||
document.getElementById('th-settings').classList.toggle('hidden', stock);
|
if (quality >= 7) return '';
|
||||||
|
const color = quality >= 4 ? 'text-amber-500' : 'text-red-600';
|
||||||
|
return ` <span class="${color} font-bold" title="Качество цены: ${quality}/9">!</span>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function escapeHtml(text) {
|
function escapeHtml(text) {
|
||||||
@@ -176,48 +175,6 @@
|
|||||||
.replace(/'/g, ''');
|
.replace(/'/g, ''');
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatPriceSettings(item) {
|
|
||||||
// Format price settings to match admin pricing interface style
|
|
||||||
let settings = [];
|
|
||||||
const hasManualPrice = item.manual_price && item.manual_price > 0;
|
|
||||||
const hasMeta = item.meta_prices && item.meta_prices.trim() !== '';
|
|
||||||
const method = (item.price_method || '').toLowerCase();
|
|
||||||
|
|
||||||
// Method indicator
|
|
||||||
if (hasManualPrice) {
|
|
||||||
settings.push('<span class="text-orange-600 font-medium">РУЧН</span>');
|
|
||||||
} else if (method === 'average') {
|
|
||||||
settings.push('Сред');
|
|
||||||
} else if (method === 'weighted_median') {
|
|
||||||
settings.push('Взвеш. мед');
|
|
||||||
} else {
|
|
||||||
settings.push('Мед');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Period (only if not manual price)
|
|
||||||
if (!hasManualPrice) {
|
|
||||||
const period = item.price_period_days !== undefined && item.price_period_days !== null ? item.price_period_days : 90;
|
|
||||||
if (period === 7) settings.push('1н');
|
|
||||||
else if (period === 30) settings.push('1м');
|
|
||||||
else if (period === 90) settings.push('3м');
|
|
||||||
else if (period === 365) settings.push('1г');
|
|
||||||
else if (period === 0) settings.push('все');
|
|
||||||
else settings.push(period + 'д');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Coefficient
|
|
||||||
if (item.price_coefficient && item.price_coefficient !== 0) {
|
|
||||||
settings.push((item.price_coefficient > 0 ? '+' : '') + item.price_coefficient + '%');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Meta article indicator
|
|
||||||
if (hasMeta) {
|
|
||||||
settings.push('<span class="text-purple-600 font-medium">МЕТА</span>');
|
|
||||||
}
|
|
||||||
|
|
||||||
return settings.join(' | ') || '-';
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderItems(items) {
|
function renderItems(items) {
|
||||||
if (items.length === 0) {
|
if (items.length === 0) {
|
||||||
document.getElementById('items-body').innerHTML = `
|
document.getElementById('items-body').innerHTML = `
|
||||||
@@ -241,11 +198,12 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Price cell — add spread badge for competitor
|
// Price cell — add spread badge for competitor, plus a price-quality warning
|
||||||
let priceHtml = price;
|
let priceHtml = price;
|
||||||
if (!isWarehouseSource() && item.price_spread_pct > 0) {
|
if (!isWarehouseSource() && item.price_spread_pct > 0) {
|
||||||
priceHtml += ` <span class="text-xs text-amber-600 font-medium" title="Разброс цен конкурентов">±${item.price_spread_pct.toFixed(0)}%</span>`;
|
priceHtml += ` <span class="text-xs text-amber-600 font-medium" title="Разброс цен конкурентов">±${item.price_spread_pct.toFixed(0)}%</span>`;
|
||||||
}
|
}
|
||||||
|
priceHtml += formatPriceQualityWarning(item.price_quality);
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<tr class="hover:bg-gray-50">
|
<tr class="hover:bg-gray-50">
|
||||||
@@ -257,7 +215,6 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="${p} text-sm text-gray-500" title="${escapeHtml(description)}">${escapeHtml(truncatedDesc)}</td>
|
<td class="${p} text-sm text-gray-500" title="${escapeHtml(description)}">${escapeHtml(truncatedDesc)}</td>
|
||||||
<td class="${p} whitespace-nowrap text-right font-mono">${priceHtml}</td>
|
<td class="${p} whitespace-nowrap text-right font-mono">${priceHtml}</td>
|
||||||
${!stock ? `<td class="${p} whitespace-nowrap text-sm"><span class="text-xs bg-gray-100 px-2 py-1 rounded">${formatPriceSettings(item)}</span></td>` : ''}
|
|
||||||
</tr>
|
</tr>
|
||||||
`;
|
`;
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|||||||
Reference in New Issue
Block a user