Refine stock import UX with suggestions, ignore rules, and inline mapping controls
This commit is contained in:
@@ -997,6 +997,8 @@ func (h *PricingHandler) ImportStockLog(c *gin.Context) {
|
||||
"conflicts": result.Conflicts,
|
||||
"fallback_matches": result.FallbackMatches,
|
||||
"parse_errors": result.ParseErrors,
|
||||
"ignored": result.Ignored,
|
||||
"mapping_suggestions": result.MappingSuggestions,
|
||||
"import_date": result.ImportDate.Format("2006-01-02"),
|
||||
"warehouse_pricelist_id": result.WarehousePLID,
|
||||
"warehouse_pricelist_version": result.WarehousePLVer,
|
||||
@@ -1029,6 +1031,8 @@ func (h *PricingHandler) ImportStockLog(c *gin.Context) {
|
||||
"conflicts": p.Conflicts,
|
||||
"fallback_matches": p.FallbackMatches,
|
||||
"parse_errors": p.ParseErrors,
|
||||
"ignored": p.Ignored,
|
||||
"mapping_suggestions": p.MappingSuggestions,
|
||||
"import_date": p.ImportDate,
|
||||
"warehouse_pricelist_id": p.PricelistID,
|
||||
"warehouse_pricelist_version": p.PricelistVer,
|
||||
@@ -1078,7 +1082,7 @@ func (h *PricingHandler) UpsertStockMapping(c *gin.Context) {
|
||||
|
||||
var req struct {
|
||||
Partnumber string `json:"partnumber" binding:"required"`
|
||||
LotName string `json:"lot_name" binding:"required"`
|
||||
LotName string `json:"lot_name"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
@@ -1109,3 +1113,107 @@ func (h *PricingHandler) DeleteStockMapping(c *gin.Context) {
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"deleted": deleted})
|
||||
}
|
||||
|
||||
func (h *PricingHandler) ListStockIgnoreRules(c *gin.Context) {
|
||||
if h.stockImportService == nil {
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{
|
||||
"error": "Правила игнорирования доступны только в онлайн режиме",
|
||||
"offline": true,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
perPage, _ := strconv.Atoi(c.DefaultQuery("per_page", "50"))
|
||||
rows, total, err := h.stockImportService.ListIgnoreRules(page, perPage)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"items": rows,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"per_page": perPage,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *PricingHandler) UpsertStockIgnoreRule(c *gin.Context) {
|
||||
if h.stockImportService == nil {
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{
|
||||
"error": "Правила игнорирования доступны только в онлайн режиме",
|
||||
"offline": true,
|
||||
})
|
||||
return
|
||||
}
|
||||
var req struct {
|
||||
Target string `json:"target" binding:"required"`
|
||||
MatchType string `json:"match_type" binding:"required"`
|
||||
Pattern string `json:"pattern" binding:"required"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if err := h.stockImportService.UpsertIgnoreRule(req.Target, req.MatchType, req.Pattern); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": "ignore rule saved"})
|
||||
}
|
||||
|
||||
func (h *PricingHandler) DeleteStockIgnoreRule(c *gin.Context) {
|
||||
if h.stockImportService == nil {
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{
|
||||
"error": "Правила игнорирования доступны только в онлайн режиме",
|
||||
"offline": true,
|
||||
})
|
||||
return
|
||||
}
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
|
||||
return
|
||||
}
|
||||
deleted, err := h.stockImportService.DeleteIgnoreRule(uint(id))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"deleted": deleted})
|
||||
}
|
||||
|
||||
func (h *PricingHandler) ListLots(c *gin.Context) {
|
||||
if h.db == nil {
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{
|
||||
"error": "Список LOT доступен только в онлайн режиме",
|
||||
"offline": true,
|
||||
})
|
||||
return
|
||||
}
|
||||
perPage, _ := strconv.Atoi(c.DefaultQuery("per_page", "500"))
|
||||
if perPage < 1 {
|
||||
perPage = 500
|
||||
}
|
||||
if perPage > 5000 {
|
||||
perPage = 5000
|
||||
}
|
||||
search := strings.TrimSpace(c.Query("search"))
|
||||
query := h.db.Model(&models.Lot{}).Select("lot_name")
|
||||
if search != "" {
|
||||
query = query.Where("lot_name LIKE ?", "%"+search+"%")
|
||||
}
|
||||
var lots []models.Lot
|
||||
if err := query.Order("lot_name ASC").Limit(perPage).Find(&lots).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
items := make([]string, 0, len(lots))
|
||||
for _, lot := range lots {
|
||||
if strings.TrimSpace(lot.LotName) == "" {
|
||||
continue
|
||||
}
|
||||
items = append(items, lot.LotName)
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"items": items})
|
||||
}
|
||||
|
||||
@@ -65,3 +65,16 @@ type LotPartnumber struct {
|
||||
func (LotPartnumber) TableName() string {
|
||||
return "lot_partnumbers"
|
||||
}
|
||||
|
||||
// StockIgnoreRule contains import ignore pattern rules.
|
||||
type StockIgnoreRule struct {
|
||||
ID uint `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
||||
Target string `gorm:"column:target;size:20;not null" json:"target"` // partnumber|description
|
||||
MatchType string `gorm:"column:match_type;size:20;not null" json:"match_type"` // exact|prefix|suffix
|
||||
Pattern string `gorm:"column:pattern;size:500;not null" json:"pattern"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at"`
|
||||
}
|
||||
|
||||
func (StockIgnoreRule) TableName() string {
|
||||
return "stock_ignore_rules"
|
||||
}
|
||||
|
||||
@@ -21,40 +21,51 @@ import (
|
||||
)
|
||||
|
||||
type StockImportProgress struct {
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Current int `json:"current,omitempty"`
|
||||
Total int `json:"total,omitempty"`
|
||||
RowsTotal int `json:"rows_total,omitempty"`
|
||||
ValidRows int `json:"valid_rows,omitempty"`
|
||||
Inserted int `json:"inserted,omitempty"`
|
||||
Deleted int64 `json:"deleted,omitempty"`
|
||||
Unmapped int `json:"unmapped,omitempty"`
|
||||
Conflicts int `json:"conflicts,omitempty"`
|
||||
FallbackMatches int `json:"fallback_matches,omitempty"`
|
||||
ParseErrors int `json:"parse_errors,omitempty"`
|
||||
ImportDate string `json:"import_date,omitempty"`
|
||||
PricelistID uint `json:"warehouse_pricelist_id,omitempty"`
|
||||
PricelistVer string `json:"warehouse_pricelist_version,omitempty"`
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Current int `json:"current,omitempty"`
|
||||
Total int `json:"total,omitempty"`
|
||||
RowsTotal int `json:"rows_total,omitempty"`
|
||||
ValidRows int `json:"valid_rows,omitempty"`
|
||||
Inserted int `json:"inserted,omitempty"`
|
||||
Deleted int64 `json:"deleted,omitempty"`
|
||||
Unmapped int `json:"unmapped,omitempty"`
|
||||
Conflicts int `json:"conflicts,omitempty"`
|
||||
FallbackMatches int `json:"fallback_matches,omitempty"`
|
||||
ParseErrors int `json:"parse_errors,omitempty"`
|
||||
Ignored int `json:"ignored,omitempty"`
|
||||
MappingSuggestions []StockMappingSuggestion `json:"mapping_suggestions,omitempty"`
|
||||
ImportDate string `json:"import_date,omitempty"`
|
||||
PricelistID uint `json:"warehouse_pricelist_id,omitempty"`
|
||||
PricelistVer string `json:"warehouse_pricelist_version,omitempty"`
|
||||
}
|
||||
|
||||
type StockImportResult struct {
|
||||
RowsTotal int
|
||||
ValidRows int
|
||||
Inserted int
|
||||
Deleted int64
|
||||
Unmapped int
|
||||
Conflicts int
|
||||
FallbackMatches int
|
||||
ParseErrors int
|
||||
ImportDate time.Time
|
||||
WarehousePLID uint
|
||||
WarehousePLVer string
|
||||
RowsTotal int
|
||||
ValidRows int
|
||||
Inserted int
|
||||
Deleted int64
|
||||
Unmapped int
|
||||
Conflicts int
|
||||
FallbackMatches int
|
||||
ParseErrors int
|
||||
Ignored int
|
||||
MappingSuggestions []StockMappingSuggestion
|
||||
ImportDate time.Time
|
||||
WarehousePLID uint
|
||||
WarehousePLVer string
|
||||
}
|
||||
|
||||
type pendingMapping struct {
|
||||
Partnumber string
|
||||
Description string
|
||||
type StockMappingSuggestion struct {
|
||||
Partnumber string `json:"partnumber"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
}
|
||||
|
||||
type stockIgnoreRule struct {
|
||||
Target string
|
||||
MatchType string
|
||||
Pattern string
|
||||
}
|
||||
|
||||
type StockImportService struct {
|
||||
@@ -128,26 +139,42 @@ func (s *StockImportService) Import(
|
||||
conflicts int
|
||||
fallbackMatches int
|
||||
parseErrors int
|
||||
pendingByPN = make(map[string]pendingMapping)
|
||||
ignored int
|
||||
suggestionsByPN = make(map[string]StockMappingSuggestion)
|
||||
)
|
||||
ignoreRules, err := s.loadIgnoreRules()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, row := range rows {
|
||||
if strings.TrimSpace(row.Article) == "" {
|
||||
parseErrors++
|
||||
continue
|
||||
}
|
||||
if shouldIgnoreStockRow(row, ignoreRules) {
|
||||
ignored++
|
||||
continue
|
||||
}
|
||||
lot, matchType, resolveErr := resolver.resolve(row.Article)
|
||||
if resolveErr != nil {
|
||||
trimmedPN := strings.TrimSpace(row.Article)
|
||||
if trimmedPN != "" {
|
||||
key := normalizeKey(trimmedPN)
|
||||
if key != "" {
|
||||
candidate := pendingMapping{
|
||||
reason := "unmapped"
|
||||
if errors.Is(resolveErr, errResolveConflict) {
|
||||
reason = "conflict"
|
||||
}
|
||||
candidate := StockMappingSuggestion{
|
||||
Partnumber: trimmedPN,
|
||||
Description: strings.TrimSpace(row.Description),
|
||||
Reason: reason,
|
||||
}
|
||||
if prev, ok := pendingByPN[key]; !ok || (strings.TrimSpace(prev.Description) == "" && candidate.Description != "") {
|
||||
pendingByPN[key] = candidate
|
||||
if prev, ok := suggestionsByPN[key]; !ok ||
|
||||
(strings.TrimSpace(prev.Description) == "" && candidate.Description != "") ||
|
||||
(prev.Reason != "conflict" && candidate.Reason == "conflict") {
|
||||
suggestionsByPN[key] = candidate
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -181,15 +208,7 @@ func (s *StockImportService) Import(
|
||||
})
|
||||
}
|
||||
|
||||
if len(pendingByPN) > 0 {
|
||||
pending := make([]pendingMapping, 0, len(pendingByPN))
|
||||
for _, m := range pendingByPN {
|
||||
pending = append(pending, m)
|
||||
}
|
||||
if err := s.upsertPendingMappings(pending); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
suggestions := collectSortedSuggestions(suggestionsByPN, 200)
|
||||
|
||||
if len(records) == 0 {
|
||||
return nil, fmt.Errorf("no valid rows after mapping")
|
||||
@@ -256,35 +275,39 @@ func (s *StockImportService) Import(
|
||||
warehousePLVer = pl.Version
|
||||
|
||||
result := &StockImportResult{
|
||||
RowsTotal: len(rows),
|
||||
ValidRows: len(records),
|
||||
Inserted: inserted,
|
||||
Deleted: deleted,
|
||||
Unmapped: unmapped,
|
||||
Conflicts: conflicts,
|
||||
FallbackMatches: fallbackMatches,
|
||||
ParseErrors: parseErrors,
|
||||
ImportDate: importDate,
|
||||
WarehousePLID: warehousePLID,
|
||||
WarehousePLVer: warehousePLVer,
|
||||
RowsTotal: len(rows),
|
||||
ValidRows: len(records),
|
||||
Inserted: inserted,
|
||||
Deleted: deleted,
|
||||
Unmapped: unmapped,
|
||||
Conflicts: conflicts,
|
||||
FallbackMatches: fallbackMatches,
|
||||
ParseErrors: parseErrors,
|
||||
Ignored: ignored,
|
||||
MappingSuggestions: suggestions,
|
||||
ImportDate: importDate,
|
||||
WarehousePLID: warehousePLID,
|
||||
WarehousePLVer: warehousePLVer,
|
||||
}
|
||||
|
||||
report(StockImportProgress{
|
||||
Status: "completed",
|
||||
Message: "Импорт завершен",
|
||||
RowsTotal: result.RowsTotal,
|
||||
ValidRows: result.ValidRows,
|
||||
Inserted: result.Inserted,
|
||||
Deleted: result.Deleted,
|
||||
Unmapped: result.Unmapped,
|
||||
Conflicts: result.Conflicts,
|
||||
FallbackMatches: result.FallbackMatches,
|
||||
ParseErrors: result.ParseErrors,
|
||||
ImportDate: result.ImportDate.Format("2006-01-02"),
|
||||
PricelistID: result.WarehousePLID,
|
||||
PricelistVer: result.WarehousePLVer,
|
||||
Current: 100,
|
||||
Total: 100,
|
||||
Status: "completed",
|
||||
Message: "Импорт завершен",
|
||||
RowsTotal: result.RowsTotal,
|
||||
ValidRows: result.ValidRows,
|
||||
Inserted: result.Inserted,
|
||||
Deleted: result.Deleted,
|
||||
Unmapped: result.Unmapped,
|
||||
Conflicts: result.Conflicts,
|
||||
FallbackMatches: result.FallbackMatches,
|
||||
ParseErrors: result.ParseErrors,
|
||||
Ignored: result.Ignored,
|
||||
MappingSuggestions: result.MappingSuggestions,
|
||||
ImportDate: result.ImportDate.Format("2006-01-02"),
|
||||
PricelistID: result.WarehousePLID,
|
||||
PricelistVer: result.WarehousePLVer,
|
||||
Current: 100,
|
||||
Total: 100,
|
||||
})
|
||||
|
||||
return result, nil
|
||||
@@ -382,16 +405,18 @@ func (s *StockImportService) UpsertMapping(partnumber, lotName, description stri
|
||||
partnumber = strings.TrimSpace(partnumber)
|
||||
lotName = strings.TrimSpace(lotName)
|
||||
description = strings.TrimSpace(description)
|
||||
if partnumber == "" || lotName == "" {
|
||||
return fmt.Errorf("partnumber and lot_name are required")
|
||||
if partnumber == "" {
|
||||
return fmt.Errorf("partnumber is required")
|
||||
}
|
||||
|
||||
var lotCount int64
|
||||
if err := s.db.Model(&models.Lot{}).Where("lot_name = ?", lotName).Count(&lotCount).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if lotCount == 0 {
|
||||
return fmt.Errorf("lot not found: %s", lotName)
|
||||
if lotName != "" {
|
||||
var lotCount int64
|
||||
if err := s.db.Model(&models.Lot{}).Where("lot_name = ?", lotName).Count(&lotCount).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if lotCount == 0 {
|
||||
return fmt.Errorf("lot not found: %s", lotName)
|
||||
}
|
||||
}
|
||||
|
||||
return s.db.Transaction(func(tx *gorm.DB) error {
|
||||
@@ -434,55 +459,153 @@ func (s *StockImportService) DeleteMapping(partnumber string) (int64, error) {
|
||||
return res.RowsAffected, res.Error
|
||||
}
|
||||
|
||||
func (s *StockImportService) upsertPendingMappings(rows []pendingMapping) error {
|
||||
if s.db == nil || len(rows) == 0 {
|
||||
func (s *StockImportService) ListIgnoreRules(page, perPage int) ([]models.StockIgnoreRule, int64, error) {
|
||||
if s.db == nil {
|
||||
return nil, 0, fmt.Errorf("offline mode: ignore rules unavailable")
|
||||
}
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if perPage < 1 {
|
||||
perPage = 50
|
||||
}
|
||||
if perPage > 500 {
|
||||
perPage = 500
|
||||
}
|
||||
|
||||
offset := (page - 1) * perPage
|
||||
query := s.db.Model(&models.StockIgnoreRule{})
|
||||
var total int64
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
var rows []models.StockIgnoreRule
|
||||
if err := query.Order("id DESC").Offset(offset).Limit(perPage).Find(&rows).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
return rows, total, nil
|
||||
}
|
||||
|
||||
func (s *StockImportService) UpsertIgnoreRule(target, matchType, pattern string) error {
|
||||
if s.db == nil {
|
||||
return fmt.Errorf("offline mode: ignore rules unavailable")
|
||||
}
|
||||
target = normalizeIgnoreTarget(target)
|
||||
matchType = normalizeIgnoreMatchType(matchType)
|
||||
pattern = strings.TrimSpace(pattern)
|
||||
if target == "" || matchType == "" || pattern == "" {
|
||||
return fmt.Errorf("target, match_type and pattern are required")
|
||||
}
|
||||
return s.db.Clauses(clause.OnConflict{DoNothing: true}).Create(&models.StockIgnoreRule{
|
||||
Target: target,
|
||||
MatchType: matchType,
|
||||
Pattern: pattern,
|
||||
}).Error
|
||||
}
|
||||
|
||||
func (s *StockImportService) DeleteIgnoreRule(id uint) (int64, error) {
|
||||
if s.db == nil {
|
||||
return 0, fmt.Errorf("offline mode: ignore rules unavailable")
|
||||
}
|
||||
res := s.db.Delete(&models.StockIgnoreRule{}, id)
|
||||
return res.RowsAffected, res.Error
|
||||
}
|
||||
|
||||
func (s *StockImportService) loadIgnoreRules() ([]stockIgnoreRule, error) {
|
||||
var rows []models.StockIgnoreRule
|
||||
if err := s.db.Find(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rules := make([]stockIgnoreRule, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
target := normalizeIgnoreTarget(row.Target)
|
||||
matchType := normalizeIgnoreMatchType(row.MatchType)
|
||||
pattern := normalizeKey(row.Pattern)
|
||||
if target == "" || matchType == "" || pattern == "" {
|
||||
continue
|
||||
}
|
||||
rules = append(rules, stockIgnoreRule{
|
||||
Target: target,
|
||||
MatchType: matchType,
|
||||
Pattern: pattern,
|
||||
})
|
||||
}
|
||||
return rules, nil
|
||||
}
|
||||
|
||||
func collectSortedSuggestions(src map[string]StockMappingSuggestion, limit int) []StockMappingSuggestion {
|
||||
if len(src) == 0 {
|
||||
return nil
|
||||
}
|
||||
return s.db.Transaction(func(tx *gorm.DB) error {
|
||||
for _, row := range rows {
|
||||
pn := strings.TrimSpace(row.Partnumber)
|
||||
if pn == "" {
|
||||
continue
|
||||
items := make([]StockMappingSuggestion, 0, len(src))
|
||||
for _, item := range src {
|
||||
items = append(items, item)
|
||||
}
|
||||
sort.Slice(items, func(i, j int) bool {
|
||||
return strings.ToLower(items[i].Partnumber) < strings.ToLower(items[j].Partnumber)
|
||||
})
|
||||
if limit > 0 && len(items) > limit {
|
||||
return items[:limit]
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
func shouldIgnoreStockRow(row stockImportRow, rules []stockIgnoreRule) bool {
|
||||
if len(rules) == 0 {
|
||||
return false
|
||||
}
|
||||
partnumber := normalizeKey(row.Article)
|
||||
description := normalizeKey(row.Description)
|
||||
for _, rule := range rules {
|
||||
candidate := ""
|
||||
if rule.Target == "partnumber" {
|
||||
candidate = partnumber
|
||||
} else {
|
||||
candidate = description
|
||||
}
|
||||
if candidate == "" || rule.Pattern == "" {
|
||||
continue
|
||||
}
|
||||
switch rule.MatchType {
|
||||
case "exact":
|
||||
if candidate == rule.Pattern {
|
||||
return true
|
||||
}
|
||||
desc := strings.TrimSpace(row.Description)
|
||||
var existing []models.LotPartnumber
|
||||
if err := tx.Where("LOWER(TRIM(partnumber)) = LOWER(TRIM(?))", pn).Find(&existing).Error; err != nil {
|
||||
return err
|
||||
case "prefix":
|
||||
if strings.HasPrefix(candidate, rule.Pattern) {
|
||||
return true
|
||||
}
|
||||
if len(existing) == 0 {
|
||||
var descPtr *string
|
||||
if desc != "" {
|
||||
descPtr = &desc
|
||||
}
|
||||
if err := tx.Create(&models.LotPartnumber{
|
||||
Partnumber: pn,
|
||||
LotName: "",
|
||||
Description: descPtr,
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
if desc == "" {
|
||||
continue
|
||||
}
|
||||
needsDescription := true
|
||||
for _, item := range existing {
|
||||
if item.Description != nil && strings.TrimSpace(*item.Description) != "" {
|
||||
needsDescription = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if needsDescription {
|
||||
if err := tx.Model(&models.LotPartnumber{}).
|
||||
Where("LOWER(TRIM(partnumber)) = LOWER(TRIM(?))", pn).
|
||||
Update("description", desc).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
case "suffix":
|
||||
if strings.HasSuffix(candidate, rule.Pattern) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func normalizeIgnoreTarget(v string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(v)) {
|
||||
case "partnumber":
|
||||
return "partnumber"
|
||||
case "description":
|
||||
return "description"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeIgnoreMatchType(v string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(v)) {
|
||||
case "exact":
|
||||
return "exact"
|
||||
case "prefix":
|
||||
return "prefix"
|
||||
case "suffix":
|
||||
return "suffix"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
|
||||
Reference in New Issue
Block a user