@fayz-ai/plugin-shop 0.8.0 → 0.8.3
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.
- package/dist/ShopPage-ALBPZAEW.cjs +745 -0
- package/dist/ShopPage-ALBPZAEW.cjs.map +1 -0
- package/dist/ShopPage-TVNY74FF.js +743 -0
- package/dist/ShopPage-TVNY74FF.js.map +1 -0
- package/dist/ShopPage.d.ts +23 -1
- package/dist/ShopPage.d.ts.map +1 -1
- package/dist/data/customerDataProvider.d.ts +5 -0
- package/dist/data/customerDataProvider.d.ts.map +1 -0
- package/dist/data/discountDataProvider.d.ts +5 -0
- package/dist/data/discountDataProvider.d.ts.map +1 -0
- package/dist/data/productDataProvider.d.ts +6 -0
- package/dist/data/productDataProvider.d.ts.map +1 -0
- package/dist/data/shared.d.ts +12 -0
- package/dist/data/shared.d.ts.map +1 -0
- package/dist/data/shippingZoneDataProvider.d.ts +13 -0
- package/dist/data/shippingZoneDataProvider.d.ts.map +1 -0
- package/dist/index.cjs +983 -26
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +984 -27
- package/dist/index.js.map +1 -1
- package/dist/views/OrderDetailView.d.ts +10 -0
- package/dist/views/OrderDetailView.d.ts.map +1 -0
- package/dist/views/ProductImagesTab.d.ts +19 -0
- package/dist/views/ProductImagesTab.d.ts.map +1 -0
- package/dist/views/customerEntity.d.ts +4 -0
- package/dist/views/customerEntity.d.ts.map +1 -0
- package/dist/views/customerLookup.d.ts +22 -0
- package/dist/views/customerLookup.d.ts.map +1 -0
- package/dist/views/discountEntity.d.ts +4 -0
- package/dist/views/discountEntity.d.ts.map +1 -0
- package/dist/views/productEntity.d.ts +6 -0
- package/dist/views/productEntity.d.ts.map +1 -0
- package/dist/views/shippingZoneEntity.d.ts +4 -0
- package/dist/views/shippingZoneEntity.d.ts.map +1 -0
- package/package.json +5 -5
- package/src/ShopPage.tsx +156 -54
- package/src/data/customerDataProvider.ts +31 -0
- package/src/data/discountDataProvider.ts +42 -0
- package/src/data/productDataProvider.ts +54 -0
- package/src/data/shared.ts +42 -0
- package/src/data/shippingZoneDataProvider.ts +49 -0
- package/src/index.ts +101 -12
- package/src/views/OrderDetailView.tsx +579 -0
- package/src/views/ProductImagesTab.tsx +183 -0
- package/src/views/customerEntity.tsx +110 -0
- package/src/views/customerLookup.ts +31 -0
- package/src/views/discountEntity.tsx +137 -0
- package/src/views/productEntity.tsx +229 -0
- package/src/views/shippingZoneEntity.tsx +123 -0
- package/dist/ShopPage-65PPVKBD.js +0 -271
- package/dist/ShopPage-65PPVKBD.js.map +0 -1
- package/dist/ShopPage-PG5GKOZU.cjs +0 -273
- 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
|
|
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
|
|
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] =
|
|
26
|
-
const [locale, setLocale] =
|
|
27
|
-
const [showOutOfStock, setShowOutOfStock] =
|
|
28
|
-
const [trackInventory, setTrackInventory] =
|
|
29
|
-
const [requireSku, setRequireSku] =
|
|
30
|
-
const [guestCheckout, setGuestCheckout] =
|
|
31
|
-
const [enableDiscounts, setEnableDiscounts] =
|
|
32
|
-
const [orderEmails, setOrderEmails] =
|
|
33
|
-
const [lowStockAlerts, setLowStockAlerts] =
|
|
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 =
|
|
1034
|
+
var ShopPage = React2__default.default.lazy(() => import('./ShopPage-ALBPZAEW.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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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:
|
|
157
|
-
position:
|
|
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:
|
|
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 = () =>
|
|
1175
|
+
const ShopSettingsTab = () => React2__default.default.createElement(ShopSettings, { currency: curr });
|
|
219
1176
|
ShopSettingsTab.displayName = "ShopSettingsTab";
|
|
220
1177
|
return ShopSettingsTab;
|
|
221
1178
|
})(),
|