@empty-complete-org/medusa-product-attributes 0.14.1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/.medusa/server/src/admin/index.js +441 -202
  2. package/.medusa/server/src/admin/index.mjs +442 -203
  3. package/.medusa/server/src/api/admin/{attribute-presets → attribute-templates}/[id]/apply/route.js +1 -1
  4. package/.medusa/server/src/api/admin/attribute-templates/[id]/apply/route.js.map +1 -0
  5. package/.medusa/server/src/api/admin/{attribute-presets → attribute-templates}/route.js +6 -6
  6. package/.medusa/server/src/api/admin/attribute-templates/route.js.map +1 -0
  7. package/.medusa/server/src/api/admin/category/[categoryId]/custom-attributes/route.js +1 -1
  8. package/.medusa/server/src/api/admin/category/[categoryId]/custom-attributes/route.js.map +1 -1
  9. package/.medusa/server/src/api/admin/global-attributes/route.d.ts +4 -0
  10. package/.medusa/server/src/api/admin/global-attributes/route.js +30 -0
  11. package/.medusa/server/src/api/admin/global-attributes/route.js.map +1 -0
  12. package/.medusa/server/src/api/admin/product/[productId]/attribute-schema/route.d.ts +2 -0
  13. package/.medusa/server/src/api/admin/product/[productId]/attribute-schema/route.js +34 -0
  14. package/.medusa/server/src/api/admin/product/[productId]/attribute-schema/route.js.map +1 -0
  15. package/.medusa/server/src/modules/product-attributes/index.d.ts +5 -5
  16. package/.medusa/server/src/modules/product-attributes/migrations/Migration20260407120000.d.ts +5 -0
  17. package/.medusa/server/src/modules/product-attributes/migrations/Migration20260407120000.js +58 -0
  18. package/.medusa/server/src/modules/product-attributes/migrations/Migration20260407120000.js.map +1 -0
  19. package/.medusa/server/src/modules/product-attributes/models/{attribute-preset.d.ts → attribute-template.d.ts} +3 -3
  20. package/.medusa/server/src/modules/product-attributes/models/{attribute-preset.js → attribute-template.js} +3 -3
  21. package/.medusa/server/src/modules/product-attributes/models/attribute-template.js.map +1 -0
  22. package/.medusa/server/src/modules/product-attributes/models/category-custom-attribute.d.ts +2 -1
  23. package/.medusa/server/src/modules/product-attributes/models/category-custom-attribute.js +2 -1
  24. package/.medusa/server/src/modules/product-attributes/models/category-custom-attribute.js.map +1 -1
  25. package/.medusa/server/src/modules/product-attributes/models/product-custom-attribute.d.ts +2 -1
  26. package/.medusa/server/src/modules/product-attributes/service.d.ts +56 -17
  27. package/.medusa/server/src/modules/product-attributes/service.js +44 -18
  28. package/.medusa/server/src/modules/product-attributes/service.js.map +1 -1
  29. package/README.md +96 -312
  30. package/README.ru.md +85 -0
  31. package/package.json +1 -1
  32. package/.medusa/server/src/api/admin/attribute-presets/[id]/apply/route.js.map +0 -1
  33. package/.medusa/server/src/api/admin/attribute-presets/route.js.map +0 -1
  34. package/.medusa/server/src/modules/product-attributes/models/attribute-preset.js.map +0 -1
  35. /package/.medusa/server/src/api/admin/{attribute-presets → attribute-templates}/[id]/apply/route.d.ts +0 -0
  36. /package/.medusa/server/src/api/admin/{attribute-presets → attribute-templates}/route.d.ts +0 -0
@@ -32,35 +32,201 @@ const sdk = new Medusa__default.default({
32
32
  type: "session"
33
33
  }
34
34
  });
35
- const emptyForm$1 = () => ({ label: "", type: "text", unit: "" });
35
+ const attributes$1 = "Атрибуты";
36
+ const characteristics$1 = "Характеристики";
37
+ const inherited$1 = "Унаследованные";
38
+ const own$1 = "Свои";
39
+ const global$1 = "глобальный";
40
+ const fromParent$1 = "из родителя";
41
+ const add$1 = "Добавить";
42
+ const fromTemplate$1 = "Из шаблона";
43
+ const chooseTemplate$1 = "Выберите шаблон";
44
+ const close$1 = "Закрыть";
45
+ const loading$1 = "Загрузка…";
46
+ const loadFailed$1 = "Не удалось загрузить.";
47
+ const noAttributes$1 = "Нет атрибутов. Добавьте первый.";
48
+ const noTemplates$1 = "Шаблонов нет. Создайте в Settings → Attribute Templates.";
49
+ const noGlobals$1 = "Глобальных атрибутов нет.";
50
+ const cancel$1 = "Отмена";
51
+ const confirmDelete$1 = "Удалить «{name}»?";
52
+ const create$1 = "Создать";
53
+ const save$1 = "Сохранить";
54
+ const saved$1 = "Сохранено ✓";
55
+ const name$1 = "Название";
56
+ const description$1 = "Описание (необязательно)";
57
+ const unit$1 = "ед.";
58
+ const value$1 = "Значение";
59
+ const yes$1 = "Да";
60
+ const no$1 = "Нет";
61
+ const upload$1 = "Загрузить";
62
+ const uploadError$1 = "Ошибка загрузки файла";
63
+ const fileUrl$1 = "URL файла";
64
+ const templates$1 = "Шаблоны атрибутов";
65
+ const templatesDesc$1 = "Заготовки атрибутов, которые можно применить к любой категории.";
66
+ const globals$1 = "Глобальные атрибуты";
67
+ const globalsDesc$1 = "Применяются ко всем товарам автоматически. Значения у продуктов необязательны.";
68
+ const ru = {
69
+ attributes: attributes$1,
70
+ characteristics: characteristics$1,
71
+ inherited: inherited$1,
72
+ own: own$1,
73
+ global: global$1,
74
+ fromParent: fromParent$1,
75
+ add: add$1,
76
+ fromTemplate: fromTemplate$1,
77
+ chooseTemplate: chooseTemplate$1,
78
+ close: close$1,
79
+ loading: loading$1,
80
+ loadFailed: loadFailed$1,
81
+ noAttributes: noAttributes$1,
82
+ noTemplates: noTemplates$1,
83
+ noGlobals: noGlobals$1,
84
+ "delete": "Удалить",
85
+ cancel: cancel$1,
86
+ confirmDelete: confirmDelete$1,
87
+ create: create$1,
88
+ save: save$1,
89
+ saved: saved$1,
90
+ name: name$1,
91
+ description: description$1,
92
+ unit: unit$1,
93
+ value: value$1,
94
+ "type.text": "Текст",
95
+ "type.number": "Число",
96
+ "type.file": "Файл",
97
+ "type.boolean": "Да/Нет",
98
+ yes: yes$1,
99
+ no: no$1,
100
+ upload: upload$1,
101
+ uploadError: uploadError$1,
102
+ fileUrl: fileUrl$1,
103
+ templates: templates$1,
104
+ templatesDesc: templatesDesc$1,
105
+ globals: globals$1,
106
+ globalsDesc: globalsDesc$1
107
+ };
108
+ const attributes = "Attributes";
109
+ const characteristics = "Characteristics";
110
+ const inherited = "Inherited";
111
+ const own = "Own";
112
+ const global = "global";
113
+ const fromParent = "from parent";
114
+ const add = "Add";
115
+ const fromTemplate = "From template";
116
+ const chooseTemplate = "Choose a template";
117
+ const close = "Close";
118
+ const loading = "Loading…";
119
+ const loadFailed = "Failed to load.";
120
+ const noAttributes = "No attributes. Add your first.";
121
+ const noTemplates = "No templates. Create in Settings → Attribute Templates.";
122
+ const noGlobals = "No global attributes.";
123
+ const cancel = "Cancel";
124
+ const confirmDelete = "Delete «{name}»?";
125
+ const create = "Create";
126
+ const save = "Save";
127
+ const saved = "Saved ✓";
128
+ const name = "Name";
129
+ const description = "Description (optional)";
130
+ const unit = "unit";
131
+ const value = "Value";
132
+ const yes = "Yes";
133
+ const no = "No";
134
+ const upload = "Upload";
135
+ const uploadError = "Upload failed";
136
+ const fileUrl = "File URL";
137
+ const templates = "Attribute Templates";
138
+ const templatesDesc = "Reusable attribute blueprints. Apply to any category in one click.";
139
+ const globals = "Global Attributes";
140
+ const globalsDesc = "Applied to every product automatically. Product values are optional.";
141
+ const en = {
142
+ attributes,
143
+ characteristics,
144
+ inherited,
145
+ own,
146
+ global,
147
+ fromParent,
148
+ add,
149
+ fromTemplate,
150
+ chooseTemplate,
151
+ close,
152
+ loading,
153
+ loadFailed,
154
+ noAttributes,
155
+ noTemplates,
156
+ noGlobals,
157
+ "delete": "Delete",
158
+ cancel,
159
+ confirmDelete,
160
+ create,
161
+ save,
162
+ saved,
163
+ name,
164
+ description,
165
+ unit,
166
+ value,
167
+ "type.text": "Text",
168
+ "type.number": "Number",
169
+ "type.file": "File",
170
+ "type.boolean": "Yes/No",
171
+ yes,
172
+ no,
173
+ upload,
174
+ uploadError,
175
+ fileUrl,
176
+ templates,
177
+ templatesDesc,
178
+ globals,
179
+ globalsDesc
180
+ };
181
+ const locales = { ru, en };
182
+ function detectLang() {
183
+ var _a;
184
+ if (typeof window === "undefined") return "en";
185
+ const stored = window.localStorage.getItem("i18nextLng");
186
+ if (stored) {
187
+ const short = stored.slice(0, 2).toLowerCase();
188
+ if (locales[short]) return short;
189
+ }
190
+ const nav = (((_a = window.navigator) == null ? void 0 : _a.language) || "en").slice(0, 2).toLowerCase();
191
+ return locales[nav] ? nav : "en";
192
+ }
193
+ function useT() {
194
+ const dict = React.useMemo(() => {
195
+ const lang = detectLang();
196
+ return locales[lang] ?? locales.en;
197
+ }, []);
198
+ return (key, fallback) => dict[key] ?? fallback ?? key;
199
+ }
200
+ const emptyForm$2 = () => ({ label: "", type: "text", unit: "" });
36
201
  const CategoryAttributeTemplatesWidget = ({
37
202
  data
38
203
  }) => {
39
204
  var _a, _b;
40
205
  const categoryId = data.id;
206
+ const t = useT();
41
207
  const qc = reactQuery.useQueryClient();
42
208
  const queryKey = ["category-custom-attributes", categoryId];
43
209
  const [showAddForm, setShowAddForm] = React.useState(false);
44
- const [showPresetList, setShowPresetList] = React.useState(false);
45
- const [addForm, setAddForm] = React.useState(emptyForm$1());
210
+ const [showTemplateList, setShowTemplateList] = React.useState(false);
211
+ const [addForm, setAddForm] = React.useState(emptyForm$2());
46
212
  const [confirmDeleteId, setConfirmDeleteId] = React.useState(null);
47
213
  const [mutationError, setMutationError] = React.useState(null);
48
- const presetsQuery = reactQuery.useQuery({
49
- queryKey: ["attribute-presets"],
50
- queryFn: () => sdk.client.fetch(`/admin/attribute-presets`),
51
- enabled: showPresetList
214
+ const templatesQuery = reactQuery.useQuery({
215
+ queryKey: ["attribute-templates"],
216
+ queryFn: () => sdk.client.fetch(`/admin/attribute-templates`),
217
+ enabled: showTemplateList
52
218
  });
53
- const applyPresetMutation = reactQuery.useMutation({
54
- mutationFn: (presetId) => sdk.client.fetch(`/admin/attribute-presets/${presetId}/apply`, {
219
+ const applyTemplateMutation = reactQuery.useMutation({
220
+ mutationFn: (templateId) => sdk.client.fetch(`/admin/attribute-templates/${templateId}/apply`, {
55
221
  method: "POST",
56
222
  body: { category_id: categoryId }
57
223
  }),
58
224
  onSuccess: () => {
59
225
  qc.invalidateQueries({ queryKey });
60
- setShowPresetList(false);
226
+ setShowTemplateList(false);
61
227
  },
62
228
  onError: (err) => {
63
- setMutationError((err == null ? void 0 : err.message) || "Ошибка при применении пресета");
229
+ setMutationError((err == null ? void 0 : err.message) || "Ошибка при применении шаблона");
64
230
  }
65
231
  });
66
232
  const {
@@ -71,9 +237,9 @@ const CategoryAttributeTemplatesWidget = ({
71
237
  queryKey,
72
238
  queryFn: () => sdk.client.fetch(`/admin/category/${categoryId}/custom-attributes`)
73
239
  });
74
- const attributes = (result == null ? void 0 : result.category_custom_attributes) ?? [];
75
- const ownAttributes = attributes.filter((a) => !a.inherited);
76
- const inheritedAttributes = attributes.filter((a) => a.inherited);
240
+ const attributes2 = (result == null ? void 0 : result.category_custom_attributes) ?? [];
241
+ const ownAttributes = attributes2.filter((a) => !a.inherited);
242
+ const inheritedAttributes = attributes2.filter((a) => a.inherited);
77
243
  const createMutation = reactQuery.useMutation({
78
244
  mutationFn: (body) => sdk.client.fetch(`/admin/category/${categoryId}/custom-attributes`, {
79
245
  method: "POST",
@@ -82,7 +248,7 @@ const CategoryAttributeTemplatesWidget = ({
82
248
  onSuccess: () => {
83
249
  qc.invalidateQueries({ queryKey });
84
250
  setShowAddForm(false);
85
- setAddForm(emptyForm$1());
251
+ setAddForm(emptyForm$2());
86
252
  setMutationError(null);
87
253
  },
88
254
  onError: (err) => {
@@ -107,51 +273,54 @@ const CategoryAttributeTemplatesWidget = ({
107
273
  unit: addForm.type === "number" && addForm.unit.trim() ? addForm.unit.trim() : null
108
274
  });
109
275
  };
110
- const typeLabel2 = (t) => t === "text" ? "Текст" : t === "number" ? "Число" : t === "file" ? "Файл" : t === "boolean" ? "Да/Нет" : t;
276
+ const typeLabel2 = (v) => t(`type.${v}`, v);
111
277
  return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", children: [
112
278
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between px-6 py-4", children: [
113
- /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: "Атрибуты" }),
114
- !showAddForm && !showPresetList && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
279
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: t("attributes") }),
280
+ !showAddForm && !showTemplateList && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
115
281
  /* @__PURE__ */ jsxRuntime.jsx(
116
282
  ui.Button,
117
283
  {
118
284
  variant: "secondary",
119
285
  size: "small",
120
- onClick: () => setShowPresetList(true),
121
- children: "Из пресета"
286
+ onClick: () => setShowTemplateList(true),
287
+ children: t("fromTemplate")
122
288
  }
123
289
  ),
124
- /* @__PURE__ */ jsxRuntime.jsx(
290
+ /* @__PURE__ */ jsxRuntime.jsxs(
125
291
  ui.Button,
126
292
  {
127
293
  variant: "secondary",
128
294
  size: "small",
129
295
  onClick: () => setShowAddForm(true),
130
- children: "+ Добавить"
296
+ children: [
297
+ "+ ",
298
+ t("add")
299
+ ]
131
300
  }
132
301
  )
133
302
  ] })
134
303
  ] }),
135
- showPresetList && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-3", children: [
304
+ showTemplateList && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-3", children: [
136
305
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mb-2 flex items-center justify-between", children: [
137
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", weight: "plus", children: "Выберите пресет" }),
306
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", weight: "plus", children: "Выберите шаблон" }),
138
307
  /* @__PURE__ */ jsxRuntime.jsx(
139
308
  ui.Button,
140
309
  {
141
310
  size: "small",
142
311
  variant: "secondary",
143
- onClick: () => setShowPresetList(false),
312
+ onClick: () => setShowTemplateList(false),
144
313
  children: "Закрыть"
145
314
  }
146
315
  )
147
316
  ] }),
148
- presetsQuery.isLoading && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-muted text-sm", children: "Загрузка…" }),
149
- ((_a = presetsQuery.data) == null ? void 0 : _a.attribute_presets.length) === 0 && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-muted text-sm", children: "Пресетов нет. Создайте в настройках Product Attributes." }),
150
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-col gap-1", children: (_b = presetsQuery.data) == null ? void 0 : _b.attribute_presets.map((p) => /* @__PURE__ */ jsxRuntime.jsxs(
317
+ templatesQuery.isLoading && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-muted text-sm", children: t("loading") }),
318
+ ((_a = templatesQuery.data) == null ? void 0 : _a.attribute_templates.length) === 0 && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-muted text-sm", children: "Пресетов нет. Создайте в настройках Product Attributes." }),
319
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-col gap-1", children: (_b = templatesQuery.data) == null ? void 0 : _b.attribute_templates.map((p) => /* @__PURE__ */ jsxRuntime.jsxs(
151
320
  "button",
152
321
  {
153
- onClick: () => applyPresetMutation.mutate(p.id),
154
- disabled: applyPresetMutation.isPending,
322
+ onClick: () => applyTemplateMutation.mutate(p.id),
323
+ disabled: applyTemplateMutation.isPending,
155
324
  className: "flex items-center justify-between rounded border border-ui-border-base px-3 py-2 text-left text-sm hover:bg-ui-bg-subtle disabled:opacity-50",
156
325
  children: [
157
326
  /* @__PURE__ */ jsxRuntime.jsx("span", { children: p.label }),
@@ -164,7 +333,7 @@ const CategoryAttributeTemplatesWidget = ({
164
333
  p.id
165
334
  )) })
166
335
  ] }),
167
- isLoading && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-muted text-sm", children: "Загрузка…" }) }),
336
+ isLoading && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-muted text-sm", children: t("loading") }) }),
168
337
  isError && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-error text-sm", children: "Не удалось загрузить атрибуты." }) }),
169
338
  inheritedAttributes.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
170
339
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-2 bg-ui-bg-subtle", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "xsmall", weight: "plus", className: "text-ui-fg-muted uppercase", children: "Унаследованные" }) }),
@@ -243,7 +412,7 @@ const CategoryAttributeTemplatesWidget = ({
243
412
  )
244
413
  ) })
245
414
  ] }),
246
- !isLoading && !isError && attributes.length === 0 && !showAddForm && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-muted text-sm", children: "Нет атрибутов. Добавьте первый." }) }),
415
+ !isLoading && !isError && attributes2.length === 0 && !showAddForm && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-muted text-sm", children: "Нет атрибутов. Добавьте первый." }) }),
247
416
  showAddForm && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 px-6 py-3", children: [
248
417
  /* @__PURE__ */ jsxRuntime.jsx(
249
418
  ui.Input,
@@ -298,7 +467,7 @@ const CategoryAttributeTemplatesWidget = ({
298
467
  size: "small",
299
468
  onClick: () => {
300
469
  setShowAddForm(false);
301
- setAddForm(emptyForm$1());
470
+ setAddForm(emptyForm$2());
302
471
  setMutationError(null);
303
472
  },
304
473
  children: "Отмена"
@@ -314,19 +483,17 @@ adminSdk.defineWidgetConfig({
314
483
  const ProductAttributeValuesWidget = ({
315
484
  data
316
485
  }) => {
317
- var _a, _b, _c;
486
+ var _a;
318
487
  const productId = data.id;
319
488
  const productHandle = data.handle || productId;
320
- const categoryId = ((_b = (_a = data.categories) == null ? void 0 : _a[0]) == null ? void 0 : _b.id) ?? null;
321
489
  const qc = reactQuery.useQueryClient();
322
490
  const [formValues, setFormValues] = React.useState({});
323
491
  const [saveSuccess, setSaveSuccess] = React.useState(false);
324
492
  const [uploadingId, setUploadingId] = React.useState(null);
325
493
  const fileInputs = React.useRef({});
326
494
  const attributesQuery = reactQuery.useQuery({
327
- queryKey: ["category-custom-attributes", categoryId],
328
- queryFn: () => sdk.client.fetch(`/admin/category/${categoryId}/custom-attributes`),
329
- enabled: !!categoryId
495
+ queryKey: ["product-attribute-schema", productId],
496
+ queryFn: () => sdk.client.fetch(`/admin/product/${productId}/attribute-schema`)
330
497
  });
331
498
  const valuesQuery = reactQuery.useQuery({
332
499
  queryKey: ["product-custom-attributes", productId],
@@ -334,10 +501,10 @@ const ProductAttributeValuesWidget = ({
334
501
  });
335
502
  React.useEffect(() => {
336
503
  if (!attributesQuery.data || !valuesQuery.data) return;
337
- const attributes2 = attributesQuery.data.category_custom_attributes;
504
+ const attributes22 = attributesQuery.data.attributes;
338
505
  const values = valuesQuery.data.product_custom_attributes;
339
506
  const initial = {};
340
- for (const attr of attributes2) {
507
+ for (const attr of attributes22) {
341
508
  const existing = values.find(
342
509
  (v) => {
343
510
  var _a2;
@@ -363,12 +530,12 @@ const ProductAttributeValuesWidget = ({
363
530
  });
364
531
  const handleSave = () => {
365
532
  if (!attributesQuery.data) return;
366
- const attributes2 = attributesQuery.data.category_custom_attributes;
367
- const attributesToUpdate = attributes2.filter((attr) => formValues[attr.id] !== void 0).map((attr) => ({ id: attr.id, value: formValues[attr.id] ?? "" }));
533
+ const attributes22 = attributesQuery.data.attributes;
534
+ const attributesToUpdate = attributes22.filter((attr) => formValues[attr.id] !== void 0).map((attr) => ({ id: attr.id, value: formValues[attr.id] ?? "" }));
368
535
  saveMutation.mutate({ attributes: attributesToUpdate });
369
536
  };
370
537
  const handleFileUpload = async (attr, file) => {
371
- var _a2, _b2;
538
+ var _a2, _b;
372
539
  const attrId = attr.id;
373
540
  setUploadingId(attrId);
374
541
  try {
@@ -397,7 +564,7 @@ const ProductAttributeValuesWidget = ({
397
564
  return;
398
565
  }
399
566
  const res = await response.json();
400
- const url = (_b2 = (_a2 = res == null ? void 0 : res.files) == null ? void 0 : _a2[0]) == null ? void 0 : _b2.url;
567
+ const url = (_b = (_a2 = res == null ? void 0 : res.files) == null ? void 0 : _a2[0]) == null ? void 0 : _b.url;
401
568
  if (url) {
402
569
  setFormValues((prev) => ({ ...prev, [attrId]: url }));
403
570
  } else {
@@ -411,22 +578,16 @@ const ProductAttributeValuesWidget = ({
411
578
  setUploadingId(null);
412
579
  }
413
580
  };
414
- if (!categoryId) {
415
- return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", children: [
416
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: "Характеристики" }) }),
417
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-muted text-sm", children: "Назначьте категорию товару, чтобы заполнить характеристики." }) })
418
- ] });
419
- }
420
581
  const isLoading = attributesQuery.isLoading || valuesQuery.isLoading;
421
582
  const isError = attributesQuery.isError || valuesQuery.isError;
422
- const attributes = ((_c = attributesQuery.data) == null ? void 0 : _c.category_custom_attributes) ?? [];
583
+ const attributes2 = ((_a = attributesQuery.data) == null ? void 0 : _a.attributes) ?? [];
423
584
  return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", children: [
424
585
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: "Характеристики" }) }),
425
586
  isLoading && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-muted text-sm", children: "Загрузка…" }) }),
426
587
  isError && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-error text-sm", children: "Не удалось загрузить характеристики." }) }),
427
- !isLoading && !isError && attributes.length === 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-muted text-sm", children: "В категории нет атрибутов. Добавьте их в настройках категории." }) }),
428
- !isLoading && !isError && attributes.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
429
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "divide-y", children: attributes.map((attr) => /* @__PURE__ */ jsxRuntime.jsxs(
588
+ !isLoading && !isError && attributes2.length === 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-muted text-sm", children: "В категории нет атрибутов. Добавьте их в настройках категории." }) }),
589
+ !isLoading && !isError && attributes2.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
590
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "divide-y", children: attributes2.map((attr) => /* @__PURE__ */ jsxRuntime.jsxs(
430
591
  "div",
431
592
  {
432
593
  className: "grid grid-cols-2 items-center gap-4 px-6 py-3",
@@ -524,11 +685,76 @@ const ProductAttributeValuesWidget = ({
524
685
  adminSdk.defineWidgetConfig({
525
686
  zone: "product.details.after"
526
687
  });
688
+ var __defProp$1 = Object.defineProperty;
689
+ var __getOwnPropSymbols$1 = Object.getOwnPropertySymbols;
690
+ var __hasOwnProp$1 = Object.prototype.hasOwnProperty;
691
+ var __propIsEnum$1 = Object.prototype.propertyIsEnumerable;
692
+ var __defNormalProp$1 = (obj, key, value2) => key in obj ? __defProp$1(obj, key, { enumerable: true, configurable: true, writable: true, value: value2 }) : obj[key] = value2;
693
+ var __spreadValues$1 = (a, b) => {
694
+ for (var prop in b || (b = {}))
695
+ if (__hasOwnProp$1.call(b, prop))
696
+ __defNormalProp$1(a, prop, b[prop]);
697
+ if (__getOwnPropSymbols$1)
698
+ for (var prop of __getOwnPropSymbols$1(b)) {
699
+ if (__propIsEnum$1.call(b, prop))
700
+ __defNormalProp$1(a, prop, b[prop]);
701
+ }
702
+ return a;
703
+ };
704
+ var __objRest$1 = (source, exclude) => {
705
+ var target = {};
706
+ for (var prop in source)
707
+ if (__hasOwnProp$1.call(source, prop) && exclude.indexOf(prop) < 0)
708
+ target[prop] = source[prop];
709
+ if (source != null && __getOwnPropSymbols$1)
710
+ for (var prop of __getOwnPropSymbols$1(source)) {
711
+ if (exclude.indexOf(prop) < 0 && __propIsEnum$1.call(source, prop))
712
+ target[prop] = source[prop];
713
+ }
714
+ return target;
715
+ };
716
+ const Globe = React__namespace.forwardRef(
717
+ (_a, ref) => {
718
+ var _b = _a, { color = "currentColor" } = _b, props = __objRest$1(_b, ["color"]);
719
+ return /* @__PURE__ */ React__namespace.createElement(
720
+ "svg",
721
+ __spreadValues$1({
722
+ xmlns: "http://www.w3.org/2000/svg",
723
+ width: 15,
724
+ height: 15,
725
+ viewBox: "0 0 15 15",
726
+ fill: "none",
727
+ ref
728
+ }, props),
729
+ /* @__PURE__ */ React__namespace.createElement(
730
+ "path",
731
+ {
732
+ stroke: color,
733
+ strokeLinecap: "round",
734
+ strokeLinejoin: "round",
735
+ strokeWidth: 1.5,
736
+ d: "M7.5 13.945c1.473 0 2.667-2.886 2.667-6.445S8.973 1.056 7.5 1.056 4.833 3.94 4.833 7.5s1.194 6.445 2.667 6.445M1.056 7.5h12.888"
737
+ }
738
+ ),
739
+ /* @__PURE__ */ React__namespace.createElement(
740
+ "path",
741
+ {
742
+ stroke: color,
743
+ strokeLinecap: "round",
744
+ strokeLinejoin: "round",
745
+ strokeWidth: 1.5,
746
+ d: "M7.5 13.945a6.444 6.444 0 1 0 0-12.89 6.444 6.444 0 0 0 0 12.89"
747
+ }
748
+ )
749
+ );
750
+ }
751
+ );
752
+ Globe.displayName = "Globe";
527
753
  var __defProp = Object.defineProperty;
528
754
  var __getOwnPropSymbols = Object.getOwnPropertySymbols;
529
755
  var __hasOwnProp = Object.prototype.hasOwnProperty;
530
756
  var __propIsEnum = Object.prototype.propertyIsEnumerable;
531
- var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
757
+ var __defNormalProp = (obj, key, value2) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value: value2 }) : obj[key] = value2;
532
758
  var __spreadValues = (a, b) => {
533
759
  for (var prop in b || (b = {}))
534
760
  if (__hasOwnProp.call(b, prop))
@@ -552,7 +778,7 @@ var __objRest = (source, exclude) => {
552
778
  }
553
779
  return target;
554
780
  };
555
- const CogSixTooth = React__namespace.forwardRef(
781
+ const SquaresPlus = React__namespace.forwardRef(
556
782
  (_a, ref) => {
557
783
  var _b = _a, { color = "currentColor" } = _b, props = __objRest(_b, ["color"]);
558
784
  return /* @__PURE__ */ React__namespace.createElement(
@@ -566,42 +792,39 @@ const CogSixTooth = React__namespace.forwardRef(
566
792
  ref
567
793
  }, props),
568
794
  /* @__PURE__ */ React__namespace.createElement(
569
- "g",
795
+ "path",
570
796
  {
571
797
  stroke: color,
572
798
  strokeLinecap: "round",
573
799
  strokeLinejoin: "round",
574
800
  strokeWidth: 1.5,
575
- clipPath: "url(#a)"
576
- },
577
- /* @__PURE__ */ React__namespace.createElement("path", { d: "M7.5 9.5a2 2 0 1 0 0-4 2 2 0 0 0 0 4" }),
578
- /* @__PURE__ */ React__namespace.createElement("path", { d: "m12.989 5.97-.826-.292a5 5 0 0 0-.323-.685 5 5 0 0 0-.43-.621l.16-.86a1.43 1.43 0 0 0-.692-1.503l-.312-.18a1.43 1.43 0 0 0-1.647.152l-.663.566a5 5 0 0 0-1.513 0L6.08 1.98a1.43 1.43 0 0 0-1.647-.152l-.312.18a1.43 1.43 0 0 0-.691 1.503l.16.857c-.32.4-.574.841-.758 1.31l-.82.29a1.43 1.43 0 0 0-.956 1.35v.36c0 .608.383 1.15.955 1.35l.826.292c.09.232.194.462.323.684.128.222.275.427.43.622l-.16.86c-.111.597.166 1.2.691 1.503l.312.18a1.43 1.43 0 0 0 1.647-.152l.663-.567a5 5 0 0 0 1.512 0l.663.568a1.43 1.43 0 0 0 1.647.152l.312-.18c.526-.304.803-.906.691-1.502l-.16-.86c.32-.398.575-.84.757-1.308l.822-.29c.572-.202.956-.743.956-1.35v-.36c0-.608-.383-1.149-.956-1.35z" })
579
- ),
580
- /* @__PURE__ */ React__namespace.createElement("defs", null, /* @__PURE__ */ React__namespace.createElement("clipPath", { id: "a" }, /* @__PURE__ */ React__namespace.createElement("path", { fill: "#fff", d: "M0 0h15v15H0z" })))
801
+ d: "M5.056 1.944H2.833a.89.89 0 0 0-.889.89v2.221c0 .491.398.89.89.89h2.222c.49 0 .888-.399.888-.89V2.833a.89.89 0 0 0-.888-.889M12.167 1.944H9.944a.89.89 0 0 0-.888.89v2.221c0 .491.398.89.888.89h2.223c.49 0 .889-.399.889-.89V2.833a.89.89 0 0 0-.89-.889M5.056 9.056H2.833a.89.89 0 0 0-.889.889v2.222c0 .49.398.889.89.889h2.222c.49 0 .888-.398.888-.89V9.946a.89.89 0 0 0-.888-.89M11.056 8.611v4.445M13.278 10.833H8.833"
802
+ }
803
+ )
581
804
  );
582
805
  }
583
806
  );
584
- CogSixTooth.displayName = "CogSixTooth";
585
- const emptyForm = () => ({
807
+ SquaresPlus.displayName = "SquaresPlus";
808
+ const emptyForm$1 = () => ({
586
809
  label: "",
587
810
  type: "text",
588
811
  unit: "",
589
812
  description: ""
590
813
  });
591
- const typeLabel = (t) => t === "text" ? "Текст" : t === "number" ? "Число" : t === "file" ? "Файл" : t === "boolean" ? "Да/Нет" : t;
592
- const ProductAttributesSettingsPage = () => {
814
+ const typeLabel$1 = (t) => t === "text" ? "Текст" : t === "number" ? "Число" : t === "file" ? "Файл" : t === "boolean" ? "Да/Нет" : t;
815
+ const AttributeTemplatesSettingsPage = () => {
593
816
  const qc = reactQuery.useQueryClient();
594
- const queryKey = ["attribute-presets"];
817
+ const queryKey = ["attribute-templates"];
595
818
  const [showAddForm, setShowAddForm] = React.useState(false);
596
- const [addForm, setAddForm] = React.useState(emptyForm());
819
+ const [addForm, setAddForm] = React.useState(emptyForm$1());
597
820
  const [confirmDeleteId, setConfirmDeleteId] = React.useState(null);
598
821
  const { data, isLoading, isError } = reactQuery.useQuery({
599
822
  queryKey,
600
- queryFn: () => sdk.client.fetch(`/admin/attribute-presets`)
823
+ queryFn: () => sdk.client.fetch(`/admin/attribute-templates`)
601
824
  });
602
- const presets = (data == null ? void 0 : data.attribute_presets) ?? [];
825
+ const templates2 = (data == null ? void 0 : data.attribute_templates) ?? [];
603
826
  const createMutation = reactQuery.useMutation({
604
- mutationFn: (body) => sdk.client.fetch(`/admin/attribute-presets`, {
827
+ mutationFn: (body) => sdk.client.fetch(`/admin/attribute-templates`, {
605
828
  method: "POST",
606
829
  body: {
607
830
  label: body.label,
@@ -613,11 +836,11 @@ const ProductAttributesSettingsPage = () => {
613
836
  onSuccess: () => {
614
837
  qc.invalidateQueries({ queryKey });
615
838
  setShowAddForm(false);
616
- setAddForm(emptyForm());
839
+ setAddForm(emptyForm$1());
617
840
  }
618
841
  });
619
842
  const deleteMutation = reactQuery.useMutation({
620
- mutationFn: (id) => sdk.client.fetch(`/admin/attribute-presets`, {
843
+ mutationFn: (id) => sdk.client.fetch(`/admin/attribute-templates`, {
621
844
  method: "PATCH",
622
845
  body: { id, deleted_at: (/* @__PURE__ */ new Date()).toISOString() }
623
846
  }),
@@ -638,154 +861,158 @@ const ProductAttributesSettingsPage = () => {
638
861
  return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", children: [
639
862
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between px-6 py-4", children: [
640
863
  /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
641
- /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h1", children: "Пресеты атрибутов" }),
642
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: "Глобальные шаблоны атрибутов, которые можно применить к любой категории." })
864
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h1", children: "Шаблоны атрибутов" }),
865
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: "Заготовки атрибутов, которые можно применить к любой категории." })
643
866
  ] }),
644
- !showAddForm && /* @__PURE__ */ jsxRuntime.jsx(
645
- ui.Button,
646
- {
647
- variant: "secondary",
648
- size: "small",
649
- onClick: () => setShowAddForm(true),
650
- children: "+ Добавить пресет"
651
- }
652
- )
867
+ !showAddForm && /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "secondary", size: "small", onClick: () => setShowAddForm(true), children: "+ Добавить" })
653
868
  ] }),
654
869
  isLoading && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-muted text-sm", children: "Загрузка…" }) }),
655
- isError && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-error text-sm", children: "Не удалось загрузить пресеты." }) }),
656
- !isLoading && !isError && presets.length === 0 && !showAddForm && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-muted text-sm", children: "Пресетов пока нет. Добавьте первый." }) }),
657
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "divide-y", children: presets.map(
658
- (p) => confirmDeleteId === p.id ? /* @__PURE__ */ jsxRuntime.jsxs(
659
- "div",
660
- {
661
- className: "flex items-center gap-3 px-6 py-3 text-sm",
662
- children: [
663
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex-1", children: [
664
- "Удалить «",
665
- p.label,
666
- "»?"
667
- ] }),
668
- /* @__PURE__ */ jsxRuntime.jsx(
669
- ui.Button,
670
- {
671
- size: "small",
672
- variant: "danger",
673
- onClick: () => deleteMutation.mutate(p.id),
674
- isLoading: deleteMutation.isPending,
675
- children: "Удалить"
676
- }
677
- ),
678
- /* @__PURE__ */ jsxRuntime.jsx(
679
- ui.Button,
680
- {
681
- size: "small",
682
- variant: "secondary",
683
- onClick: () => setConfirmDeleteId(null),
684
- children: "Отмена"
685
- }
686
- )
687
- ]
688
- },
689
- p.id
690
- ) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3 px-6 py-3", children: [
870
+ isError && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-error text-sm", children: "Не удалось загрузить шаблоны." }) }),
871
+ !isLoading && !isError && templates2.length === 0 && !showAddForm && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-muted text-sm", children: "Шаблонов пока нет." }) }),
872
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "divide-y", children: templates2.map(
873
+ (p) => confirmDeleteId === p.id ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3 px-6 py-3 text-sm", children: [
874
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex-1", children: [
875
+ "Удалить «",
876
+ p.label,
877
+ "»?"
878
+ ] }),
879
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", variant: "danger", onClick: () => deleteMutation.mutate(p.id), isLoading: deleteMutation.isPending, children: "Удалить" }),
880
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", variant: "secondary", onClick: () => setConfirmDeleteId(null), children: "Отмена" })
881
+ ] }, p.id) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3 px-6 py-3", children: [
691
882
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1", children: [
692
883
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
693
884
  /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", weight: "plus", children: p.label }),
694
885
  /* @__PURE__ */ jsxRuntime.jsxs(ui.Badge, { size: "2xsmall", color: "grey", children: [
695
- typeLabel(p.type),
886
+ typeLabel$1(p.type),
696
887
  p.unit ? `, ${p.unit}` : ""
697
888
  ] })
698
889
  ] }),
699
890
  p.description && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "xsmall", className: "text-ui-fg-subtle", children: p.description })
700
891
  ] }),
701
- /* @__PURE__ */ jsxRuntime.jsx(
702
- "button",
703
- {
704
- onClick: () => setConfirmDeleteId(p.id),
705
- className: "text-xs text-ui-fg-error hover:underline",
706
- children: "Удалить"
707
- }
708
- )
892
+ /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: () => setConfirmDeleteId(p.id), className: "text-xs text-ui-fg-error hover:underline", children: "Удалить" })
709
893
  ] }, p.id)
710
894
  ) }),
711
895
  showAddForm && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-2 px-6 py-4", children: [
712
896
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
713
- /* @__PURE__ */ jsxRuntime.jsx(
714
- ui.Input,
715
- {
716
- value: addForm.label,
717
- onChange: (e) => setAddForm((f) => ({ ...f, label: e.target.value })),
718
- placeholder: "Название (например, Сертификат)",
719
- className: "flex-1 h-8 text-sm",
720
- autoFocus: true
721
- }
722
- ),
723
- /* @__PURE__ */ jsxRuntime.jsxs(
724
- "select",
725
- {
726
- value: addForm.type,
727
- onChange: (e) => setAddForm((f) => ({
728
- ...f,
729
- type: e.target.value,
730
- unit: e.target.value === "number" ? f.unit : ""
731
- })),
732
- className: "h-8 rounded border border-ui-border-base bg-ui-bg-base px-2 text-sm",
733
- children: [
734
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "text", children: "Текст" }),
735
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "number", children: "Число" }),
736
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "file", children: "Файл" }),
737
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "boolean", children: "Да/Нет" })
738
- ]
739
- }
740
- ),
741
- addForm.type === "number" && /* @__PURE__ */ jsxRuntime.jsx(
742
- ui.Input,
743
- {
744
- value: addForm.unit,
745
- onChange: (e) => setAddForm((f) => ({ ...f, unit: e.target.value })),
746
- placeholder: "ед. (кг, м...)",
747
- className: "w-28 h-8 text-sm"
748
- }
749
- )
897
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Input, { value: addForm.label, onChange: (e) => setAddForm((f) => ({ ...f, label: e.target.value })), placeholder: "Название", className: "flex-1 h-8 text-sm", autoFocus: true }),
898
+ /* @__PURE__ */ jsxRuntime.jsxs("select", { value: addForm.type, onChange: (e) => setAddForm((f) => ({ ...f, type: e.target.value, unit: e.target.value === "number" ? f.unit : "" })), className: "h-8 rounded border border-ui-border-base bg-ui-bg-base px-2 text-sm", children: [
899
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "text", children: "Текст" }),
900
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "number", children: "Число" }),
901
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "file", children: "Файл" }),
902
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "boolean", children: "Да/Нет" })
903
+ ] }),
904
+ addForm.type === "number" && /* @__PURE__ */ jsxRuntime.jsx(ui.Input, { value: addForm.unit, onChange: (e) => setAddForm((f) => ({ ...f, unit: e.target.value })), placeholder: "ед.", className: "w-28 h-8 text-sm" })
750
905
  ] }),
751
- /* @__PURE__ */ jsxRuntime.jsx(
752
- ui.Input,
753
- {
754
- value: addForm.description,
755
- onChange: (e) => setAddForm((f) => ({ ...f, description: e.target.value })),
756
- placeholder: "Описание (необязательно)",
757
- className: "h-8 text-sm"
758
- }
759
- ),
906
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Input, { value: addForm.description, onChange: (e) => setAddForm((f) => ({ ...f, description: e.target.value })), placeholder: "Описание (необязательно)", className: "h-8 text-sm" }),
760
907
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex justify-end gap-2", children: [
761
- /* @__PURE__ */ jsxRuntime.jsx(
762
- ui.Button,
763
- {
764
- variant: "secondary",
765
- size: "small",
766
- onClick: () => {
767
- setShowAddForm(false);
768
- setAddForm(emptyForm());
769
- },
770
- children: "Отмена"
771
- }
772
- ),
773
- /* @__PURE__ */ jsxRuntime.jsx(
774
- ui.Button,
775
- {
776
- size: "small",
777
- onClick: handleAdd,
778
- isLoading: createMutation.isPending,
779
- children: "Создать"
780
- }
781
- )
908
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "secondary", size: "small", onClick: () => {
909
+ setShowAddForm(false);
910
+ setAddForm(emptyForm$1());
911
+ }, children: "Отмена" }),
912
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", onClick: handleAdd, isLoading: createMutation.isPending, children: "Создать" })
782
913
  ] })
783
914
  ] })
784
915
  ] });
785
916
  };
917
+ const config$1 = adminSdk.defineRouteConfig({
918
+ label: "Attribute Templates",
919
+ icon: SquaresPlus
920
+ });
921
+ const emptyForm = () => ({ label: "", type: "text", unit: "" });
922
+ const typeLabel = (t) => t === "text" ? "Текст" : t === "number" ? "Число" : t === "file" ? "Файл" : t === "boolean" ? "Да/Нет" : t;
923
+ const GlobalAttributesSettingsPage = () => {
924
+ const qc = reactQuery.useQueryClient();
925
+ const queryKey = ["global-attributes"];
926
+ const [showAddForm, setShowAddForm] = React.useState(false);
927
+ const [addForm, setAddForm] = React.useState(emptyForm());
928
+ const [confirmDeleteId, setConfirmDeleteId] = React.useState(null);
929
+ const { data, isLoading, isError } = reactQuery.useQuery({
930
+ queryKey,
931
+ queryFn: () => sdk.client.fetch(`/admin/global-attributes`)
932
+ });
933
+ const attrs = (data == null ? void 0 : data.global_attributes) ?? [];
934
+ const createMutation = reactQuery.useMutation({
935
+ mutationFn: (body) => sdk.client.fetch(`/admin/global-attributes`, {
936
+ method: "POST",
937
+ body: {
938
+ label: body.label,
939
+ type: body.type,
940
+ unit: body.type === "number" && body.unit ? body.unit : null
941
+ }
942
+ }),
943
+ onSuccess: () => {
944
+ qc.invalidateQueries({ queryKey });
945
+ setShowAddForm(false);
946
+ setAddForm(emptyForm());
947
+ }
948
+ });
949
+ const deleteMutation = reactQuery.useMutation({
950
+ mutationFn: (id) => sdk.client.fetch(`/admin/global-attributes`, {
951
+ method: "PATCH",
952
+ body: { id, deleted_at: (/* @__PURE__ */ new Date()).toISOString() }
953
+ }),
954
+ onSuccess: () => {
955
+ qc.invalidateQueries({ queryKey });
956
+ setConfirmDeleteId(null);
957
+ }
958
+ });
959
+ const handleAdd = () => {
960
+ if (!addForm.label.trim()) return;
961
+ createMutation.mutate({
962
+ label: addForm.label.trim(),
963
+ type: addForm.type,
964
+ unit: addForm.unit.trim()
965
+ });
966
+ };
967
+ return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", children: [
968
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between px-6 py-4", children: [
969
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
970
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h1", children: "Глобальные атрибуты" }),
971
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: "Применяются ко всем товарам автоматически. Значения у продуктов необязательны." })
972
+ ] }),
973
+ !showAddForm && /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "secondary", size: "small", onClick: () => setShowAddForm(true), children: "+ Добавить" })
974
+ ] }),
975
+ isLoading && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-muted text-sm", children: "Загрузка…" }) }),
976
+ isError && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-error text-sm", children: "Не удалось загрузить." }) }),
977
+ !isLoading && !isError && attrs.length === 0 && !showAddForm && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-muted text-sm", children: "Глобальных атрибутов нет." }) }),
978
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "divide-y", children: attrs.map(
979
+ (a) => confirmDeleteId === a.id ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3 px-6 py-3 text-sm", children: [
980
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex-1", children: [
981
+ "Удалить «",
982
+ a.label,
983
+ "»?"
984
+ ] }),
985
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", variant: "danger", onClick: () => deleteMutation.mutate(a.id), isLoading: deleteMutation.isPending, children: "Удалить" }),
986
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", variant: "secondary", onClick: () => setConfirmDeleteId(null), children: "Отмена" })
987
+ ] }, a.id) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3 px-6 py-3", children: [
988
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex-1 text-sm", children: a.label }),
989
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Badge, { size: "2xsmall", color: "grey", children: [
990
+ typeLabel(a.type),
991
+ a.unit ? `, ${a.unit}` : ""
992
+ ] }),
993
+ /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: () => setConfirmDeleteId(a.id), className: "text-xs text-ui-fg-error hover:underline", children: "Удалить" })
994
+ ] }, a.id)
995
+ ) }),
996
+ showAddForm && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 px-6 py-4", children: [
997
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Input, { value: addForm.label, onChange: (e) => setAddForm((f) => ({ ...f, label: e.target.value })), placeholder: "Название", className: "flex-1 h-8 text-sm", autoFocus: true }),
998
+ /* @__PURE__ */ jsxRuntime.jsxs("select", { value: addForm.type, onChange: (e) => setAddForm((f) => ({ ...f, type: e.target.value, unit: e.target.value === "number" ? f.unit : "" })), className: "h-8 rounded border border-ui-border-base bg-ui-bg-base px-2 text-sm", children: [
999
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "text", children: "Текст" }),
1000
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "number", children: "Число" }),
1001
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "file", children: "Файл" }),
1002
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "boolean", children: "Да/Нет" })
1003
+ ] }),
1004
+ addForm.type === "number" && /* @__PURE__ */ jsxRuntime.jsx(ui.Input, { value: addForm.unit, onChange: (e) => setAddForm((f) => ({ ...f, unit: e.target.value })), placeholder: "ед.", className: "w-28 h-8 text-sm" }),
1005
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", onClick: handleAdd, isLoading: createMutation.isPending, children: "Создать" }),
1006
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "secondary", size: "small", onClick: () => {
1007
+ setShowAddForm(false);
1008
+ setAddForm(emptyForm());
1009
+ }, children: "Отмена" })
1010
+ ] })
1011
+ ] });
1012
+ };
786
1013
  const config = adminSdk.defineRouteConfig({
787
- label: "Product Attributes",
788
- icon: CogSixTooth
1014
+ label: "Global Attributes",
1015
+ icon: Globe
789
1016
  });
790
1017
  const widgetModule = { widgets: [
791
1018
  {
@@ -800,17 +1027,29 @@ const widgetModule = { widgets: [
800
1027
  const routeModule = {
801
1028
  routes: [
802
1029
  {
803
- Component: ProductAttributesSettingsPage,
804
- path: "/settings/product-attributes"
1030
+ Component: AttributeTemplatesSettingsPage,
1031
+ path: "/settings/attribute-templates"
1032
+ },
1033
+ {
1034
+ Component: GlobalAttributesSettingsPage,
1035
+ path: "/settings/global-attributes"
805
1036
  }
806
1037
  ]
807
1038
  };
808
1039
  const menuItemModule = {
809
1040
  menuItems: [
1041
+ {
1042
+ label: config$1.label,
1043
+ icon: config$1.icon,
1044
+ path: "/settings/attribute-templates",
1045
+ nested: void 0,
1046
+ rank: void 0,
1047
+ translationNs: void 0
1048
+ },
810
1049
  {
811
1050
  label: config.label,
812
1051
  icon: config.icon,
813
- path: "/settings/product-attributes",
1052
+ path: "/settings/global-attributes",
814
1053
  nested: void 0,
815
1054
  rank: void 0,
816
1055
  translationNs: void 0