package adminstore import "context" // DefaultKind is used for products whose category has no archive kind (or no // category at all). const DefaultKind = "generic" // FoodKind keeps the mature, dedicated food_detail path; every other kind is // driven generically by kind_field + product.attributes. const FoodKind = "food" // genericBaseFields are the core completeness fields for any non-food kind. // Food keeps its own richer CompletenessFields list. var genericBaseFields = []string{"name", "gtin", "brand", "category", "image"} // KindField describes one editable spec field for an archive kind. It drives // both the dynamic admin form and the kind-aware completeness computation. type KindField struct { Kind string `json:"kind"` FieldKey string `json:"field_key"` GroupLabel string `json:"group_label"` LabelZH string `json:"label_zh"` FieldType string `json:"field_type"` Unit *string `json:"unit"` Options []string `json:"options"` Placeholder *string `json:"placeholder"` SortOrder int `json:"sort_order"` Qualified bool `json:"qualified"` } // ListKindFields returns the ordered field template for one archive kind. func (s *Store) ListKindFields(ctx context.Context, kind string) ([]KindField, error) { rows, err := s.pool.Query(ctx, ` SELECT kind, field_key, group_label, label_zh, field_type, unit, options, placeholder, sort_order, qualified FROM kind_field WHERE kind = $1 ORDER BY sort_order, field_key`, kind) if err != nil { return nil, err } defer rows.Close() out := []KindField{} for rows.Next() { var f KindField if err := rows.Scan(&f.Kind, &f.FieldKey, &f.GroupLabel, &f.LabelZH, &f.FieldType, &f.Unit, &f.Options, &f.Placeholder, &f.SortOrder, &f.Qualified); err != nil { return nil, err } out = append(out, f) } return out, rows.Err() } // kindQualifiedKeys returns, per kind, the attribute keys that count toward the // completeness/qualified score. Loaded in one query so list views stay cheap. func (s *Store) kindQualifiedKeys(ctx context.Context, q queryer) (map[string][]string, error) { rows, err := s.pool.Query(ctx, "SELECT kind, field_key FROM kind_field WHERE qualified ORDER BY sort_order, field_key") if err != nil { return nil, err } defer rows.Close() m := map[string][]string{} for rows.Next() { var kind, key string if err := rows.Scan(&kind, &key); err != nil { return nil, err } m[kind] = append(m[kind], key) } return m, rows.Err() } // completenessKeys returns the ordered list of field keys that define a full // archive for the given kind. func completenessKeys(kind string, qualifiedAttrKeys []string) []string { if kind == FoodKind { return CompletenessFields } keys := make([]string, 0, len(genericBaseFields)+len(qualifiedAttrKeys)) keys = append(keys, genericBaseFields...) keys = append(keys, qualifiedAttrKeys...) return keys } // attrPresent reports whether an attribute value is meaningfully filled in. func attrPresent(attrs map[string]any, key string) bool { v, ok := attrs[key] if !ok || v == nil { return false } switch t := v.(type) { case string: return t != "" case []any: return len(t) > 0 default: return true } }