diff --git a/internal/handlers/pricelist.go b/internal/handlers/pricelist.go index d45f032..83c9fb4 100644 --- a/internal/handlers/pricelist.go +++ b/internal/handlers/pricelist.go @@ -116,6 +116,7 @@ func (h *PricelistHandler) Create(c *gin.Context) { changes = &pricelist.PriceChangeSummary{ Changed: []pricelist.PriceChangeItem{}, Missing: []pricelist.PriceChangeItem{}, + Added: []pricelist.PriceChangeItem{}, } } c.JSON(http.StatusCreated, gin.H{ @@ -182,6 +183,7 @@ func (h *PricelistHandler) CreateWithProgress(c *gin.Context) { changes = &pricelist.PriceChangeSummary{ Changed: []pricelist.PriceChangeItem{}, Missing: []pricelist.PriceChangeItem{}, + Added: []pricelist.PriceChangeItem{}, } } diff --git a/internal/services/pricelist/service.go b/internal/services/pricelist/service.go index fa6e93b..bc346fa 100644 --- a/internal/services/pricelist/service.go +++ b/internal/services/pricelist/service.go @@ -51,6 +51,7 @@ type PriceChangeSummary struct { PreviousPricelistVersion string `json:"previous_pricelist_version,omitempty"` Changed []PriceChangeItem `json:"changed"` Missing []PriceChangeItem `json:"missing"` + Added []PriceChangeItem `json:"added"` } func NewService(db *gorm.DB, repo *repository.PricelistRepository, componentRepo *repository.ComponentRepository, pricingSvc *pricing.Service) *Service { @@ -524,7 +525,7 @@ func (s *Service) StreamItemsForExport(pricelistID uint, batchSize int, callback // BuildPriceChangeSummary compares a newly created pricelist with the previous pricelist of the same source. func (s *Service) BuildPriceChangeSummary(pricelistID uint) (*PriceChangeSummary, error) { if s.db == nil { - return &PriceChangeSummary{Changed: []PriceChangeItem{}, Missing: []PriceChangeItem{}}, nil + return &PriceChangeSummary{Changed: []PriceChangeItem{}, Missing: []PriceChangeItem{}, Added: []PriceChangeItem{}}, nil } var current models.Pricelist @@ -539,7 +540,7 @@ func (s *Service) BuildPriceChangeSummary(pricelistID uint) (*PriceChangeSummary First(&previous).Error if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { - return &PriceChangeSummary{Changed: []PriceChangeItem{}, Missing: []PriceChangeItem{}}, nil + return &PriceChangeSummary{Changed: []PriceChangeItem{}, Missing: []PriceChangeItem{}, Added: []PriceChangeItem{}}, nil } return nil, fmt.Errorf("loading previous pricelist: %w", err) } @@ -568,8 +569,14 @@ func (s *Service) BuildPriceChangeSummary(pricelistID uint) (*PriceChangeSummary curMap[strings.TrimSpace(it.LotName)] = it.Price } + prevMap := make(map[string]float64, len(prevItems)) + for _, it := range prevItems { + prevMap[strings.TrimSpace(it.LotName)] = it.Price + } + changed := make([]PriceChangeItem, 0) missing := make([]PriceChangeItem, 0) + added := make([]PriceChangeItem, 0) const eps = 0.000001 for _, prev := range prevItems { lotName := strings.TrimSpace(prev.LotName) @@ -606,10 +613,28 @@ func (s *Service) BuildPriceChangeSummary(pricelistID uint) (*PriceChangeSummary }) } + for _, cur := range curItems { + lotName := strings.TrimSpace(cur.LotName) + if lotName == "" { + continue + } + if _, exists := prevMap[lotName]; !exists { + priceCopy := cur.Price + added = append(added, PriceChangeItem{ + LotName: lotName, + OldPrice: 0, + NewPrice: &priceCopy, + Diff: nil, + ChangeType: "added", + }) + } + } + return &PriceChangeSummary{ PreviousPricelistID: previous.ID, PreviousPricelistVersion: previous.Version, Changed: changed, Missing: missing, + Added: added, }, nil } diff --git a/web/templates/admin_pricing.html b/web/templates/admin_pricing.html index 72b43e2..2eed909 100644 --- a/web/templates/admin_pricing.html +++ b/web/templates/admin_pricing.html @@ -1982,18 +1982,18 @@ function formatPricelistChangePrice(value) { return Number(value).toLocaleString('ru-RU', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); } -function renderPricelistChangesTable(items, { showDiff = true } = {}) { +function renderPricelistChangesTable(items, { showOldPrice = true, showDiff = true } = {}) { if (!items || items.length === 0) return '
Нет данных
'; const rows = items.map(item => { const t = item.change_type || ''; - const rowClass = t === 'increased' ? 'bg-red-50' : (t === 'decreased' ? 'bg-green-50' : ''); + const rowClass = t === 'increased' ? 'bg-red-50' : (t === 'decreased' ? 'bg-green-50' : (t === 'added' ? 'bg-blue-50' : '')); const diffValue = Number(item.diff || 0); const diffClass = diffValue > 0 ? 'text-red-700' : (diffValue < 0 ? 'text-green-700' : 'text-gray-700'); const diffText = (showDiff && item.diff != null) ? `${diffValue > 0 ? '+' : ''}${formatPricelistChangePrice(diffValue)}` : '-'; return ` ${escapeHtml(item.lot_name || '')} - ${formatPricelistChangePrice(item.old_price)} + ${showOldPrice ? `${formatPricelistChangePrice(item.old_price)}` : ''} ${item.new_price == null ? '-' : formatPricelistChangePrice(item.new_price)} ${diffText} @@ -2005,7 +2005,7 @@ function renderPricelistChangesTable(items, { showDiff = true } = {}) { Позиция - Было + ${showOldPrice ? 'Было' : ''} Стало Изм. @@ -2020,9 +2020,10 @@ function openPricelistPriceChangesModal(pricelist) { const changes = pricelist?.price_changes || {}; const changed = Array.isArray(changes.changed) ? changes.changed : []; const missing = Array.isArray(changes.missing) ? changes.missing : []; + const added = Array.isArray(changes.added) ? changes.added : []; const prevVersion = changes.previous_pricelist_version || null; document.getElementById('pricelist-price-changes-summary').innerHTML = prevVersion - ? `Сравнение с прайслистом ${escapeHtml(prevVersion)}. Изменилось: ${changed.length}, пропало (нет котировок): ${missing.length}.` + ? `Сравнение с прайслистом ${escapeHtml(prevVersion)}. Изменилось: ${changed.length}, пропало: ${missing.length}, добавлено: ${added.length}.` : 'Предыдущий прайслист для сравнения не найден.'; document.getElementById('pricelist-price-changes-content').innerHTML = `
@@ -2033,6 +2034,10 @@ function openPricelistPriceChangesModal(pricelist) {

Пропали котировки (цена была раньше)

${renderPricelistChangesTable(missing, { showDiff: false })}
+
+

Новые позиции

+ ${renderPricelistChangesTable(added, { showOldPrice: false, showDiff: false })} +
`; document.getElementById('pricelist-price-changes-modal').classList.remove('hidden'); document.getElementById('pricelist-price-changes-modal').classList.add('flex'); diff --git a/web/templates/pricelists.html b/web/templates/pricelists.html index f9cc276..559c43f 100644 --- a/web/templates/pricelists.html +++ b/web/templates/pricelists.html @@ -212,13 +212,13 @@ return Number(value).toLocaleString('ru-RU', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); } - function renderPriceChangesTable(items, { showNewPrice = true, showDiff = true } = {}) { + function renderPriceChangesTable(items, { showOldPrice = true, showNewPrice = true, showDiff = true } = {}) { if (!items || items.length === 0) return '
Нет данных
'; const rows = items.map(item => { const changeType = item.change_type || ''; const rowClass = changeType === 'increased' ? 'bg-red-50' - : (changeType === 'decreased' ? 'bg-green-50' : ''); + : (changeType === 'decreased' ? 'bg-green-50' : (changeType === 'added' ? 'bg-blue-50' : '')); const diffValue = Number(item.diff || 0); const diffText = showDiff && item.diff !== null && item.diff !== undefined ? `${diffValue > 0 ? '+' : ''}${formatPrice(diffValue)}` @@ -227,7 +227,7 @@ return ` ${escapeHtml(item.lot_name || '')} - ${formatPrice(item.old_price)} + ${showOldPrice ? `${formatPrice(item.old_price)}` : ''} ${showNewPrice ? `${item.new_price == null ? '-' : formatPrice(item.new_price)}` : ''} ${showDiff ? `${diffText}` : ''} @@ -240,7 +240,7 @@ Позиция - Было + ${showOldPrice ? 'Было' : ''} ${showNewPrice ? 'Стало' : ''} ${showDiff ? 'Изм.' : ''} @@ -255,15 +255,17 @@ const changes = pl?.price_changes || {}; const changed = Array.isArray(changes.changed) ? changes.changed : []; const missing = Array.isArray(changes.missing) ? changes.missing : []; + const added = Array.isArray(changes.added) ? changes.added : []; const prevVersion = changes.previous_pricelist_version || null; document.getElementById('price-changes-summary').innerHTML = prevVersion - ? `Сравнение с прайслистом ${escapeHtml(prevVersion)}. Изменилось: ${changed.length}, пропало (нет котировок): ${missing.length}.` + ? `Сравнение с прайслистом ${escapeHtml(prevVersion)}. Изменилось: ${changed.length}, пропало: ${missing.length}, добавлено: ${added.length}.` : 'Предыдущий прайслист для сравнения не найден.'; let html = ''; html += `

Изменившиеся цены

${renderPriceChangesTable(changed)}
`; html += `

Пропали котировки (цена была раньше)

${renderPriceChangesTable(missing, { showNewPrice: true, showDiff: false })}
`; + html += `

Новые позиции

${renderPriceChangesTable(added, { showOldPrice: false, showNewPrice: true, showDiff: false })}
`; document.getElementById('price-changes-content').innerHTML = html; document.getElementById('price-changes-modal').classList.remove('hidden');