fix: pricelist selection preserved when opening configurations
- Remove 'auto (latest active)' option from pricelist dropdowns; new configs pre-select the first active pricelist instead - Stop resetting stored pricelist_id to null when it is not in the active list (deactivated pricelists are shown as inactive options) - RefreshPricesNoAuth now accepts an optional pricelist_id; uses the UI-selected pricelist, then the config's stored pricelist, then latest as a last-resort fallback — no longer silently overwrites the stored pricelist on every price refresh - Same fix applied to RefreshPrices (with auth) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1126,7 +1126,12 @@ func setupRouter(cfg *config.Config, local *localdb.LocalDB, connMgr *db.Connect
|
|||||||
|
|
||||||
configs.POST("/:uuid/refresh-prices", func(c *gin.Context) {
|
configs.POST("/:uuid/refresh-prices", func(c *gin.Context) {
|
||||||
uuid := c.Param("uuid")
|
uuid := c.Param("uuid")
|
||||||
config, err := configService.RefreshPricesNoAuth(uuid)
|
var req struct {
|
||||||
|
PricelistID *uint `json:"pricelist_id"`
|
||||||
|
}
|
||||||
|
// Ignore bind error — pricelist_id is optional
|
||||||
|
_ = c.ShouldBindJSON(&req)
|
||||||
|
config, err := configService.RefreshPricesNoAuth(uuid, req.PricelistID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
respondError(c, http.StatusInternalServerError, "internal server error", err)
|
respondError(c, http.StatusInternalServerError, "internal server error", err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -399,17 +399,29 @@ func (s *LocalConfigurationService) RefreshPrices(uuid string, ownerUsername str
|
|||||||
return nil, ErrConfigForbidden
|
return nil, ErrConfigForbidden
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refresh local pricelists when online and use latest active/local pricelist for recalculation.
|
// Refresh local pricelists when online.
|
||||||
if s.isOnline() {
|
if s.isOnline() {
|
||||||
_ = s.syncService.SyncPricelistsIfNeeded()
|
_ = s.syncService.SyncPricelistsIfNeeded()
|
||||||
}
|
}
|
||||||
latestPricelist, latestErr := s.localDB.GetLatestLocalPricelist()
|
|
||||||
|
// Use the pricelist stored in the config; fall back to latest if unavailable.
|
||||||
|
var pricelist *localdb.LocalPricelist
|
||||||
|
if localCfg.PricelistID != nil && *localCfg.PricelistID > 0 {
|
||||||
|
if pl, err := s.localDB.GetLocalPricelistByServerID(*localCfg.PricelistID); err == nil {
|
||||||
|
pricelist = pl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if pricelist == nil {
|
||||||
|
if pl, err := s.localDB.GetLatestLocalPricelist(); err == nil {
|
||||||
|
pricelist = pl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Update prices for all items from pricelist
|
// Update prices for all items from pricelist
|
||||||
updatedItems := make(localdb.LocalConfigItems, len(localCfg.Items))
|
updatedItems := make(localdb.LocalConfigItems, len(localCfg.Items))
|
||||||
for i, item := range localCfg.Items {
|
for i, item := range localCfg.Items {
|
||||||
if latestErr == nil && latestPricelist != nil {
|
if pricelist != nil {
|
||||||
price, err := s.localDB.GetLocalPriceForLot(latestPricelist.ID, item.LotName)
|
price, err := s.localDB.GetLocalPriceForLot(pricelist.ID, item.LotName)
|
||||||
if err == nil && price > 0 {
|
if err == nil && price > 0 {
|
||||||
updatedItems[i] = localdb.LocalConfigItem{
|
updatedItems[i] = localdb.LocalConfigItem{
|
||||||
LotName: item.LotName,
|
LotName: item.LotName,
|
||||||
@@ -434,8 +446,8 @@ func (s *LocalConfigurationService) RefreshPrices(uuid string, ownerUsername str
|
|||||||
}
|
}
|
||||||
|
|
||||||
localCfg.TotalPrice = &total
|
localCfg.TotalPrice = &total
|
||||||
if latestErr == nil && latestPricelist != nil {
|
if pricelist != nil {
|
||||||
localCfg.PricelistID = &latestPricelist.ServerID
|
localCfg.PricelistID = &pricelist.ServerID
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set price update timestamp and mark for sync
|
// Set price update timestamp and mark for sync
|
||||||
@@ -762,8 +774,10 @@ func (s *LocalConfigurationService) ListTemplates(page, perPage int) ([]models.C
|
|||||||
return templates[start:end], total, nil
|
return templates[start:end], total, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// RefreshPricesNoAuth updates all component prices in the configuration without ownership check
|
// RefreshPricesNoAuth updates all component prices in the configuration without ownership check.
|
||||||
func (s *LocalConfigurationService) RefreshPricesNoAuth(uuid string) (*models.Configuration, error) {
|
// pricelistServerID optionally specifies which pricelist to use; if nil, the config's stored
|
||||||
|
// pricelist is used; if that is also absent, the latest local pricelist is used as a fallback.
|
||||||
|
func (s *LocalConfigurationService) RefreshPricesNoAuth(uuid string, pricelistServerID *uint) (*models.Configuration, error) {
|
||||||
// Get configuration from local SQLite
|
// Get configuration from local SQLite
|
||||||
localCfg, err := s.localDB.GetConfigurationByUUID(uuid)
|
localCfg, err := s.localDB.GetConfigurationByUUID(uuid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -773,13 +787,36 @@ func (s *LocalConfigurationService) RefreshPricesNoAuth(uuid string) (*models.Co
|
|||||||
if s.isOnline() {
|
if s.isOnline() {
|
||||||
_ = s.syncService.SyncPricelistsIfNeeded()
|
_ = s.syncService.SyncPricelistsIfNeeded()
|
||||||
}
|
}
|
||||||
latestPricelist, latestErr := s.localDB.GetLatestLocalPricelist()
|
|
||||||
|
// Resolve which pricelist to use:
|
||||||
|
// 1. Explicitly requested pricelist (from UI selection)
|
||||||
|
// 2. Pricelist stored in the configuration
|
||||||
|
// 3. Latest local pricelist as last-resort fallback
|
||||||
|
var targetServerID *uint
|
||||||
|
if pricelistServerID != nil && *pricelistServerID > 0 {
|
||||||
|
targetServerID = pricelistServerID
|
||||||
|
} else if localCfg.PricelistID != nil && *localCfg.PricelistID > 0 {
|
||||||
|
targetServerID = localCfg.PricelistID
|
||||||
|
}
|
||||||
|
|
||||||
|
var pricelist *localdb.LocalPricelist
|
||||||
|
if targetServerID != nil {
|
||||||
|
if pl, err := s.localDB.GetLocalPricelistByServerID(*targetServerID); err == nil {
|
||||||
|
pricelist = pl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if pricelist == nil {
|
||||||
|
// Fallback: use latest local pricelist
|
||||||
|
if pl, err := s.localDB.GetLatestLocalPricelist(); err == nil {
|
||||||
|
pricelist = pl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Update prices for all items from pricelist
|
// Update prices for all items from pricelist
|
||||||
updatedItems := make(localdb.LocalConfigItems, len(localCfg.Items))
|
updatedItems := make(localdb.LocalConfigItems, len(localCfg.Items))
|
||||||
for i, item := range localCfg.Items {
|
for i, item := range localCfg.Items {
|
||||||
if latestErr == nil && latestPricelist != nil {
|
if pricelist != nil {
|
||||||
price, err := s.localDB.GetLocalPriceForLot(latestPricelist.ID, item.LotName)
|
price, err := s.localDB.GetLocalPriceForLot(pricelist.ID, item.LotName)
|
||||||
if err == nil && price > 0 {
|
if err == nil && price > 0 {
|
||||||
updatedItems[i] = localdb.LocalConfigItem{
|
updatedItems[i] = localdb.LocalConfigItem{
|
||||||
LotName: item.LotName,
|
LotName: item.LotName,
|
||||||
@@ -804,8 +841,8 @@ func (s *LocalConfigurationService) RefreshPricesNoAuth(uuid string) (*models.Co
|
|||||||
}
|
}
|
||||||
|
|
||||||
localCfg.TotalPrice = &total
|
localCfg.TotalPrice = &total
|
||||||
if latestErr == nil && latestPricelist != nil {
|
if pricelist != nil {
|
||||||
localCfg.PricelistID = &latestPricelist.ServerID
|
localCfg.PricelistID = &pricelist.ServerID
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set price update timestamp and mark for sync
|
// Set price update timestamp and mark for sync
|
||||||
|
|||||||
@@ -925,14 +925,9 @@ async function loadActivePricelists(force = false) {
|
|||||||
const resp = await fetch(`/api/pricelists?active_only=true&source=${source}&per_page=200`);
|
const resp = await fetch(`/api/pricelists?active_only=true&source=${source}&per_page=200`);
|
||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
activePricelistsBySource[source] = data.pricelists || [];
|
activePricelistsBySource[source] = data.pricelists || [];
|
||||||
const existing = selectedPricelistIds[source];
|
// Do not reset the stored pricelist — it may be inactive but must be preserved
|
||||||
if (existing && activePricelistsBySource[source].some(pl => Number(pl.id) === Number(existing))) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
selectedPricelistIds[source] = null;
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
activePricelistsBySource[source] = [];
|
activePricelistsBySource[source] = [];
|
||||||
selectedPricelistIds[source] = null;
|
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
activePricelistsLoadedAt = Date.now();
|
activePricelistsLoadedAt = Date.now();
|
||||||
@@ -954,11 +949,25 @@ function renderPricelistSelectOptions(selectId, source) {
|
|||||||
select.value = '';
|
select.value = '';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
select.innerHTML = `<option value="">Авто (последний активный)</option>` + pricelists.map(pl => {
|
select.innerHTML = pricelists.map(pl => {
|
||||||
return `<option value="${pl.id}">${escapeHtml(pl.version)}</option>`;
|
return `<option value="${pl.id}">${escapeHtml(pl.version)}</option>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
const current = selectedPricelistIds[source];
|
const current = selectedPricelistIds[source];
|
||||||
select.value = current ? String(current) : '';
|
if (current) {
|
||||||
|
select.value = String(current);
|
||||||
|
// Stored pricelist may be inactive — add it as a virtual option if not found
|
||||||
|
if (!select.value) {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = String(current);
|
||||||
|
opt.textContent = `ID ${current} (неактивный)`;
|
||||||
|
select.prepend(opt);
|
||||||
|
select.value = String(current);
|
||||||
|
}
|
||||||
|
} else if (pricelists.length > 0) {
|
||||||
|
// New config: pre-select the first (latest) pricelist
|
||||||
|
selectedPricelistIds[source] = Number(pricelists[0].id);
|
||||||
|
select.value = String(pricelists[0].id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function syncPriceSettingsControls() {
|
function syncPriceSettingsControls() {
|
||||||
@@ -984,9 +993,9 @@ function getPricelistVersionById(source, id) {
|
|||||||
function renderPricelistSettingsSummary() {
|
function renderPricelistSettingsSummary() {
|
||||||
const summary = document.getElementById('pricelist-settings-summary');
|
const summary = document.getElementById('pricelist-settings-summary');
|
||||||
if (!summary) return;
|
if (!summary) return;
|
||||||
const estimate = selectedPricelistIds.estimate ? getPricelistVersionById('estimate', selectedPricelistIds.estimate) || `ID ${selectedPricelistIds.estimate}` : 'авто';
|
const estimate = selectedPricelistIds.estimate ? getPricelistVersionById('estimate', selectedPricelistIds.estimate) || `ID ${selectedPricelistIds.estimate}` : '—';
|
||||||
const warehouse = selectedPricelistIds.warehouse ? getPricelistVersionById('warehouse', selectedPricelistIds.warehouse) || `ID ${selectedPricelistIds.warehouse}` : 'авто';
|
const warehouse = selectedPricelistIds.warehouse ? getPricelistVersionById('warehouse', selectedPricelistIds.warehouse) || `ID ${selectedPricelistIds.warehouse}` : '—';
|
||||||
const competitor = selectedPricelistIds.competitor ? getPricelistVersionById('competitor', selectedPricelistIds.competitor) || `ID ${selectedPricelistIds.competitor}` : 'авто';
|
const competitor = selectedPricelistIds.competitor ? getPricelistVersionById('competitor', selectedPricelistIds.competitor) || `ID ${selectedPricelistIds.competitor}` : '—';
|
||||||
const refreshState = disablePriceRefresh ? ' | Обновление цен: выкл' : '';
|
const refreshState = disablePriceRefresh ? ' | Обновление цен: выкл' : '';
|
||||||
const stockFilterState = onlyInStock ? ' | Только наличие: вкл' : '';
|
const stockFilterState = onlyInStock ? ' | Только наличие: вкл' : '';
|
||||||
summary.textContent = `Estimate: ${estimate}, Склад: ${warehouse}, Конкуренты: ${competitor}${refreshState}${stockFilterState}`;
|
summary.textContent = `Estimate: ${estimate}, Склад: ${warehouse}, Конкуренты: ${competitor}${refreshState}${stockFilterState}`;
|
||||||
@@ -1062,16 +1071,16 @@ function applyPriceSettings() {
|
|||||||
const inStockVal = Boolean(document.getElementById('settings-only-in-stock')?.checked);
|
const inStockVal = Boolean(document.getElementById('settings-only-in-stock')?.checked);
|
||||||
|
|
||||||
const prevWarehouseID = currentWarehousePricelistID();
|
const prevWarehouseID = currentWarehousePricelistID();
|
||||||
selectedPricelistIds.estimate = Number.isFinite(estimateVal) && estimateVal > 0 ? estimateVal : null;
|
if (Number.isFinite(estimateVal) && estimateVal > 0) {
|
||||||
selectedPricelistIds.warehouse = Number.isFinite(warehouseVal) && warehouseVal > 0 ? warehouseVal : null;
|
selectedPricelistIds.estimate = estimateVal;
|
||||||
selectedPricelistIds.competitor = Number.isFinite(competitorVal) && competitorVal > 0 ? competitorVal : null;
|
|
||||||
if (selectedPricelistIds.estimate) {
|
|
||||||
resolvedAutoPricelistIds.estimate = null;
|
resolvedAutoPricelistIds.estimate = null;
|
||||||
}
|
}
|
||||||
if (selectedPricelistIds.warehouse) {
|
if (Number.isFinite(warehouseVal) && warehouseVal > 0) {
|
||||||
|
selectedPricelistIds.warehouse = warehouseVal;
|
||||||
resolvedAutoPricelistIds.warehouse = null;
|
resolvedAutoPricelistIds.warehouse = null;
|
||||||
}
|
}
|
||||||
if (selectedPricelistIds.competitor) {
|
if (Number.isFinite(competitorVal) && competitorVal > 0) {
|
||||||
|
selectedPricelistIds.competitor = competitorVal;
|
||||||
resolvedAutoPricelistIds.competitor = null;
|
resolvedAutoPricelistIds.competitor = null;
|
||||||
}
|
}
|
||||||
disablePriceRefresh = disableVal;
|
disablePriceRefresh = disableVal;
|
||||||
@@ -2508,11 +2517,14 @@ async function refreshPrices() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const refreshPayload = {};
|
||||||
|
if (selectedPricelistIds.estimate) {
|
||||||
|
refreshPayload.pricelist_id = selectedPricelistIds.estimate;
|
||||||
|
}
|
||||||
const resp = await fetch('/api/configs/' + configUUID + '/refresh-prices', {
|
const resp = await fetch('/api/configs/' + configUUID + '/refresh-prices', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: { 'Content-Type': 'application/json' },
|
||||||
'Content-Type': 'application/json'
|
body: JSON.stringify(refreshPayload)
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!resp.ok) {
|
if (!resp.ok) {
|
||||||
|
|||||||
Reference in New Issue
Block a user