@fayz-ai/plugin-shop 0.8.0 → 0.8.1

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 (55) hide show
  1. package/dist/ShopPage-HCLXMN7Q.js +743 -0
  2. package/dist/ShopPage-HCLXMN7Q.js.map +1 -0
  3. package/dist/ShopPage-JF2SQBKO.cjs +745 -0
  4. package/dist/ShopPage-JF2SQBKO.cjs.map +1 -0
  5. package/dist/ShopPage.d.ts +23 -1
  6. package/dist/ShopPage.d.ts.map +1 -1
  7. package/dist/data/customerDataProvider.d.ts +5 -0
  8. package/dist/data/customerDataProvider.d.ts.map +1 -0
  9. package/dist/data/discountDataProvider.d.ts +5 -0
  10. package/dist/data/discountDataProvider.d.ts.map +1 -0
  11. package/dist/data/productDataProvider.d.ts +6 -0
  12. package/dist/data/productDataProvider.d.ts.map +1 -0
  13. package/dist/data/shared.d.ts +12 -0
  14. package/dist/data/shared.d.ts.map +1 -0
  15. package/dist/data/shippingZoneDataProvider.d.ts +13 -0
  16. package/dist/data/shippingZoneDataProvider.d.ts.map +1 -0
  17. package/dist/index.cjs +983 -26
  18. package/dist/index.cjs.map +1 -1
  19. package/dist/index.d.ts +2 -0
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +984 -27
  22. package/dist/index.js.map +1 -1
  23. package/dist/views/OrderDetailView.d.ts +10 -0
  24. package/dist/views/OrderDetailView.d.ts.map +1 -0
  25. package/dist/views/ProductImagesTab.d.ts +19 -0
  26. package/dist/views/ProductImagesTab.d.ts.map +1 -0
  27. package/dist/views/customerEntity.d.ts +4 -0
  28. package/dist/views/customerEntity.d.ts.map +1 -0
  29. package/dist/views/customerLookup.d.ts +22 -0
  30. package/dist/views/customerLookup.d.ts.map +1 -0
  31. package/dist/views/discountEntity.d.ts +4 -0
  32. package/dist/views/discountEntity.d.ts.map +1 -0
  33. package/dist/views/productEntity.d.ts +6 -0
  34. package/dist/views/productEntity.d.ts.map +1 -0
  35. package/dist/views/shippingZoneEntity.d.ts +4 -0
  36. package/dist/views/shippingZoneEntity.d.ts.map +1 -0
  37. package/package.json +5 -5
  38. package/src/ShopPage.tsx +156 -54
  39. package/src/data/customerDataProvider.ts +31 -0
  40. package/src/data/discountDataProvider.ts +42 -0
  41. package/src/data/productDataProvider.ts +54 -0
  42. package/src/data/shared.ts +42 -0
  43. package/src/data/shippingZoneDataProvider.ts +49 -0
  44. package/src/index.ts +101 -12
  45. package/src/views/OrderDetailView.tsx +575 -0
  46. package/src/views/ProductImagesTab.tsx +183 -0
  47. package/src/views/customerEntity.tsx +110 -0
  48. package/src/views/customerLookup.ts +31 -0
  49. package/src/views/discountEntity.tsx +137 -0
  50. package/src/views/productEntity.tsx +229 -0
  51. package/src/views/shippingZoneEntity.tsx +123 -0
  52. package/dist/ShopPage-65PPVKBD.js +0 -271
  53. package/dist/ShopPage-65PPVKBD.js.map +0 -1
  54. package/dist/ShopPage-PG5GKOZU.cjs +0 -273
  55. package/dist/ShopPage-PG5GKOZU.cjs.map +0 -1
package/dist/index.cjs CHANGED
@@ -1,15 +1,919 @@
1
1
  'use strict';
2
2
 
3
- var React = require('react');
3
+ var React2 = require('react');
4
4
  var shop = require('@fayz-ai/shop');
5
5
  var saas = require('@fayz-ai/saas');
6
6
  var jsxRuntime = require('react/jsx-runtime');
7
7
 
8
8
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
9
9
 
10
- var React__default = /*#__PURE__*/_interopDefault(React);
10
+ var React2__default = /*#__PURE__*/_interopDefault(React2);
11
11
 
12
12
  // src/index.ts
13
+ function ProductImagesTab({ item, getProvider }) {
14
+ const product = item;
15
+ const [images, setImages] = React2.useState([]);
16
+ const [busy, setBusy] = React2.useState(false);
17
+ const [error, setError] = React2.useState(null);
18
+ const fileRef = React2.useRef(null);
19
+ async function reload() {
20
+ const fresh = await getProvider().getProduct(product.id);
21
+ setImages(sortImages(fresh?.images ?? []));
22
+ }
23
+ React2.useEffect(() => {
24
+ void reload().catch(() => {
25
+ });
26
+ }, [product.id]);
27
+ async function run(action) {
28
+ setBusy(true);
29
+ setError(null);
30
+ try {
31
+ await action();
32
+ await reload();
33
+ } catch (err) {
34
+ setError(err instanceof Error ? err.message : String(err));
35
+ } finally {
36
+ setBusy(false);
37
+ }
38
+ }
39
+ async function upload(files) {
40
+ if (!files?.length) return;
41
+ await run(async () => {
42
+ for (const file of Array.from(files)) {
43
+ await getProvider().uploadProductImage(product.id, file);
44
+ }
45
+ if (fileRef.current) fileRef.current.value = "";
46
+ });
47
+ }
48
+ function move(index, delta) {
49
+ const target = index + delta;
50
+ if (target < 0 || target >= images.length) return;
51
+ const a = images[index];
52
+ const b = images[target];
53
+ void run(async () => {
54
+ const update2 = getProvider().updateProductImage;
55
+ if (!update2) throw new Error("Este provider n\xE3o suporta reordenar imagens.");
56
+ await update2(a.id, { sortOrder: b.sortOrder ?? target });
57
+ await update2(b.id, { sortOrder: a.sortOrder ?? index });
58
+ });
59
+ }
60
+ const update = getProvider().updateProductImage;
61
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-4", children: [
62
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-center justify-between gap-3", children: [
63
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
64
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-sm font-semibold", children: "Fotos do produto" }),
65
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-muted-foreground", children: "A primeira foto \xE9 a capa \u2014 aparece no cat\xE1logo, no carrinho e no pedido. As demais viram o carrossel na p\xE1gina do produto." })
66
+ ] }),
67
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "inline-flex cursor-pointer items-center rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground transition hover:opacity-90", children: [
68
+ busy ? "Enviando\u2026" : "Adicionar fotos",
69
+ /* @__PURE__ */ jsxRuntime.jsx(
70
+ "input",
71
+ {
72
+ ref: fileRef,
73
+ type: "file",
74
+ accept: "image/*",
75
+ multiple: true,
76
+ disabled: busy,
77
+ className: "hidden",
78
+ onChange: (event) => void upload(event.target.files)
79
+ }
80
+ )
81
+ ] })
82
+ ] }),
83
+ error && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "rounded-md border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-800", children: error }),
84
+ images.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "rounded-lg border border-dashed px-4 py-10 text-center text-sm text-muted-foreground", children: "Nenhuma foto ainda. Produtos com mais de uma foto vendem melhor \u2014 mostre a embalagem, o produto pronto e uma refer\xEAncia de tamanho." }) : /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-4", children: images.map((image, index) => /* @__PURE__ */ jsxRuntime.jsxs("li", { className: "overflow-hidden rounded-lg border", children: [
85
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative aspect-square bg-muted", children: [
86
+ /* @__PURE__ */ jsxRuntime.jsx("img", { src: image.url, alt: image.altText ?? "", className: "h-full w-full object-cover" }),
87
+ image.isPrimary && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "absolute left-2 top-2 rounded-full bg-primary px-2 py-0.5 text-[11px] font-bold text-primary-foreground", children: "Capa" })
88
+ ] }),
89
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between gap-1 px-2 py-1.5", children: [
90
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-1", children: [
91
+ /* @__PURE__ */ jsxRuntime.jsx(
92
+ IconButton,
93
+ {
94
+ label: "Mover para tr\xE1s",
95
+ disabled: busy || index === 0,
96
+ onClick: () => move(index, -1),
97
+ children: "\u2190"
98
+ }
99
+ ),
100
+ /* @__PURE__ */ jsxRuntime.jsx(
101
+ IconButton,
102
+ {
103
+ label: "Mover para frente",
104
+ disabled: busy || index === images.length - 1,
105
+ onClick: () => move(index, 1),
106
+ children: "\u2192"
107
+ }
108
+ )
109
+ ] }),
110
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-1", children: [
111
+ !image.isPrimary && update && /* @__PURE__ */ jsxRuntime.jsx(
112
+ "button",
113
+ {
114
+ type: "button",
115
+ disabled: busy,
116
+ onClick: () => void run(() => update(image.id, { isPrimary: true })),
117
+ className: "rounded px-1.5 py-0.5 text-xs text-muted-foreground hover:bg-muted hover:text-foreground disabled:opacity-50",
118
+ children: "Usar como capa"
119
+ }
120
+ ),
121
+ /* @__PURE__ */ jsxRuntime.jsx(
122
+ "button",
123
+ {
124
+ type: "button",
125
+ disabled: busy,
126
+ onClick: () => void run(() => getProvider().deleteProductImage(image.id)),
127
+ className: "rounded px-1.5 py-0.5 text-xs text-red-600 hover:bg-red-50 disabled:opacity-50",
128
+ children: "Excluir"
129
+ }
130
+ )
131
+ ] })
132
+ ] })
133
+ ] }, image.id)) })
134
+ ] });
135
+ }
136
+ function IconButton({ label, disabled, onClick, children }) {
137
+ return /* @__PURE__ */ jsxRuntime.jsx(
138
+ "button",
139
+ {
140
+ type: "button",
141
+ "aria-label": label,
142
+ title: label,
143
+ disabled,
144
+ onClick,
145
+ className: "rounded border px-1.5 py-0.5 text-xs text-muted-foreground transition hover:bg-muted hover:text-foreground disabled:opacity-40",
146
+ children
147
+ }
148
+ );
149
+ }
150
+ function sortImages(images) {
151
+ return [...images].sort((a, b) => {
152
+ if (a.isPrimary !== b.isPrimary) return a.isPrimary ? -1 : 1;
153
+ return (a.sortOrder ?? 0) - (b.sortOrder ?? 0);
154
+ });
155
+ }
156
+ var STATUS_TONE = {
157
+ active: "bg-emerald-100 text-emerald-800",
158
+ draft: "bg-gray-100 text-gray-700",
159
+ archived: "bg-orange-100 text-orange-700"
160
+ };
161
+ var STATUS_LABEL = {
162
+ active: "Ativo",
163
+ draft: "Rascunho",
164
+ archived: "Arquivado"
165
+ };
166
+ function money(value, currency = "BRL") {
167
+ const n = Number(value);
168
+ if (!Number.isFinite(n)) return "\u2014";
169
+ return new Intl.NumberFormat("pt-BR", { style: "currency", currency }).format(n);
170
+ }
171
+ function slugify(value) {
172
+ return value.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
173
+ }
174
+ function productImage(getProvider) {
175
+ return {
176
+ get: (row) => {
177
+ const images = row.images;
178
+ return images?.find((i) => i.isPrimary)?.url ?? images?.[0]?.url;
179
+ },
180
+ upload: async (file, row) => {
181
+ if (!row.id) throw new Error("Salve o produto antes de enviar a imagem.");
182
+ const image = await getProvider().uploadProductImage(row.id, file);
183
+ return image.url;
184
+ },
185
+ accept: "image/*"
186
+ };
187
+ }
188
+ function buildShopProductEntity(currencyCode = "BRL", getProvider) {
189
+ const fields = [
190
+ // — Geral —
191
+ {
192
+ key: "name",
193
+ label: "Nome do produto",
194
+ type: "text",
195
+ required: true,
196
+ group: "general",
197
+ placeholder: "Costela Bovina Defumada 400g",
198
+ searchable: true,
199
+ showInTable: true,
200
+ sortable: true,
201
+ renderCell: (_v, row) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
202
+ row.images?.[0]?.url && /* @__PURE__ */ jsxRuntime.jsx(
203
+ "img",
204
+ {
205
+ src: row.images[0].url,
206
+ alt: "",
207
+ className: "h-9 w-9 rounded object-cover",
208
+ loading: "lazy"
209
+ }
210
+ ),
211
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-w-0", children: [
212
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "truncate font-medium", children: row.name }),
213
+ row.sku && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-muted-foreground", children: row.sku })
214
+ ] })
215
+ ] })
216
+ },
217
+ {
218
+ key: "slug",
219
+ label: "URL",
220
+ type: "text",
221
+ group: "general",
222
+ placeholder: "gerado a partir do nome",
223
+ hint: "Endere\xE7o do produto na loja. Deixe em branco para gerar automaticamente.",
224
+ showInTable: false
225
+ },
226
+ {
227
+ key: "description",
228
+ label: "Descri\xE7\xE3o",
229
+ type: "textarea",
230
+ group: "general",
231
+ span: 2,
232
+ placeholder: "Como \xE9 feito, como preparar, o que acompanha\u2026",
233
+ showInTable: false
234
+ },
235
+ // — Preço —
236
+ {
237
+ key: "price",
238
+ label: "Pre\xE7o de venda",
239
+ type: "currency",
240
+ required: true,
241
+ group: "pricing",
242
+ currency: currencyCode,
243
+ currencySymbol: "R$",
244
+ currencyLocale: "pt-BR",
245
+ showInTable: true,
246
+ sortable: true,
247
+ renderCell: (v, row) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-right", children: [
248
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium", children: money(v, row.currency) }),
249
+ row.compareAtPrice > row.price && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ml-2 text-xs text-muted-foreground line-through", children: money(row.compareAtPrice, row.currency) })
250
+ ] })
251
+ },
252
+ {
253
+ key: "compareAtPrice",
254
+ label: "Pre\xE7o comparativo",
255
+ type: "currency",
256
+ group: "pricing",
257
+ currency: currencyCode,
258
+ currencySymbol: "R$",
259
+ currencyLocale: "pt-BR",
260
+ hint: 'Pre\xE7o "de" riscado. Deixe vazio se n\xE3o estiver em promo\xE7\xE3o.',
261
+ showInTable: false
262
+ },
263
+ {
264
+ key: "discount",
265
+ label: "Desconto",
266
+ type: "computed",
267
+ group: "pricing",
268
+ showInTable: false,
269
+ compute: (vals) => {
270
+ const price = Number(vals.price) || 0;
271
+ const compare = Number(vals.compareAtPrice) || 0;
272
+ if (compare <= 0 || price <= 0 || compare <= price) return null;
273
+ const pct = (compare - price) / compare * 100;
274
+ return { display: `-${pct.toFixed(0)}%`, tone: "positive" };
275
+ }
276
+ },
277
+ // — Estoque —
278
+ {
279
+ key: "sku",
280
+ label: "SKU",
281
+ type: "text",
282
+ group: "inventory",
283
+ placeholder: "ART-001",
284
+ searchable: true,
285
+ showInTable: false
286
+ },
287
+ {
288
+ key: "inventoryCount",
289
+ label: "Estoque",
290
+ type: "number",
291
+ group: "inventory",
292
+ min: 0,
293
+ placeholder: "0",
294
+ hint: "Chega a zero e o produto aparece como esgotado na loja.",
295
+ showInTable: true,
296
+ sortable: true,
297
+ renderCell: (v) => {
298
+ const n = Number(v) || 0;
299
+ return /* @__PURE__ */ jsxRuntime.jsx("span", { className: `block text-right ${n === 0 ? "font-medium text-destructive" : n <= 3 ? "text-orange-600" : ""}`, children: n });
300
+ }
301
+ },
302
+ // — Publicação —
303
+ {
304
+ key: "status",
305
+ label: "Status",
306
+ type: "segmented",
307
+ required: true,
308
+ group: "publishing",
309
+ span: 2,
310
+ options: [
311
+ { label: "Rascunho", value: "draft", description: "S\xF3 voc\xEA v\xEA. N\xE3o aparece na loja." },
312
+ { label: "Ativo", value: "active", description: "Publicado e \xE0 venda." },
313
+ { label: "Arquivado", value: "archived", description: "Fora da loja, hist\xF3rico preservado." }
314
+ ],
315
+ showInTable: true,
316
+ sortable: false,
317
+ renderCell: (v) => {
318
+ const s = String(v ?? "draft");
319
+ return /* @__PURE__ */ jsxRuntime.jsx("span", { className: `inline-flex items-center rounded-full px-2 py-0.5 text-[10px] font-medium ${STATUS_TONE[s] ?? STATUS_TONE.draft}`, children: STATUS_LABEL[s] ?? s });
320
+ }
321
+ },
322
+ {
323
+ key: "sortOrder",
324
+ label: "Ordem na vitrine",
325
+ type: "number",
326
+ group: "publishing",
327
+ placeholder: "0",
328
+ hint: "Menor aparece primeiro.",
329
+ showInTable: false
330
+ },
331
+ // — Envio —
332
+ {
333
+ key: "isPhysical",
334
+ label: "Produto f\xEDsico",
335
+ type: "boolean",
336
+ group: "shipping",
337
+ hint: "Desmarque para itens digitais, que n\xE3o calculam frete.",
338
+ showInTable: false
339
+ },
340
+ { key: "weight", label: "Peso", type: "number", group: "shipping", min: 0, placeholder: "0.4", showInTable: false },
341
+ {
342
+ key: "weightUnit",
343
+ label: "Unidade",
344
+ type: "select",
345
+ group: "shipping",
346
+ showInTable: false,
347
+ options: [{ label: "kg", value: "kg" }, { label: "g", value: "g" }, { label: "lb", value: "lb" }]
348
+ }
349
+ ];
350
+ return {
351
+ name: "Produto",
352
+ namePlural: "Produtos",
353
+ icon: "Package",
354
+ displayField: "name",
355
+ fields,
356
+ fieldGroups: [
357
+ // imageSlot renders the scaffolding's image uploader alongside the group.
358
+ { id: "general", label: "Informa\xE7\xF5es", columns: 2, imageSlot: true },
359
+ { id: "pricing", label: "Pre\xE7o", description: 'O pre\xE7o comparativo vira o "de" riscado na vitrine.', columns: 3 },
360
+ { id: "inventory", label: "Estoque", columns: 2 },
361
+ { id: "publishing", label: "Publica\xE7\xE3o", description: "Controla se o produto aparece na loja.", columns: 2 },
362
+ { id: "shipping", label: "Envio", columns: 3 }
363
+ ],
364
+ facets: [{ field: "status", allLabel: "Todos" }],
365
+ image: getProvider ? productImage(getProvider) : void 0,
366
+ // The form's image slot sets the COVER; this tab manages the rest of the
367
+ // gallery. plg_shop_product_images has always held a collection, and until
368
+ // now nothing in the admin could add a second row to it — so the storefront
369
+ // carousel had exactly one slide for every product in the pool.
370
+ detailTabs: getProvider ? [{
371
+ id: "images",
372
+ label: "Fotos",
373
+ icon: "Image",
374
+ component: ProductImagesTab,
375
+ props: { getProvider }
376
+ }] : void 0
377
+ };
378
+ }
379
+ function money2(value) {
380
+ const n = Number(value);
381
+ if (!Number.isFinite(n)) return "R$ 0,00";
382
+ return new Intl.NumberFormat("pt-BR", { style: "currency", currency: "BRL" }).format(n);
383
+ }
384
+ function buildShopCustomerEntity() {
385
+ const fields = [
386
+ // — Contato —
387
+ {
388
+ key: "firstName",
389
+ label: "Nome",
390
+ type: "text",
391
+ required: true,
392
+ group: "contact",
393
+ placeholder: "Maria",
394
+ searchable: true,
395
+ showInTable: true,
396
+ sortable: true,
397
+ renderCell: (_v, row) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-w-0", children: [
398
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "truncate font-medium", children: [row.firstName, row.lastName].filter(Boolean).join(" ") }),
399
+ row.email && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "truncate text-xs text-muted-foreground", children: row.email })
400
+ ] })
401
+ },
402
+ { key: "lastName", label: "Sobrenome", type: "text", group: "contact", placeholder: "Silva", searchable: true, showInTable: false },
403
+ {
404
+ key: "email",
405
+ label: "E-mail",
406
+ type: "text",
407
+ group: "contact",
408
+ placeholder: "maria@exemplo.com",
409
+ searchable: true,
410
+ hint: "\xC9 por ele que o pedido \xE9 vinculado ao cliente no checkout.",
411
+ showInTable: false
412
+ },
413
+ {
414
+ key: "phone",
415
+ label: "Telefone",
416
+ type: "text",
417
+ group: "contact",
418
+ placeholder: "(21) 99999-0000",
419
+ searchable: true,
420
+ showInTable: true,
421
+ sortable: false,
422
+ renderCell: (v) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: v || "\u2014" })
423
+ },
424
+ // — Histórico (somente leitura) —
425
+ {
426
+ key: "ordersCount",
427
+ label: "Pedidos",
428
+ type: "number",
429
+ showInForm: false,
430
+ showInTable: true,
431
+ sortable: true,
432
+ renderCell: (v) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "block text-right", children: Number(v) || 0 })
433
+ },
434
+ {
435
+ key: "totalSpent",
436
+ label: "Total gasto",
437
+ type: "number",
438
+ showInForm: false,
439
+ showInTable: true,
440
+ sortable: true,
441
+ renderCell: (v) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "block text-right font-medium", children: money2(v) })
442
+ },
443
+ // — Interno —
444
+ {
445
+ key: "notes",
446
+ label: "Observa\xE7\xF5es",
447
+ type: "textarea",
448
+ group: "internal",
449
+ span: 2,
450
+ placeholder: "Prefer\xEAncias, restri\xE7\xF5es, combinados de entrega\u2026",
451
+ hint: "Vis\xEDvel s\xF3 no admin. O cliente nunca v\xEA.",
452
+ showInTable: false
453
+ }
454
+ ];
455
+ return {
456
+ name: "Cliente",
457
+ namePlural: "Clientes",
458
+ icon: "Users",
459
+ displayField: "firstName",
460
+ // `person` layout brings the shared archetype tabs — including Endereços,
461
+ // which reads the core addresses table and therefore works in every app.
462
+ layout: "person",
463
+ // Without archetypeKind the shell shows every person tab, including the
464
+ // staff-only Agenda (its visibleFor filter only runs when a kind is set).
465
+ data: { table: "plg_shop_customers", archetypeKind: "customer" },
466
+ fields,
467
+ // The SAME tab and the SAME provider beauty-saas uses. Since the shop's
468
+ // orders now live in public.orders with party_id set, the canonical provider
469
+ // already returns them — a shop-specific adapter would be a second
470
+ // implementation of "list this person's orders", which is how two apps end
471
+ // up with two different answers to the same question.
472
+ detailTabs: [{
473
+ id: "orders",
474
+ label: "Pedidos",
475
+ icon: "ShoppingBag",
476
+ aliases: ["appointments", "quotes"],
477
+ component: saas.ClientOrdersTab,
478
+ props: {
479
+ provider: saas.createClientOrdersProvider(),
480
+ currency: { code: "BRL", locale: "pt-BR" },
481
+ // The one part that legitimately differs per app: where a document
482
+ // leads. Beauty sends a booking to the agenda and an invoice to
483
+ // receivables; a shop order goes to the order screen.
484
+ navigator: {
485
+ onNavigate: (doc) => saas.adminNavigateTo(`/shop/orders/${doc.id}`)
486
+ }
487
+ }
488
+ }],
489
+ fieldGroups: [
490
+ { id: "contact", label: "Contato", columns: 2 },
491
+ { id: "internal", label: "Notas internas", columns: 1 }
492
+ ]
493
+ };
494
+ }
495
+ var STATUS_TONE2 = {
496
+ active: "bg-emerald-100 text-emerald-800",
497
+ draft: "bg-gray-100 text-gray-700",
498
+ expired: "bg-orange-100 text-orange-700",
499
+ disabled: "bg-red-100 text-red-700"
500
+ };
501
+ function buildShopDiscountEntity() {
502
+ const fields = [
503
+ // — Cupom —
504
+ {
505
+ key: "title",
506
+ label: "Nome interno",
507
+ type: "text",
508
+ required: true,
509
+ group: "coupon",
510
+ placeholder: "Bem-vindo 10%",
511
+ hint: "S\xF3 o admin v\xEA. Serve para voc\xEA identificar a campanha.",
512
+ searchable: true,
513
+ showInTable: true,
514
+ sortable: true,
515
+ renderCell: (_v, row) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-w-0", children: [
516
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "truncate font-medium", children: row.title }),
517
+ row.code && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "font-mono text-xs text-muted-foreground", children: row.code })
518
+ ] })
519
+ },
520
+ {
521
+ key: "code",
522
+ label: "C\xF3digo",
523
+ type: "text",
524
+ group: "coupon",
525
+ placeholder: "ARTORIUS10",
526
+ hint: "O que o cliente digita no carrinho. Mai\xFAsculas e min\xFAsculas n\xE3o importam.",
527
+ searchable: true,
528
+ showInTable: false
529
+ },
530
+ // — Regra —
531
+ {
532
+ key: "type",
533
+ label: "Tipo",
534
+ type: "segmented",
535
+ required: true,
536
+ group: "rule",
537
+ span: 2,
538
+ options: [
539
+ { label: "Percentual", value: "percentage", description: "Desconta uma % do subtotal." },
540
+ { label: "Valor fixo", value: "fixed_amount", description: "Desconta um valor em reais." },
541
+ { label: "Frete gr\xE1tis", value: "free_shipping", description: "Zera o frete do pedido." }
542
+ ],
543
+ showInTable: true,
544
+ sortable: false,
545
+ renderCell: (v) => {
546
+ const labels = {
547
+ percentage: "Percentual",
548
+ fixed_amount: "Valor fixo",
549
+ free_shipping: "Frete gr\xE1tis",
550
+ buy_x_get_y: "Leve X pague Y"
551
+ };
552
+ return /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: labels[String(v)] ?? String(v) });
553
+ }
554
+ },
555
+ {
556
+ key: "value",
557
+ label: "Valor",
558
+ type: "number",
559
+ group: "rule",
560
+ min: 0,
561
+ placeholder: "10",
562
+ hint: "Percentual: 10 = 10%. Valor fixo: 10 = R$ 10,00. Frete gr\xE1tis ignora este campo.",
563
+ showInTable: true,
564
+ sortable: true,
565
+ renderCell: (_v, row) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "block text-right font-medium", children: row.type === "percentage" ? `${row.value}%` : row.type === "free_shipping" ? "\u2014" : new Intl.NumberFormat("pt-BR", { style: "currency", currency: "BRL" }).format(Number(row.value) || 0) })
566
+ },
567
+ // — Limites —
568
+ {
569
+ key: "usageLimit",
570
+ label: "Limite de usos",
571
+ type: "number",
572
+ group: "limits",
573
+ min: 0,
574
+ placeholder: "sem limite",
575
+ hint: "Deixe vazio para uso ilimitado.",
576
+ showInTable: false
577
+ },
578
+ {
579
+ key: "oncePerCustomer",
580
+ label: "Uma vez por cliente",
581
+ type: "boolean",
582
+ group: "limits",
583
+ hint: "S\xF3 vale para quem se identifica no checkout \u2014 compra como visitante n\xE3o \xE9 rastreada.",
584
+ showInTable: false
585
+ },
586
+ {
587
+ key: "timesUsed",
588
+ label: "Usos",
589
+ type: "number",
590
+ showInForm: false,
591
+ showInTable: true,
592
+ sortable: true,
593
+ renderCell: (_v, row) => /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "block text-right text-muted-foreground", children: [
594
+ row.timesUsed,
595
+ row.usageLimit ? ` / ${row.usageLimit}` : ""
596
+ ] })
597
+ },
598
+ // — Vigência —
599
+ { key: "startsAt", label: "In\xEDcio", type: "date", group: "schedule", showInTable: false },
600
+ {
601
+ key: "endsAt",
602
+ label: "Fim",
603
+ type: "date",
604
+ group: "schedule",
605
+ hint: "Vazio = sem data de expira\xE7\xE3o.",
606
+ showInTable: false
607
+ },
608
+ {
609
+ key: "status",
610
+ label: "Status",
611
+ type: "segmented",
612
+ required: true,
613
+ group: "schedule",
614
+ span: 2,
615
+ options: [
616
+ { label: "Rascunho", value: "draft", description: "N\xE3o aceito no carrinho." },
617
+ { label: "Ativo", value: "active", description: "Valendo agora." },
618
+ { label: "Desativado", value: "disabled", description: "Suspenso sem apagar o hist\xF3rico." }
619
+ ],
620
+ showInTable: true,
621
+ sortable: false,
622
+ renderCell: (v) => {
623
+ const s = String(v ?? "draft");
624
+ return /* @__PURE__ */ jsxRuntime.jsx("span", { className: `inline-flex items-center rounded-full px-2 py-0.5 text-[10px] font-medium ${STATUS_TONE2[s] ?? STATUS_TONE2.draft}`, children: s });
625
+ }
626
+ }
627
+ ];
628
+ return {
629
+ name: "Desconto",
630
+ namePlural: "Descontos",
631
+ icon: "Tag",
632
+ displayField: "title",
633
+ fields,
634
+ fieldGroups: [
635
+ { id: "coupon", label: "Cupom", columns: 2 },
636
+ { id: "rule", label: "Regra", description: "O desconto \xE9 recalculado no servidor a cada pedido.", columns: 2 },
637
+ { id: "limits", label: "Limites de uso", columns: 2 },
638
+ { id: "schedule", label: "Vig\xEAncia", columns: 2 }
639
+ ],
640
+ facets: [{ field: "status", allLabel: "Todos" }]
641
+ };
642
+ }
643
+ var cep = (value) => {
644
+ const digits = String(value ?? "").replace(/\D/g, "").padStart(8, "0");
645
+ return `${digits.slice(0, 5)}-${digits.slice(5)}`;
646
+ };
647
+ function buildShopShippingZoneEntity(currency = "BRL") {
648
+ const money3 = (value) => value == null ? "\u2014" : new Intl.NumberFormat("pt-BR", { style: "currency", currency }).format(Number(value));
649
+ const fields = [
650
+ {
651
+ key: "name",
652
+ label: "Nome da zona",
653
+ type: "text",
654
+ required: true,
655
+ group: "zone",
656
+ placeholder: "Rio de Janeiro \u2014 capital",
657
+ hint: "Aparece para o cliente na hora de escolher a entrega.",
658
+ searchable: true,
659
+ showInTable: true,
660
+ sortable: true
661
+ },
662
+ {
663
+ key: "carrier",
664
+ label: "Transportadora",
665
+ type: "text",
666
+ group: "zone",
667
+ placeholder: "Correios, motoboy pr\xF3prio\u2026",
668
+ hint: "Opcional. Mostrado antes do nome da zona quando preenchido.",
669
+ showInTable: true
670
+ },
671
+ {
672
+ key: "postalFrom",
673
+ label: "CEP inicial",
674
+ type: "text",
675
+ required: true,
676
+ group: "zone",
677
+ placeholder: "20000-000",
678
+ hint: "In\xEDcio da faixa. Pode digitar com ou sem tra\xE7o.",
679
+ showInTable: true,
680
+ renderCell: (_v, row) => /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "whitespace-nowrap font-mono text-xs", children: [
681
+ cep(row.postalFrom),
682
+ " \u2192 ",
683
+ cep(row.postalTo)
684
+ ] })
685
+ },
686
+ {
687
+ key: "postalTo",
688
+ label: "CEP final",
689
+ type: "text",
690
+ required: true,
691
+ group: "zone",
692
+ placeholder: "23799-999",
693
+ hint: "Fim da faixa, inclusive.",
694
+ showInTable: false
695
+ },
696
+ {
697
+ key: "rate",
698
+ label: "Valor do frete",
699
+ type: "number",
700
+ required: true,
701
+ group: "pricing",
702
+ hint: "Cobrado nos pedidos entregues nessa faixa.",
703
+ showInTable: true,
704
+ sortable: true,
705
+ renderCell: (value) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "tabular-nums", children: money3(value) })
706
+ },
707
+ {
708
+ key: "freeAbove",
709
+ label: "Frete gr\xE1tis acima de",
710
+ type: "number",
711
+ group: "pricing",
712
+ hint: "Deixe vazio para nunca dar frete gr\xE1tis nessa zona. Vale mais que a regra geral da loja.",
713
+ showInTable: true,
714
+ renderCell: (value) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "tabular-nums", children: money3(value) })
715
+ },
716
+ {
717
+ key: "etaMinDays",
718
+ label: "Prazo m\xEDnimo (dias)",
719
+ type: "number",
720
+ group: "delivery",
721
+ hint: "0 = no mesmo dia.",
722
+ showInTable: false
723
+ },
724
+ {
725
+ key: "etaMaxDays",
726
+ label: "Prazo m\xE1ximo (dias)",
727
+ type: "number",
728
+ group: "delivery",
729
+ hint: 'O cliente v\xEA a faixa "de X a Y dias \xFAteis" na p\xE1gina do produto.',
730
+ showInTable: true,
731
+ renderCell: (_v, row) => row.etaMinDays == null && row.etaMaxDays == null ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: "\u2014" }) : /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
732
+ row.etaMinDays ?? 0,
733
+ "\u2013",
734
+ row.etaMaxDays ?? row.etaMinDays,
735
+ " dias"
736
+ ] })
737
+ },
738
+ {
739
+ key: "active",
740
+ label: "Ativa",
741
+ type: "boolean",
742
+ group: "delivery",
743
+ hint: "Desativar para de cotar essa faixa sem apagar a configura\xE7\xE3o.",
744
+ showInTable: true,
745
+ renderCell: (value) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: `rounded-full px-2 py-0.5 text-xs font-medium ${value ? "bg-emerald-100 text-emerald-800" : "bg-gray-100 text-gray-700"}`, children: value ? "Ativa" : "Inativa" })
746
+ },
747
+ {
748
+ key: "sortOrder",
749
+ label: "Ordem",
750
+ type: "number",
751
+ group: "delivery",
752
+ hint: "Desempate quando duas zonas custam o mesmo. Menor aparece primeiro.",
753
+ showInTable: false
754
+ }
755
+ ];
756
+ return {
757
+ name: "Zona de entrega",
758
+ namePlural: "Entrega",
759
+ icon: "Truck",
760
+ displayField: "name",
761
+ fields,
762
+ fieldGroups: [
763
+ {
764
+ id: "zone",
765
+ label: "Faixa de CEP",
766
+ columns: 2,
767
+ description: "Faixas podem se sobrepor: quando duas cobrem o mesmo CEP, o cliente paga a mais barata."
768
+ },
769
+ {
770
+ id: "pricing",
771
+ label: "Pre\xE7o",
772
+ columns: 2,
773
+ description: "O frete cobrado \xE9 recalculado no servidor a cada pedido, a partir daqui."
774
+ },
775
+ { id: "delivery", label: "Prazo e status", columns: 2 }
776
+ ],
777
+ facets: [{ field: "active", allLabel: "Todas" }]
778
+ };
779
+ }
780
+
781
+ // src/data/shared.ts
782
+ var resolve = (p) => typeof p === "function" ? p() : p;
783
+ function applyQuery(rows, query, facetKey) {
784
+ const prop = (row, key) => row[key];
785
+ let data = rows;
786
+ if (facetKey) {
787
+ const wanted = query.filters?.[facetKey];
788
+ if (wanted && wanted !== "all") data = data.filter((row) => prop(row, facetKey) === wanted);
789
+ }
790
+ if (query.sortBy) {
791
+ const dir = query.sortDir === "desc" ? -1 : 1;
792
+ const key = query.sortBy;
793
+ data = [...data].sort((a, b) => {
794
+ const av = prop(a, key);
795
+ const bv = prop(b, key);
796
+ if (typeof av === "number" && typeof bv === "number") return (av - bv) * dir;
797
+ return String(av ?? "").localeCompare(String(bv ?? ""), "pt-BR") * dir;
798
+ });
799
+ }
800
+ const total = data.length;
801
+ const page = query.page ?? 1;
802
+ const size = query.pageSize ?? total;
803
+ return { data: data.slice((page - 1) * size, page * size), total };
804
+ }
805
+
806
+ // src/data/productDataProvider.ts
807
+ function createShopProductDataProvider(provider) {
808
+ return {
809
+ async list(query) {
810
+ const rows = await resolve(provider).listProducts(
811
+ query.search ? { search: query.search } : void 0
812
+ );
813
+ return applyQuery(rows, query, "status");
814
+ },
815
+ async create(data) {
816
+ const input = data;
817
+ const slug = input.slug?.trim() || slugify(String(input.name ?? ""));
818
+ return resolve(provider).createProduct({
819
+ ...input,
820
+ slug
821
+ });
822
+ },
823
+ async update(id, data) {
824
+ const patch = { ...data };
825
+ if (patch.slug !== void 0 && !String(patch.slug).trim()) {
826
+ patch.slug = slugify(String(patch.name ?? ""));
827
+ }
828
+ return resolve(provider).updateProduct(id, patch);
829
+ },
830
+ async remove(id) {
831
+ await resolve(provider).deleteProduct(id);
832
+ }
833
+ };
834
+ }
835
+
836
+ // src/data/customerDataProvider.ts
837
+ function createShopCustomerDataProvider(provider) {
838
+ return {
839
+ async list(query) {
840
+ const rows = await resolve(provider).listCustomers(
841
+ query.search ? { search: query.search } : void 0
842
+ );
843
+ return applyQuery(rows, query);
844
+ },
845
+ async create(data) {
846
+ return resolve(provider).createCustomer(data);
847
+ },
848
+ async update(id, data) {
849
+ const { ordersCount: _o, totalSpent: _t, ...patch } = data;
850
+ return resolve(provider).updateCustomer(id, patch);
851
+ },
852
+ async remove(id) {
853
+ await resolve(provider).deleteCustomer(id);
854
+ }
855
+ };
856
+ }
857
+
858
+ // src/data/discountDataProvider.ts
859
+ function createShopDiscountDataProvider(provider) {
860
+ return {
861
+ async list(query) {
862
+ const rows = await resolve(provider).listDiscounts();
863
+ const term = query.search?.trim().toLowerCase();
864
+ const filtered = term ? rows.filter((d) => d.title.toLowerCase().includes(term) || (d.code ?? "").toLowerCase().includes(term)) : rows;
865
+ return applyQuery(filtered, query, "status");
866
+ },
867
+ async create(data) {
868
+ const input = data;
869
+ return resolve(provider).createDiscount({
870
+ ...input,
871
+ // Codes are matched case-insensitively server-side; store them upper so
872
+ // the admin list reads consistently.
873
+ code: input.code ? String(input.code).trim().toUpperCase() : void 0
874
+ });
875
+ },
876
+ async update(id, data) {
877
+ const { timesUsed: _used, ...patch } = data;
878
+ if (patch.code !== void 0) {
879
+ patch.code = patch.code ? String(patch.code).trim().toUpperCase() : null;
880
+ }
881
+ return resolve(provider).updateDiscount(id, patch);
882
+ },
883
+ async remove(id) {
884
+ await resolve(provider).deleteDiscount(id);
885
+ }
886
+ };
887
+ }
888
+
889
+ // src/data/shippingZoneDataProvider.ts
890
+ function createShopShippingZoneDataProvider(provider) {
891
+ const shop = () => {
892
+ const p = resolve(provider);
893
+ if (!p.listShippingZones) {
894
+ throw new Error("Este provider de loja n\xE3o suporta zonas de entrega.");
895
+ }
896
+ return p;
897
+ };
898
+ return {
899
+ async list(query) {
900
+ const rows = await shop().listShippingZones();
901
+ const term = query.search?.trim().toLowerCase();
902
+ const filtered = term ? rows.filter((z) => z.name.toLowerCase().includes(term) || (z.carrier ?? "").toLowerCase().includes(term)) : rows;
903
+ return applyQuery(filtered, query);
904
+ },
905
+ async create(data) {
906
+ return shop().createShippingZone(data);
907
+ },
908
+ async update(id, data) {
909
+ const { id: _id, tenantId: _tenant, ...patch } = data;
910
+ return shop().updateShippingZone(id, patch);
911
+ },
912
+ async remove(id) {
913
+ await shop().deleteShippingZone(id);
914
+ }
915
+ };
916
+ }
13
917
  var CURRENCIES = [
14
918
  { value: "BRL", label: "Real (R$)" },
15
919
  { value: "USD", label: "US Dollar ($)" },
@@ -22,15 +926,15 @@ var LOCALES = [
22
926
  { value: "es-ES", label: "Espa\xF1ol" }
23
927
  ];
24
928
  function ShopSettings({ currency }) {
25
- const [code, setCode] = React__default.default.useState(currency?.code ?? "BRL");
26
- const [locale, setLocale] = React__default.default.useState(currency?.locale ?? "pt-BR");
27
- const [showOutOfStock, setShowOutOfStock] = React__default.default.useState(true);
28
- const [trackInventory, setTrackInventory] = React__default.default.useState(true);
29
- const [requireSku, setRequireSku] = React__default.default.useState(false);
30
- const [guestCheckout, setGuestCheckout] = React__default.default.useState(true);
31
- const [enableDiscounts, setEnableDiscounts] = React__default.default.useState(true);
32
- const [orderEmails, setOrderEmails] = React__default.default.useState(true);
33
- const [lowStockAlerts, setLowStockAlerts] = React__default.default.useState(false);
929
+ const [code, setCode] = React2__default.default.useState(currency?.code ?? "BRL");
930
+ const [locale, setLocale] = React2__default.default.useState(currency?.locale ?? "pt-BR");
931
+ const [showOutOfStock, setShowOutOfStock] = React2__default.default.useState(true);
932
+ const [trackInventory, setTrackInventory] = React2__default.default.useState(true);
933
+ const [requireSku, setRequireSku] = React2__default.default.useState(false);
934
+ const [guestCheckout, setGuestCheckout] = React2__default.default.useState(true);
935
+ const [enableDiscounts, setEnableDiscounts] = React2__default.default.useState(true);
936
+ const [orderEmails, setOrderEmails] = React2__default.default.useState(true);
937
+ const [lowStockAlerts, setLowStockAlerts] = React2__default.default.useState(false);
34
938
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-4", children: [
35
939
  /* @__PURE__ */ jsxRuntime.jsxs(saas.SettingsGroup, { title: "Loja", description: "Moeda e formata\xE7\xE3o da vitrine.", children: [
36
940
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -127,18 +1031,55 @@ function ShopSettings({ currency }) {
127
1031
  }
128
1032
 
129
1033
  // src/index.ts
130
- var ShopPage = React__default.default.lazy(() => import('./ShopPage-PG5GKOZU.cjs').then((m) => ({ default: m.ShopPage })));
1034
+ var ShopPage = React2__default.default.lazy(() => import('./ShopPage-JF2SQBKO.cjs').then((m) => ({ default: m.ShopPage })));
131
1035
  function createShopPlugin(options) {
132
1036
  const curr = { code: "BRL", locale: "pt-BR", symbol: "R$", ...options?.currency };
133
1037
  shop.setShopTenantResolver(() => saas.useOrganizationStore.getState().currentOrg?.id);
134
- const PageComponent = () => React__default.default.createElement(
135
- React__default.default.Suspense,
136
- { fallback: null },
137
- React__default.default.createElement(ShopPage, {
138
- provider: options?.provider
139
- })
140
- );
141
- PageComponent.displayName = "ShopPageWrapper";
1038
+ saas.registerEntityPath("shop_customer", "/shop/customers");
1039
+ const pageFor = (section) => {
1040
+ const Wrapped = () => React2__default.default.createElement(
1041
+ React2__default.default.Suspense,
1042
+ { fallback: null },
1043
+ React2__default.default.createElement(
1044
+ ShopPage,
1045
+ { provider: options?.provider, section }
1046
+ )
1047
+ );
1048
+ Wrapped.displayName = section ? `ShopPage:${section}` : "ShopPageWrapper";
1049
+ return Wrapped;
1050
+ };
1051
+ const resolveProvider = () => typeof options?.provider === "function" ? options.provider() : options?.provider ?? shop.getShopProvider();
1052
+ const ProductsCrudPage = saas.createCrudPage(buildShopProductEntity(curr.code, resolveProvider), {
1053
+ dataProvider: createShopProductDataProvider(resolveProvider),
1054
+ feature: "shop"
1055
+ });
1056
+ const CustomersCrudPage = saas.createCrudPage(buildShopCustomerEntity(), {
1057
+ dataProvider: createShopCustomerDataProvider(resolveProvider),
1058
+ feature: "shop"
1059
+ });
1060
+ const DiscountsCrudPage = saas.createCrudPage(buildShopDiscountEntity(), {
1061
+ dataProvider: createShopDiscountDataProvider(resolveProvider),
1062
+ feature: "shop"
1063
+ });
1064
+ const ShippingZonesCrudPage = saas.createCrudPage(buildShopShippingZoneEntity(curr.code), {
1065
+ dataProvider: createShopShippingZoneDataProvider(resolveProvider),
1066
+ feature: "shop"
1067
+ });
1068
+ const CRUD_PAGES = {
1069
+ products: ProductsCrudPage,
1070
+ customers: CustomersCrudPage,
1071
+ discounts: DiscountsCrudPage,
1072
+ shipping: ShippingZonesCrudPage
1073
+ };
1074
+ const SECTIONS = [
1075
+ { id: "products", label: options?.sectionLabels?.products ?? "Produtos", icon: "Package" },
1076
+ { id: "orders", label: options?.sectionLabels?.orders ?? "Pedidos", icon: "ShoppingCart" },
1077
+ { id: "customers", label: options?.sectionLabels?.customers ?? "Clientes", icon: "Users" },
1078
+ { id: "discounts", label: options?.sectionLabels?.discounts ?? "Descontos", icon: "Tag" },
1079
+ { id: "shipping", label: options?.sectionLabels?.shipping ?? "Entrega", icon: "Truck" }
1080
+ ];
1081
+ const navSection = options?.navSection ?? "main";
1082
+ const navPosition = options?.navPosition ?? 6;
142
1083
  return {
143
1084
  id: "shop",
144
1085
  name: options?.navLabel ?? "Shop",
@@ -153,20 +1094,36 @@ function createShopPlugin(options) {
153
1094
  dependencies: [],
154
1095
  navigation: [
155
1096
  {
156
- section: options?.navSection ?? "main",
157
- position: options?.navPosition ?? 6,
1097
+ section: navSection,
1098
+ position: navPosition,
158
1099
  label: options?.navLabel ?? "Shop",
159
1100
  route: "/shop",
160
1101
  icon: "ShoppingBag",
1102
+ permission: { feature: "shop", action: "read" },
1103
+ // "Loja" and "Produtos" are the same screen — don't list both.
1104
+ indexChild: false
1105
+ },
1106
+ ...SECTIONS.map((s, i) => ({
1107
+ section: navSection,
1108
+ position: navPosition + i + 1,
1109
+ label: s.label,
1110
+ route: `/shop/${s.id}`,
1111
+ icon: s.icon,
161
1112
  permission: { feature: "shop", action: "read" }
162
- }
1113
+ }))
163
1114
  ],
164
1115
  routes: [
165
1116
  {
1117
+ // Kept so existing links to /shop still resolve; renders the tabbed page.
166
1118
  path: "/shop",
167
- component: PageComponent,
1119
+ component: pageFor(),
168
1120
  permission: { feature: "shop", action: "read" }
169
- }
1121
+ },
1122
+ ...SECTIONS.map((s) => ({
1123
+ path: `/shop/${s.id}`,
1124
+ component: CRUD_PAGES[s.id] ?? pageFor(s.id),
1125
+ permission: { feature: "shop", action: "read" }
1126
+ }))
170
1127
  ],
171
1128
  widgets: [],
172
1129
  aiTools: [
@@ -215,7 +1172,7 @@ function createShopPlugin(options) {
215
1172
  label: "Shop",
216
1173
  icon: "ShoppingBag",
217
1174
  component: (() => {
218
- const ShopSettingsTab = () => React__default.default.createElement(ShopSettings, { currency: curr });
1175
+ const ShopSettingsTab = () => React2__default.default.createElement(ShopSettings, { currency: curr });
219
1176
  ShopSettingsTab.displayName = "ShopSettingsTab";
220
1177
  return ShopSettingsTab;
221
1178
  })(),