@fayz-ai/plugin-shop 0.12.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{ShopPage-VVLOH3RF.js → ShopPage-QFIM2HDU.js} +1494 -423
- package/dist/ShopPage-QFIM2HDU.js.map +1 -0
- package/dist/{chunk-HAKXHHAG.js → chunk-2VMTPRGC.js} +202 -11
- package/dist/chunk-2VMTPRGC.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1312 -298
- package/dist/index.js.map +1 -1
- package/dist/integrations/bling/data.d.ts.map +1 -1
- package/dist/integrations/mercadopago/connectorDef.d.ts +41 -0
- package/dist/integrations/mercadopago/connectorDef.d.ts.map +1 -0
- package/dist/integrations/neochef/data.d.ts.map +1 -1
- package/dist/integrations/vindi/connectorDef.d.ts +42 -29
- package/dist/integrations/vindi/connectorDef.d.ts.map +1 -1
- package/dist/lib/onboarding.d.ts +17 -0
- package/dist/lib/onboarding.d.ts.map +1 -0
- package/dist/views/CustomerModal.d.ts +33 -0
- package/dist/views/CustomerModal.d.ts.map +1 -0
- package/dist/views/NewOrderCharge.d.ts +26 -2
- package/dist/views/NewOrderCharge.d.ts.map +1 -1
- package/dist/views/NewOrderPage.d.ts +10 -1
- package/dist/views/NewOrderPage.d.ts.map +1 -1
- package/dist/views/OrderDetailView.d.ts.map +1 -1
- package/dist/views/OrdersAgentSurface.d.ts +10 -0
- package/dist/views/OrdersAgentSurface.d.ts.map +1 -0
- package/dist/views/ProductPositionsTab.d.ts +7 -0
- package/dist/views/ProductPositionsTab.d.ts.map +1 -0
- package/dist/views/ProductStockTab.d.ts +84 -0
- package/dist/views/ProductStockTab.d.ts.map +1 -0
- package/dist/views/ShopPage.d.ts.map +1 -1
- package/dist/views/StockEntryModal.d.ts +32 -0
- package/dist/views/StockEntryModal.d.ts.map +1 -0
- package/dist/views/customerEntity.d.ts.map +1 -1
- package/dist/views/discount-agent.d.ts +2 -0
- package/dist/views/discount-agent.d.ts.map +1 -0
- package/dist/views/new-order-rules.d.ts +13 -0
- package/dist/views/new-order-rules.d.ts.map +1 -1
- package/dist/views/orders-agent.d.ts +11 -0
- package/dist/views/orders-agent.d.ts.map +1 -0
- package/dist/views/product-matching.d.ts +38 -0
- package/dist/views/product-matching.d.ts.map +1 -0
- package/dist/views/productEntity.d.ts.map +1 -1
- package/dist/views/stock-agent.d.ts +82 -0
- package/dist/views/stock-agent.d.ts.map +1 -0
- package/package.json +8 -6
- package/dist/ShopPage-VVLOH3RF.js.map +0 -1
- package/dist/chunk-HAKXHHAG.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,13 +1,196 @@
|
|
|
1
|
-
import { shopCustomerLookup, resolve, invalidateProductLists, listProductsDetailed, applyQuery, errorMessage, packageMeasureLabel, PACKAGES_REGISTRY, packagesRegistry, SHIPPING_PROFILES_REGISTRY, SHIPPING_ZONES_REGISTRY, buildShopRegistries, locationSummary, LOCATIONS_REGISTRY } from './chunk-
|
|
2
|
-
import
|
|
3
|
-
import { createConnectionStore, connectionSettings, connectionStatus, settingsStatus, connectionRuns, tl, getSupabaseClientOptional, registerMessageDefaults, connectorRuntimeToken, CONNECTOR_RUNTIME_TOKEN_HEADER, resolveDataProvider } from '@fayz-ai/core';
|
|
1
|
+
import { shopCustomerLookup, buildShopCustomerEntity, resolve, invalidateProductLists, listProductsDetailed, applyQuery, errorMessage, listProductsCached, resolveTerm, candidateMeta, packageMeasureLabel, PACKAGES_REGISTRY, packagesRegistry, SHIPPING_PROFILES_REGISTRY, SHIPPING_ZONES_REGISTRY, buildShopRegistries, locationSummary, LOCATIONS_REGISTRY } from './chunk-2VMTPRGC.js';
|
|
2
|
+
import React9, { useState, useEffect, useRef, useCallback, useMemo } from 'react';
|
|
3
|
+
import { createConnectionStore, connectionSettings, connectionStatus, settingsStatus, connectionRuns, tl, getSupabaseClientOptional, registerMessageDefaults, connectorRuntimeToken, CONNECTOR_RUNTIME_TOKEN_HEADER, countByTenant, getActiveTenantId, emitDataChanged, resolveDataProvider } from '@fayz-ai/core';
|
|
4
4
|
import { getShopTenantId, setShopTenantResolver, buildShopMessageDefaults, registerShippingLabelProvider, getShopProvider, buildShopDomainEvents, axesFromOptions, planVariantMatrix, validateVariantAxes, hasOnlyDefaultVariant, MAX_VARIANT_AXES, leavesNothingSellable, productImageToMediaFile, formatVariantCombination, applyVariantMatrix } from '@fayz-ai/shop';
|
|
5
|
-
import { fayzConnectorOAuth, useOrganizationStore, registerEntityPath, registerPersonLookup, createCrudPage, adminNavigateTo,
|
|
6
|
-
import { Package, Eye, FileEdit, PackageX, UsersRound, Users, UserX, Filter, ShoppingBag, Repeat, CircleDollarSign, Tag, TicketCheck, Layers, CreditCard, ImageOff, Link2, MapPin, Music, Camera, MessageCircle, Globe, Plus, Loader2, ChevronLeft, ChevronRight, Download, RefreshCw, Trash2, Upload, ExternalLink, Check, X, AlertTriangle, Box, ChevronDown, Film, FileText, Warehouse, ArrowRight, Car, Truck, Info } from 'lucide-react';
|
|
5
|
+
import { fayzConnectorOAuth, useOrganizationStore, registerEntityPath, registerPersonLookup, createCrudPage, adminNavigateTo, useAgentSurface, askAI, AskAIButton, CrudFormPage, SettingsGroup, SelectRow, ToggleRow, useConnectors, useConnectorsPanelAvailable, openConnectorsPanel, MultiSelectRow, AddConnectorButton, AddConnectorPanel, ConnectorDetail, RegistryCrudView, TextRow, ConnectorRow, ConnectorMark, StateChip } from '@fayz-ai/admin';
|
|
6
|
+
import { Package, Eye, FileEdit, PackageX, UsersRound, Users, UserX, Filter, ShoppingBag, Repeat, CircleDollarSign, Tag, TicketCheck, Layers, CreditCard, ImageOff, Link2, MapPin, Music, Camera, MessageCircle, Globe, Plus, Loader2, WandSparkles, ChevronLeft, ChevronRight, Download, RefreshCw, Trash2, Upload, ExternalLink, Check, X, AlertTriangle, Box, ChevronDown, Film, FileText, Warehouse, ArrowRight, Car, Truck, Info } from 'lucide-react';
|
|
7
7
|
import { PageHeaderActions, PageTabs, Card, Button, Thumbnail, PageHeader, toast, ConfirmDialog, Input, Hint, Modal, ModalContent, ModalHeader, ModalTitle, ModalBody, Skeleton } from '@fayz-ai/ui';
|
|
8
8
|
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
9
|
+
import { listStockReasonCodes, resolveStockStore, createSupabaseStockStore } from '@fayz-ai/shop/inventory';
|
|
10
|
+
import { normalizeMovementType } from '@fayz-ai/plugin-inventory';
|
|
9
11
|
import { advanceWindow, createRateLimiter, sliceWindow, paginate, reconcileMirror, serializeCursor } from '@fayz-ai/core/connector-runtime';
|
|
10
12
|
|
|
13
|
+
function buildShopOnboarding() {
|
|
14
|
+
return {
|
|
15
|
+
id: "shop",
|
|
16
|
+
title: "Sua loja no ar",
|
|
17
|
+
order: 10,
|
|
18
|
+
role: "operations",
|
|
19
|
+
instructions: `Voc\xEA est\xE1 implantando uma LOJA. A pessoa n\xE3o quer aprender um sistema: ela quer vender.
|
|
20
|
+
|
|
21
|
+
- A ordem importa e \xE9 esta: ter o que vender \u2192 onde vender \u2192 como receber \u2192 como entregar \u2192 um motivo para comprar \u2192 a primeira venda. N\xE3o pule para "divulgue" antes de a loja aceitar um pedido.
|
|
22
|
+
- Importar cat\xE1logo \xE9 o passo que trava mais gente. Ofere\xE7a planilha, integra\xE7\xE3o (Bling, Nuvemshop) ou cadastrar um produto \xE0 m\xE3o para destravar \u2014 um produto j\xE1 \xE9 o suficiente para seguir.
|
|
23
|
+
- Em pagamento, pergunte COMO ela recebe hoje (Pix, cart\xE3o, maquininha) antes de falar de gateway. O nome do provedor \xE9 detalhe nosso, n\xE3o dela.
|
|
24
|
+
- Antes de dizer que a loja est\xE1 pronta, confira: tem produto ativo, tem forma de pagamento, e a vitrine est\xE1 publicada. Se faltar uma, diga qual falta.
|
|
25
|
+
- Quando estiver pronta para receber pedidos, chame o colega de growth para a primeira campanha \u2014 n\xE3o invente plano de m\xEDdia por conta pr\xF3pria.`,
|
|
26
|
+
steps: [
|
|
27
|
+
{
|
|
28
|
+
key: "catalog",
|
|
29
|
+
title: "Cadastre seu primeiro produto",
|
|
30
|
+
description: "Seu cat\xE1logo \xE9 o que a loja vende. Um produto j\xE1 destrava o resto.",
|
|
31
|
+
icon: "Package",
|
|
32
|
+
route: "/products",
|
|
33
|
+
priority: "high",
|
|
34
|
+
// O passo genérico da plataforma ("cadastre seu primeiro produto") e
|
|
35
|
+
// este são o mesmo recado — a loja é dona dele.
|
|
36
|
+
replaces: "catalog",
|
|
37
|
+
agentHint: "Pergunte onde o cat\xE1logo est\xE1 hoje (planilha, ERP, Instagram). Ofere\xE7a importar; se travar, um produto \xE0 m\xE3o j\xE1 destrava.",
|
|
38
|
+
howTo: "Produtos \u203A Adicionar. Nome, pre\xE7o e uma foto j\xE1 bastam para o produto aparecer na loja \u2014 estoque e varia\xE7\xF5es d\xE1 para completar depois. Se voc\xEA j\xE1 tem uma planilha, use Adicionar \u203A Importar: colunas de nome, pre\xE7o e estoque viram o cat\xE1logo inteiro de uma vez.",
|
|
39
|
+
celebrate: "Produto no ar \u{1F389}",
|
|
40
|
+
// A coreografia, como dado: nenhuma dessas quatro falas varia, e cada
|
|
41
|
+
// uma delas custava um turno de modelo para ser escrita de novo.
|
|
42
|
+
script: [
|
|
43
|
+
{ kind: "say", text: "Vamos come\xE7ar pelo que a loja vende. Te levo at\xE9 os Produtos." },
|
|
44
|
+
{ kind: "goto", route: "/products" },
|
|
45
|
+
{
|
|
46
|
+
kind: "say",
|
|
47
|
+
text: "Esta \xE9 a sua lista de produtos \u2014 hoje vazia. Tudo que entrar aqui aparece na vitrine."
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
// Três caminhos, e "cadastre um produto" assumia que só existia um.
|
|
51
|
+
// A pergunta é o passo: quem tem planilha não quer digitar, e quem
|
|
52
|
+
// não tem nada quer que alguém comece por ele.
|
|
53
|
+
kind: "choose",
|
|
54
|
+
id: "shop.catalog.how",
|
|
55
|
+
question: "Como voc\xEA prefere trazer seus produtos?",
|
|
56
|
+
options: [
|
|
57
|
+
{ id: "import", label: "Tenho uma planilha", meta: "Importo e mapeio as colunas com voc\xEA" },
|
|
58
|
+
{ id: "manual", label: "Vou cadastrar \xE0 m\xE3o", meta: "Te mostro onde, come\xE7ando por um" },
|
|
59
|
+
{
|
|
60
|
+
id: "suggest",
|
|
61
|
+
label: "Me sugira o cat\xE1logo",
|
|
62
|
+
meta: "Monto uma lista a partir do que j\xE1 sei do seu neg\xF3cio"
|
|
63
|
+
}
|
|
64
|
+
]
|
|
65
|
+
}
|
|
66
|
+
],
|
|
67
|
+
check: async () => await countByTenant("plg_shop_products") > 0,
|
|
68
|
+
tour: {
|
|
69
|
+
id: "shop.catalog",
|
|
70
|
+
stops: [
|
|
71
|
+
{
|
|
72
|
+
anchor: "action.new",
|
|
73
|
+
title: "\xC9 aqui que nasce um produto",
|
|
74
|
+
body: "Nome, pre\xE7o e uma foto j\xE1 bastam. Pode cadastrar agora \u2014 eu vejo quando ele entrar.",
|
|
75
|
+
route: "/products",
|
|
76
|
+
waitForUser: true
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
anchor: "action.import",
|
|
80
|
+
title: "Ou traga tudo de uma vez",
|
|
81
|
+
body: "Uma planilha com nome, pre\xE7o e estoque vira o cat\xE1logo inteiro."
|
|
82
|
+
}
|
|
83
|
+
]
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
key: "storefront",
|
|
88
|
+
title: "Configure sua loja online",
|
|
89
|
+
description: "O endere\xE7o que voc\xEA manda para o cliente \u2014 e os canais que apontam para ela.",
|
|
90
|
+
icon: "Store",
|
|
91
|
+
route: "/store",
|
|
92
|
+
priority: "high",
|
|
93
|
+
// A plataforma chama de "publique seu site"; numa loja é a vitrine, e
|
|
94
|
+
// quem sabe configurá-la é este módulo.
|
|
95
|
+
replaces: "publish",
|
|
96
|
+
agentHint: "Confira o endere\xE7o da vitrine e os canais ativos. Publicar sem frete configurado gera pedido que ningu\xE9m consegue entregar.",
|
|
97
|
+
howTo: 'Canais de venda (o item "Sua loja" na barra lateral). A Loja Online \xE9 a sua vitrine: confira o endere\xE7o, veja como est\xE1 e publique. Esse link \xE9 o que vai para a bio do Instagram e para o WhatsApp.',
|
|
98
|
+
celebrate: "Vitrine pronta \u2728",
|
|
99
|
+
script: [
|
|
100
|
+
{ kind: "say", text: "Agora a vitrine \u2014 o endere\xE7o que voc\xEA manda para o cliente." },
|
|
101
|
+
{ kind: "goto", route: "/store" },
|
|
102
|
+
{
|
|
103
|
+
kind: "ask",
|
|
104
|
+
text: "Estou na tela de canais de venda. Confira comigo o endere\xE7o da Loja Online e me diga o que falta para publicar."
|
|
105
|
+
}
|
|
106
|
+
]
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
key: "payments",
|
|
110
|
+
title: "Decida como vai receber",
|
|
111
|
+
description: "Pix, cart\xE3o ou boleto \u2014 o dinheiro precisa de um caminho.",
|
|
112
|
+
icon: "CreditCard",
|
|
113
|
+
route: "/settings/shop-payments",
|
|
114
|
+
priority: "high",
|
|
115
|
+
replaces: "payments",
|
|
116
|
+
agentHint: "Comece por como ela j\xE1 recebe hoje. Pix costuma ser o caminho mais curto para a primeira venda; cart\xE3o vem depois.",
|
|
117
|
+
howTo: "Configura\xE7\xF5es \u203A Pagamentos. Ligue as formas que voc\xEA aceita e conecte o provedor de cada uma. Para Pix voc\xEA precisa da chave ou das credenciais do provedor; sem isso o pedido entra, mas o pagamento n\xE3o \xE9 confirmado sozinho.",
|
|
118
|
+
celebrate: "Recebimento no lugar \u{1F4B3}",
|
|
119
|
+
script: [
|
|
120
|
+
{ kind: "goto", route: "/settings/shop-payments" },
|
|
121
|
+
{
|
|
122
|
+
kind: "ask",
|
|
123
|
+
text: "Estamos em Pagamentos. Pergunte como a pessoa recebe hoje (Pix, cart\xE3o, maquininha) antes de falar de provedor, e ligue a forma que ela usar."
|
|
124
|
+
}
|
|
125
|
+
]
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
key: "shipping",
|
|
129
|
+
title: "Configure a entrega",
|
|
130
|
+
description: "Zonas, prazos e quanto custa o frete.",
|
|
131
|
+
icon: "Truck",
|
|
132
|
+
route: "/settings/shop-shipping",
|
|
133
|
+
agentHint: "Pergunte at\xE9 onde entregam e como (carro pr\xF3prio, transportadora, retirada). Frete errado \xE9 o motivo n\xBA 1 de carrinho abandonado.",
|
|
134
|
+
howTo: "Configura\xE7\xF5es \u203A Entrega. Crie uma zona (por CEP ou por cidade), diga o prazo e o pre\xE7o \u2014 pode ser valor fixo para come\xE7ar. Retirada no local \xE9 uma zona com frete zero.",
|
|
135
|
+
celebrate: "Entrega configurada \u{1F69A}",
|
|
136
|
+
script: [
|
|
137
|
+
{ kind: "goto", route: "/settings/shop-shipping" },
|
|
138
|
+
{
|
|
139
|
+
kind: "ask",
|
|
140
|
+
text: "Estamos em Entrega. Pergunte at\xE9 onde a pessoa entrega e como, e monte a primeira zona junto com ela."
|
|
141
|
+
}
|
|
142
|
+
]
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
key: "discount",
|
|
146
|
+
title: "Crie um cupom de desconto",
|
|
147
|
+
description: "D\xEA um motivo para o cliente fechar a primeira compra.",
|
|
148
|
+
icon: "Tag",
|
|
149
|
+
route: "/discounts",
|
|
150
|
+
priority: "low",
|
|
151
|
+
agentHint: "Este \xE9 passo de growth: sugira um cupom de primeira compra e ajude a decidir o valor. Se houver colega de growth, passe a bola.",
|
|
152
|
+
howTo: "Descontos \u203A Adicionar. Escolha o c\xF3digo (algo curto, tipo BEMVINDO10), o tipo (percentual ou valor) e at\xE9 quando vale. O cliente digita esse c\xF3digo no checkout."
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
key: "first-order",
|
|
156
|
+
title: "Receba seu primeiro pedido",
|
|
157
|
+
description: "Fa\xE7a um pedido de teste e veja ele cair aqui dentro.",
|
|
158
|
+
icon: "ShoppingBag",
|
|
159
|
+
route: "/orders",
|
|
160
|
+
agentHint: 'Um pedido de teste \xE9 o que transforma "acho que est\xE1 no ar" em "est\xE1 no ar". Acompanhe at\xE9 ele aparecer na lista.',
|
|
161
|
+
howTo: "Abra sua vitrine como se fosse um cliente, escolha um produto e finalize a compra. O pedido aparece em Pedidos em segundos \u2014 e \xE9 assim que voc\xEA confere que pre\xE7o, frete e pagamento est\xE3o certos antes do primeiro cliente de verdade.",
|
|
162
|
+
celebrate: "Primeiro pedido na casa \u{1F6CD}\uFE0F",
|
|
163
|
+
script: [
|
|
164
|
+
{
|
|
165
|
+
kind: "say",
|
|
166
|
+
text: "\xDAltimo: um pedido de teste. Abra sua vitrine como se fosse um cliente e finalize uma compra \u2014 eu aviso aqui quando ele cair."
|
|
167
|
+
},
|
|
168
|
+
{ kind: "goto", route: "/orders" },
|
|
169
|
+
{
|
|
170
|
+
kind: "point",
|
|
171
|
+
anchor: "nav./orders",
|
|
172
|
+
title: "Todo pedido cai aqui",
|
|
173
|
+
body: "Pago, em separa\xE7\xE3o, enviado \u2014 o estado de cada venda vive nesta lista.",
|
|
174
|
+
waitForUser: true
|
|
175
|
+
}
|
|
176
|
+
],
|
|
177
|
+
check: async () => await countByTenant("plg_shop_orders") > 0,
|
|
178
|
+
tour: {
|
|
179
|
+
id: "shop.orders",
|
|
180
|
+
stops: [
|
|
181
|
+
{
|
|
182
|
+
anchor: "nav./orders",
|
|
183
|
+
title: "Todo pedido cai aqui",
|
|
184
|
+
body: "Pago, em separa\xE7\xE3o, enviado \u2014 o estado de cada venda vive nesta lista.",
|
|
185
|
+
route: "/orders"
|
|
186
|
+
}
|
|
187
|
+
]
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
]
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
11
194
|
// src/integrations/melhor-envio/labelProvider.ts
|
|
12
195
|
var MELHOR_ENVIO_CARRIER = "melhor-envio";
|
|
13
196
|
function createMelhorEnvioLabelProvider(resolve4) {
|
|
@@ -170,20 +353,20 @@ function ProductVariantsTab({ item, getProvider }) {
|
|
|
170
353
|
const product = item;
|
|
171
354
|
const shop = getProvider();
|
|
172
355
|
const supported = typeof shop.getProductVariants === "function" && typeof shop.saveProductOptions === "function" && typeof shop.createProductVariant === "function" && typeof shop.updateProductVariant === "function";
|
|
173
|
-
const [axes, setAxes] =
|
|
174
|
-
const [variants, setVariants] =
|
|
175
|
-
const [busy, setBusy] =
|
|
176
|
-
const [error, setError] =
|
|
177
|
-
const [selected, setSelected] =
|
|
178
|
-
const [dirty, setDirty] =
|
|
179
|
-
const reload =
|
|
356
|
+
const [axes, setAxes] = React9.useState([]);
|
|
357
|
+
const [variants, setVariants] = React9.useState([]);
|
|
358
|
+
const [busy, setBusy] = React9.useState(false);
|
|
359
|
+
const [error, setError] = React9.useState(null);
|
|
360
|
+
const [selected, setSelected] = React9.useState(/* @__PURE__ */ new Set());
|
|
361
|
+
const [dirty, setDirty] = React9.useState(false);
|
|
362
|
+
const reload = React9.useCallback(async () => {
|
|
180
363
|
const set = await getProvider().getProductVariants(product.id, { includeArchived: true });
|
|
181
364
|
setAxes(axesFromOptions(set.options));
|
|
182
365
|
setVariants(set.variants);
|
|
183
366
|
setSelected(/* @__PURE__ */ new Set());
|
|
184
367
|
setDirty(false);
|
|
185
368
|
}, [getProvider, product.id]);
|
|
186
|
-
|
|
369
|
+
React9.useEffect(() => {
|
|
187
370
|
if (!supported) return;
|
|
188
371
|
void reload().catch((e) => setError(message(e)));
|
|
189
372
|
}, [reload, supported]);
|
|
@@ -199,7 +382,7 @@ function ProductVariantsTab({ item, getProvider }) {
|
|
|
199
382
|
setBusy(false);
|
|
200
383
|
}
|
|
201
384
|
}
|
|
202
|
-
const plan =
|
|
385
|
+
const plan = React9.useMemo(() => planVariantMatrix(axes, variants), [axes, variants]);
|
|
203
386
|
const axisError = validateVariantAxes(axes);
|
|
204
387
|
const onlyDefault = hasOnlyDefaultVariant(variants);
|
|
205
388
|
const rows = plan.rows;
|
|
@@ -300,7 +483,7 @@ function ProductVariantsTab({ item, getProvider }) {
|
|
|
300
483
|
] });
|
|
301
484
|
}
|
|
302
485
|
function AxisEditor({ axis, index, count, disabled, onChange, onMove, onRemove }) {
|
|
303
|
-
const [draft, setDraft] =
|
|
486
|
+
const [draft, setDraft] = React9.useState("");
|
|
304
487
|
function commit(raw) {
|
|
305
488
|
const values = raw.split(",").map((v) => v.trim()).filter(Boolean);
|
|
306
489
|
if (values.length === 0) return;
|
|
@@ -447,9 +630,9 @@ function VariantRow({ row, axes, product, busy, checked, onToggle, onSave, onSto
|
|
|
447
630
|
const variant = row.variant;
|
|
448
631
|
const pending = !variant;
|
|
449
632
|
const label = formatVariantCombination(row.options, axes);
|
|
450
|
-
const commit = (
|
|
633
|
+
const commit = (field2, value, previous) => {
|
|
451
634
|
if (!variant || value === previous) return;
|
|
452
|
-
onSave(variant.id, { [
|
|
635
|
+
onSave(variant.id, { [field2]: value });
|
|
453
636
|
};
|
|
454
637
|
return /* @__PURE__ */ jsxs("tr", { className: pending ? "bg-muted/20 text-muted-foreground" : void 0, children: [
|
|
455
638
|
/* @__PURE__ */ jsx("td", { className: "px-2 py-1.5", children: /* @__PURE__ */ jsx(
|
|
@@ -573,9 +756,9 @@ function VariantRow({ row, axes, product, busy, checked, onToggle, onSave, onSto
|
|
|
573
756
|
] });
|
|
574
757
|
}
|
|
575
758
|
function BulkBar({ count, disabled, onApply, onClear }) {
|
|
576
|
-
const [price, setPrice] =
|
|
577
|
-
const [weight, setWeight] =
|
|
578
|
-
const [unit, setUnit] =
|
|
759
|
+
const [price, setPrice] = React9.useState("");
|
|
760
|
+
const [weight, setWeight] = React9.useState("");
|
|
761
|
+
const [unit, setUnit] = React9.useState("");
|
|
579
762
|
return /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2 rounded-lg border bg-muted/30 px-3 py-2 text-xs", children: [
|
|
580
763
|
/* @__PURE__ */ jsxs("span", { className: "font-medium", children: [
|
|
581
764
|
count,
|
|
@@ -654,8 +837,8 @@ function parseNumber(raw) {
|
|
|
654
837
|
return Number.isFinite(n) && n >= 0 ? n : null;
|
|
655
838
|
}
|
|
656
839
|
function NumberCell({ value, disabled, placeholder, onCommit, width = "w-20", integer }) {
|
|
657
|
-
const [draft, setDraft] =
|
|
658
|
-
|
|
840
|
+
const [draft, setDraft] = React9.useState(value == null ? "" : String(value));
|
|
841
|
+
React9.useEffect(() => {
|
|
659
842
|
setDraft(value == null ? "" : String(value));
|
|
660
843
|
}, [value]);
|
|
661
844
|
return /* @__PURE__ */ jsx(
|
|
@@ -674,8 +857,8 @@ function NumberCell({ value, disabled, placeholder, onCommit, width = "w-20", in
|
|
|
674
857
|
);
|
|
675
858
|
}
|
|
676
859
|
function TextCell({ value, disabled, placeholder, onCommit }) {
|
|
677
|
-
const [draft, setDraft] =
|
|
678
|
-
|
|
860
|
+
const [draft, setDraft] = React9.useState(value);
|
|
861
|
+
React9.useEffect(() => {
|
|
679
862
|
setDraft(value);
|
|
680
863
|
}, [value]);
|
|
681
864
|
return /* @__PURE__ */ jsx(
|
|
@@ -722,6 +905,613 @@ function describePlan(plan) {
|
|
|
722
905
|
function message(error) {
|
|
723
906
|
return error instanceof Error ? error.message : String(error);
|
|
724
907
|
}
|
|
908
|
+
var KIND_OF = {
|
|
909
|
+
// `sign: 0` = o razão aceita os dois lados (ajuste corrige para cima e para baixo).
|
|
910
|
+
entry: { kind: "receipt", sign: 1, reason: "purchase" },
|
|
911
|
+
exit: { kind: "adjustment", sign: -1, reason: "correction" },
|
|
912
|
+
adjustment: { kind: "adjustment", sign: 0, reason: "correction" },
|
|
913
|
+
loss: { kind: "loss", sign: -1, reason: "breakage" },
|
|
914
|
+
// Transferência é DOIS movimentos amarrados, com origem e destino. Metade de
|
|
915
|
+
// uma transferência é estoque sumindo de um lugar e não aparecendo no outro.
|
|
916
|
+
transfer: { kind: "transfer_out", sign: -1, reason: "transfer" }
|
|
917
|
+
};
|
|
918
|
+
function planMovement(rawType, quantity, reasonCode) {
|
|
919
|
+
const type = normalizeMovementType(rawType);
|
|
920
|
+
if (!type) {
|
|
921
|
+
return { error: "unknown_movement_type", message: "Use entrada, sa\xEDda, ajuste ou perda." };
|
|
922
|
+
}
|
|
923
|
+
if (type === "transfer") {
|
|
924
|
+
return {
|
|
925
|
+
error: "transfer_unsupported",
|
|
926
|
+
message: "Transfer\xEAncia entre locais precisa de origem e destino \u2014 ainda n\xE3o fa\xE7o por aqui."
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
const magnitude = Math.abs(Number(quantity) || 0);
|
|
930
|
+
if (magnitude === 0) return { error: "zero_quantity", message: "Movimento de zero n\xE3o \xE9 movimento." };
|
|
931
|
+
const fallback = KIND_OF[type];
|
|
932
|
+
const declared = reasonCode ? listStockReasonCodes().find((r) => r.key === reasonCode) : void 0;
|
|
933
|
+
const kind = declared?.movementKind ?? fallback.kind;
|
|
934
|
+
const sign = fallback.sign === 0 ? Number(quantity) < 0 ? -1 : 1 : fallback.sign;
|
|
935
|
+
return { kind, quantity: magnitude * sign, reasonCode: declared?.key ?? fallback.reason };
|
|
936
|
+
}
|
|
937
|
+
var WHERE_IS = {
|
|
938
|
+
id: "shop.stock.position",
|
|
939
|
+
name: "getStockPosition",
|
|
940
|
+
description: 'Answers "where is my X" for a product the merchant names in words: how many are on hand, how many are held for orders, and how that splits across locations and lots. Resolves the words itself \u2014 send the term as the merchant said it. When two products fit equally it asks with a card instead of guessing.',
|
|
941
|
+
icon: "Boxes",
|
|
942
|
+
mode: "read",
|
|
943
|
+
execution: { plane: "client" },
|
|
944
|
+
category: "Shop",
|
|
945
|
+
role: "operations",
|
|
946
|
+
parameters: {
|
|
947
|
+
type: "object",
|
|
948
|
+
properties: { term: { type: "string", description: "The product, in the merchant's words." } },
|
|
949
|
+
required: ["term"]
|
|
950
|
+
}
|
|
951
|
+
};
|
|
952
|
+
var RECORD = {
|
|
953
|
+
id: "shop.stock.record",
|
|
954
|
+
name: "recordStockMovement",
|
|
955
|
+
description: "Writes one line in the stock ledger: entrada, sa\xEDda, ajuste or perda. Takes the `variantId` from getStockPosition \u2014 never a name. Send the quantity as a plain positive number and say which movement it is; the sign belongs to the ledger, not to you. The ledger is append-only: a mistake is corrected by an opposing movement, never by editing. `occurredAt` is WHEN the goods moved, which is not when they were typed \u2014 Friday's delivery entered on Monday.",
|
|
956
|
+
icon: "ArrowDownToLine",
|
|
957
|
+
mode: "persist",
|
|
958
|
+
execution: { plane: "client" },
|
|
959
|
+
category: "Shop",
|
|
960
|
+
role: "operations",
|
|
961
|
+
permission: { feature: "shop", action: "update" },
|
|
962
|
+
parameters: {
|
|
963
|
+
type: "object",
|
|
964
|
+
properties: {
|
|
965
|
+
variantId: { type: "string", description: "From getStockPosition." },
|
|
966
|
+
productName: { type: "string", description: "So the merchant reads the card without ids." },
|
|
967
|
+
movementType: {
|
|
968
|
+
type: "string",
|
|
969
|
+
description: "entrada | sa\xEDda | ajuste | perda",
|
|
970
|
+
enum: ["entrada", "saida", "ajuste", "perda"]
|
|
971
|
+
},
|
|
972
|
+
quantity: { type: "number", description: "How many, as a positive number." },
|
|
973
|
+
reasonCode: { type: "string", description: "Reason key when the merchant gave one (purchase, breakage, expiry, theft\u2026)." },
|
|
974
|
+
locationId: { type: "string", description: "Which location. Omitted means the store default." },
|
|
975
|
+
note: { type: "string", description: "Anything the merchant said that is not a field \u2014 the invoice number, who delivered." },
|
|
976
|
+
occurredAt: { type: "string", description: "ISO date when it actually moved. Omitted means now." }
|
|
977
|
+
},
|
|
978
|
+
required: ["variantId", "productName", "movementType", "quantity"]
|
|
979
|
+
}
|
|
980
|
+
};
|
|
981
|
+
function isLedgerMissing(error) {
|
|
982
|
+
const raw = error instanceof Error ? error.message : String(error?.message ?? error);
|
|
983
|
+
return /plg_shop_stock_|\btracking\b.*does not exist/i.test(raw);
|
|
984
|
+
}
|
|
985
|
+
var LEDGER_MISSING_MESSAGE = "O controle de estoque ainda n\xE3o foi aplicado nesta loja (migration 0034). Enquanto isso o saldo vive s\xF3 no n\xFAmero do produto.";
|
|
986
|
+
function occurredAtFor(chosenDay, today) {
|
|
987
|
+
if (!chosenDay || chosenDay === today) return void 0;
|
|
988
|
+
const parsed = /* @__PURE__ */ new Date(`${chosenDay}T12:00:00`);
|
|
989
|
+
return Number.isNaN(parsed.getTime()) ? void 0 : parsed.toISOString();
|
|
990
|
+
}
|
|
991
|
+
async function commitMovement(args) {
|
|
992
|
+
const plan = planMovement(args.movementType, args.quantity, args.reasonCode);
|
|
993
|
+
if ("error" in plan) return { ok: false, ...plan };
|
|
994
|
+
const store7 = stockStore();
|
|
995
|
+
if (plan.quantity < 0) {
|
|
996
|
+
const available = await store7.getAvailable(args.variantId);
|
|
997
|
+
if (Math.abs(plan.quantity) > available) {
|
|
998
|
+
return {
|
|
999
|
+
ok: false,
|
|
1000
|
+
error: "insufficient_stock",
|
|
1001
|
+
available,
|
|
1002
|
+
message: `S\xF3 h\xE1 ${available} em estoque \u2014 a sa\xEDda de ${Math.abs(plan.quantity)} deixaria negativo.`
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
const movement = await store7.recordMovement({
|
|
1007
|
+
variantId: args.variantId,
|
|
1008
|
+
kind: plan.kind,
|
|
1009
|
+
quantity: plan.quantity,
|
|
1010
|
+
reasonCode: plan.reasonCode,
|
|
1011
|
+
...args.locationId ? { locationId: args.locationId } : {},
|
|
1012
|
+
...args.note ? { note: args.note } : {},
|
|
1013
|
+
...args.occurredAt ? { occurredAt: args.occurredAt } : {}
|
|
1014
|
+
});
|
|
1015
|
+
for (const table of [
|
|
1016
|
+
"plg_shop_products",
|
|
1017
|
+
// a Lista, e é a que o usuário estava vendo
|
|
1018
|
+
"plg_shop_product_variants",
|
|
1019
|
+
// a matriz de variantes
|
|
1020
|
+
"plg_shop_stock_balances",
|
|
1021
|
+
// as Posições
|
|
1022
|
+
"products"
|
|
1023
|
+
// o arquétipo, que as telas de Estoque leem
|
|
1024
|
+
]) {
|
|
1025
|
+
emitDataChanged({ table, op: "update" });
|
|
1026
|
+
}
|
|
1027
|
+
return {
|
|
1028
|
+
ok: true,
|
|
1029
|
+
movementId: movement.id,
|
|
1030
|
+
kind: plan.kind,
|
|
1031
|
+
quantity: plan.quantity,
|
|
1032
|
+
reasonCode: plan.reasonCode,
|
|
1033
|
+
available: await store7.getAvailable(args.variantId)
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
var text = (value) => typeof value === "string" ? value.trim() : "";
|
|
1037
|
+
var stockStore = () => resolveStockStore(getActiveTenantId() ?? "", () => createSupabaseStockStore());
|
|
1038
|
+
function summarize(balances, locations) {
|
|
1039
|
+
const nameOf = (id) => locations.find((l) => l.id === id)?.name ?? id;
|
|
1040
|
+
const total = balances.reduce(
|
|
1041
|
+
(acc, b) => ({
|
|
1042
|
+
onHand: acc.onHand + b.onHand,
|
|
1043
|
+
reserved: acc.reserved + b.reserved,
|
|
1044
|
+
available: acc.available + b.available
|
|
1045
|
+
}),
|
|
1046
|
+
{ onHand: 0, reserved: 0, available: 0 }
|
|
1047
|
+
);
|
|
1048
|
+
const byLocation = /* @__PURE__ */ new Map();
|
|
1049
|
+
for (const b of balances) {
|
|
1050
|
+
const row = byLocation.get(b.locationId) ?? { location: nameOf(b.locationId), onHand: 0, available: 0 };
|
|
1051
|
+
row.onHand += b.onHand;
|
|
1052
|
+
row.available += b.available;
|
|
1053
|
+
byLocation.set(b.locationId, row);
|
|
1054
|
+
}
|
|
1055
|
+
return { total, byLocation: [...byLocation.values()] };
|
|
1056
|
+
}
|
|
1057
|
+
function createStockAgentTools({ getProvider, onChanged }) {
|
|
1058
|
+
const failed = (error) => isLedgerMissing(error) ? { ok: false, error: "ledger_not_installed", message: LEDGER_MISSING_MESSAGE } : { ok: false, error: errorMessage(error) };
|
|
1059
|
+
const variantsOf = async (product) => {
|
|
1060
|
+
try {
|
|
1061
|
+
const set = await getProvider().getProductVariants?.(product.id);
|
|
1062
|
+
const ids = (set?.variants ?? []).map((v) => v.id);
|
|
1063
|
+
if (ids.length > 0) return ids;
|
|
1064
|
+
} catch {
|
|
1065
|
+
}
|
|
1066
|
+
return [];
|
|
1067
|
+
};
|
|
1068
|
+
return [
|
|
1069
|
+
{
|
|
1070
|
+
tool: WHERE_IS,
|
|
1071
|
+
execute: async (args) => {
|
|
1072
|
+
const term = text(args.term);
|
|
1073
|
+
if (!term) return { ok: false, error: "no_term" };
|
|
1074
|
+
try {
|
|
1075
|
+
const catalog = await listProductsCached(getProvider, { status: "active", limit: 500 });
|
|
1076
|
+
const answer = resolveTerm(term, catalog);
|
|
1077
|
+
if (!answer.resolved) {
|
|
1078
|
+
if (!answer.options?.length) return { ok: true, term, notFound: true };
|
|
1079
|
+
return {
|
|
1080
|
+
ok: true,
|
|
1081
|
+
kind: "record_choice",
|
|
1082
|
+
choices: [{
|
|
1083
|
+
id: "stock-product",
|
|
1084
|
+
question: `Qual "${term}"?`,
|
|
1085
|
+
options: answer.options.map((c) => ({ id: c.productId, label: c.name, meta: candidateMeta(c) }))
|
|
1086
|
+
}],
|
|
1087
|
+
term,
|
|
1088
|
+
needsChoice: true
|
|
1089
|
+
};
|
|
1090
|
+
}
|
|
1091
|
+
const product = catalog.find((p) => p.id === answer.resolved.productId);
|
|
1092
|
+
const variantIds = await variantsOf(product);
|
|
1093
|
+
const store7 = stockStore();
|
|
1094
|
+
const [balances, locations] = await Promise.all([
|
|
1095
|
+
store7.listBalances(variantIds.length > 0 ? { variantIds } : {}),
|
|
1096
|
+
store7.listLocations()
|
|
1097
|
+
]);
|
|
1098
|
+
const mine = variantIds.length > 0 ? balances : balances.filter(() => false);
|
|
1099
|
+
const { total, byLocation } = summarize(mine, locations);
|
|
1100
|
+
return {
|
|
1101
|
+
ok: true,
|
|
1102
|
+
product: { productId: product.id, name: product.name, sku: product.sku },
|
|
1103
|
+
// A variante é o que o lançamento precisa; sem ela o razão não tem
|
|
1104
|
+
// onde postar, e é melhor dizer isso do que gravar no lugar errado.
|
|
1105
|
+
variantId: variantIds[0] ?? null,
|
|
1106
|
+
variants: variantIds,
|
|
1107
|
+
total,
|
|
1108
|
+
byLocation,
|
|
1109
|
+
locations: locations.map((l) => ({ locationId: l.id, name: l.name, isDefault: l.isDefault }))
|
|
1110
|
+
};
|
|
1111
|
+
} catch (error) {
|
|
1112
|
+
return failed(error);
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
},
|
|
1116
|
+
{
|
|
1117
|
+
tool: RECORD,
|
|
1118
|
+
execute: async (args) => {
|
|
1119
|
+
const variantId = text(args.variantId);
|
|
1120
|
+
if (!variantId) return { ok: false, error: "no_variant", message: "Preciso da variante \u2014 pergunte a posi\xE7\xE3o primeiro." };
|
|
1121
|
+
try {
|
|
1122
|
+
const answer = await commitMovement({
|
|
1123
|
+
variantId,
|
|
1124
|
+
movementType: text(args.movementType),
|
|
1125
|
+
quantity: Number(args.quantity),
|
|
1126
|
+
reasonCode: text(args.reasonCode) || void 0,
|
|
1127
|
+
locationId: text(args.locationId) || void 0,
|
|
1128
|
+
note: text(args.note) || void 0,
|
|
1129
|
+
occurredAt: text(args.occurredAt) || void 0
|
|
1130
|
+
});
|
|
1131
|
+
if (answer.ok) onChanged?.();
|
|
1132
|
+
return answer;
|
|
1133
|
+
} catch (error) {
|
|
1134
|
+
return failed(error);
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
];
|
|
1139
|
+
}
|
|
1140
|
+
var STOCK_INSTRUCTIONS = `
|
|
1141
|
+
Esta tela \xE9 o estoque da loja. O lojista lan\xE7a o que entrou e o que saiu, e pergunta onde est\xE3o as coisas.
|
|
1142
|
+
|
|
1143
|
+
\xB7 "Cad\xEA minha costela", "quanto tem de X": \`getStockPosition\` com as palavras dele. Ela resolve o produto sozinha e devolve o saldo por local. Responda com o n\xFAmero e o local, uma linha.
|
|
1144
|
+
\xB7 Lan\xE7ar entrada ou sa\xEDda: pegue o \`variantId\` na posi\xE7\xE3o e chame \`recordStockMovement\`. Quantidade sempre positiva \u2014 o sinal \xE9 do raz\xE3o, n\xE3o seu.
|
|
1145
|
+
\xB7 A esp\xE9cie vem do que ele disse: "chegaram 20" \xE9 entrada, "quebrou 3" \xE9 perda com raz\xE3o \`breakage\`, "vencerem 2" \xE9 perda com \`expiry\`, "contei e tem 18" \xE9 ajuste.
|
|
1146
|
+
\xB7 A nota de sexta lan\xE7ada na segunda: se ele disser QUANDO chegou, mande em \`occurredAt\`. Sem isso o raz\xE3o registra hoje, e o custo cai no m\xEAs errado.
|
|
1147
|
+
\xB7 O raz\xE3o n\xE3o se edita. Errou, lan\xE7a o movimento contr\xE1rio \u2014 nunca prometa "corrigir" o lan\xE7amento.
|
|
1148
|
+
\xB7 Nunca invente produto nem variante. Termo amb\xEDguo j\xE1 virou pergunta com bot\xF5es: n\xE3o repita as op\xE7\xF5es no texto.
|
|
1149
|
+
`.trim();
|
|
1150
|
+
var stockStore2 = () => resolveStockStore(getActiveTenantId() ?? "", () => createSupabaseStockStore());
|
|
1151
|
+
var MOVEMENT_TYPES = [
|
|
1152
|
+
{ value: "entrada", label: "Entrada" },
|
|
1153
|
+
{ value: "saida", label: "Sa\xEDda" },
|
|
1154
|
+
{ value: "ajuste", label: "Ajuste" },
|
|
1155
|
+
{ value: "perda", label: "Perda" }
|
|
1156
|
+
];
|
|
1157
|
+
var KIND_FOR = {
|
|
1158
|
+
entrada: "receipt",
|
|
1159
|
+
saida: "adjustment",
|
|
1160
|
+
ajuste: "adjustment",
|
|
1161
|
+
perda: "loss"
|
|
1162
|
+
};
|
|
1163
|
+
var todayISO = () => (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
1164
|
+
var field = "mt-1 w-full rounded-md border bg-background px-2.5 py-1.5 text-sm outline-none focus:border-primary";
|
|
1165
|
+
var STOCK_INTRO = {
|
|
1166
|
+
role: "operations",
|
|
1167
|
+
persona: "Estoquista",
|
|
1168
|
+
pitch: "Diga o que entrou ou saiu que eu lan\xE7o.",
|
|
1169
|
+
description: "Lan\xE7o entrada, sa\xEDda, ajuste ou perda no raz\xE3o deste produto, e respondo quanto tem e onde. Voc\xEA confirma antes de gravar.",
|
|
1170
|
+
suggestions: [
|
|
1171
|
+
{ label: "Chegaram 20 unidades" },
|
|
1172
|
+
{ label: "Quebraram 2" }
|
|
1173
|
+
]
|
|
1174
|
+
};
|
|
1175
|
+
function ProductPositionsTab({ item, getProvider }) {
|
|
1176
|
+
const product = item;
|
|
1177
|
+
const [variants, setVariants] = useState([]);
|
|
1178
|
+
const [balances, setBalances] = useState([]);
|
|
1179
|
+
const [locations, setLocations] = useState([]);
|
|
1180
|
+
const [movements, setMovements] = useState([]);
|
|
1181
|
+
const [loading, setLoading] = useState(true);
|
|
1182
|
+
const [error, setError] = useState(null);
|
|
1183
|
+
const [variantId, setVariantId] = useState("");
|
|
1184
|
+
const [type, setType] = useState(null);
|
|
1185
|
+
const [quantity, setQuantity] = useState("");
|
|
1186
|
+
const [reasonCode, setReasonCode] = useState("");
|
|
1187
|
+
const [note, setNote] = useState("");
|
|
1188
|
+
const [noteOpen, setNoteOpen] = useState(false);
|
|
1189
|
+
const [occurredOn, setOccurredOn] = useState(() => todayISO());
|
|
1190
|
+
const [saving, setSaving] = useState(false);
|
|
1191
|
+
const quantityRef = React9.useRef(null);
|
|
1192
|
+
const load = useCallback(async () => {
|
|
1193
|
+
setLoading(true);
|
|
1194
|
+
try {
|
|
1195
|
+
const set = await getProvider().getProductVariants?.(product.id);
|
|
1196
|
+
const list = set?.variants ?? [];
|
|
1197
|
+
setVariants(list);
|
|
1198
|
+
const ids = list.map((v) => v.id);
|
|
1199
|
+
setVariantId((current) => current || ids[0] || "");
|
|
1200
|
+
const store7 = stockStore2();
|
|
1201
|
+
const [b, l] = await Promise.all([
|
|
1202
|
+
ids.length > 0 ? store7.listBalances({ variantIds: ids }) : Promise.resolve([]),
|
|
1203
|
+
store7.listLocations()
|
|
1204
|
+
]);
|
|
1205
|
+
setBalances(b);
|
|
1206
|
+
setLocations(l);
|
|
1207
|
+
setMovements(ids[0] ? await store7.listMovements({ variantId: ids[0], limit: 10 }) : []);
|
|
1208
|
+
setError(null);
|
|
1209
|
+
} catch (err) {
|
|
1210
|
+
setError(isLedgerMissing(err) ? LEDGER_MISSING_MESSAGE : errorMessage(err));
|
|
1211
|
+
} finally {
|
|
1212
|
+
setLoading(false);
|
|
1213
|
+
}
|
|
1214
|
+
}, [getProvider, product.id]);
|
|
1215
|
+
useEffect(() => {
|
|
1216
|
+
void load();
|
|
1217
|
+
}, [load]);
|
|
1218
|
+
const nameOf = useCallback(
|
|
1219
|
+
(id) => locations.find((l) => l.id === id)?.name ?? id,
|
|
1220
|
+
[locations]
|
|
1221
|
+
);
|
|
1222
|
+
const rows = useMemo(() => {
|
|
1223
|
+
const mine = variantId ? balances.filter((b) => b.variantId === variantId) : balances;
|
|
1224
|
+
return mine.map((b) => ({
|
|
1225
|
+
key: b.id,
|
|
1226
|
+
location: nameOf(b.locationId),
|
|
1227
|
+
onHand: b.onHand,
|
|
1228
|
+
reserved: b.reserved,
|
|
1229
|
+
available: b.available
|
|
1230
|
+
}));
|
|
1231
|
+
}, [balances, variantId, nameOf]);
|
|
1232
|
+
const total = useMemo(
|
|
1233
|
+
() => rows.reduce(
|
|
1234
|
+
(acc, r) => ({
|
|
1235
|
+
onHand: acc.onHand + r.onHand,
|
|
1236
|
+
reserved: acc.reserved + r.reserved,
|
|
1237
|
+
available: acc.available + r.available
|
|
1238
|
+
}),
|
|
1239
|
+
{ onHand: 0, reserved: 0, available: 0 }
|
|
1240
|
+
),
|
|
1241
|
+
[rows]
|
|
1242
|
+
);
|
|
1243
|
+
const reasons = useMemo(
|
|
1244
|
+
() => listStockReasonCodes().filter(
|
|
1245
|
+
(r) => !r.movementKind || r.movementKind === KIND_FOR[type ?? "entrada"]
|
|
1246
|
+
),
|
|
1247
|
+
[type]
|
|
1248
|
+
);
|
|
1249
|
+
useEffect(() => {
|
|
1250
|
+
setReasonCode("");
|
|
1251
|
+
}, [type]);
|
|
1252
|
+
const open = (next) => {
|
|
1253
|
+
setType(next);
|
|
1254
|
+
setQuantity("");
|
|
1255
|
+
setNote("");
|
|
1256
|
+
setNoteOpen(false);
|
|
1257
|
+
setOccurredOn(todayISO());
|
|
1258
|
+
window.setTimeout(() => quantityRef.current?.focus(), 0);
|
|
1259
|
+
};
|
|
1260
|
+
const close = () => {
|
|
1261
|
+
setType(null);
|
|
1262
|
+
setQuantity("");
|
|
1263
|
+
setNote("");
|
|
1264
|
+
setNoteOpen(false);
|
|
1265
|
+
setOccurredOn(todayISO());
|
|
1266
|
+
};
|
|
1267
|
+
const amount = Math.abs(Number(quantity) || 0);
|
|
1268
|
+
const outgoing = type === "saida" || type === "perda";
|
|
1269
|
+
const nextBalance = amount > 0 ? outgoing ? total.available - amount : total.available + amount : null;
|
|
1270
|
+
const live = React9.useRef({ product, variantId, total, getProvider, load });
|
|
1271
|
+
live.current = { product, variantId, total, getProvider, load };
|
|
1272
|
+
const contribution = useMemo(() => ({
|
|
1273
|
+
id: "shop.product.positions",
|
|
1274
|
+
pluginId: "shop",
|
|
1275
|
+
role: "operations",
|
|
1276
|
+
instructions: STOCK_INSTRUCTIONS,
|
|
1277
|
+
describe: () => {
|
|
1278
|
+
const o = live.current;
|
|
1279
|
+
return {
|
|
1280
|
+
title: `Estoque \u2014 ${o.product.name}`,
|
|
1281
|
+
record: { entity: "shop_product", id: o.product.id, label: o.product.name },
|
|
1282
|
+
state: {
|
|
1283
|
+
screen: "/products/:id#posicoes",
|
|
1284
|
+
productId: o.product.id,
|
|
1285
|
+
productName: o.product.name,
|
|
1286
|
+
variantId: o.variantId || null,
|
|
1287
|
+
onHand: o.total.onHand,
|
|
1288
|
+
reserved: o.total.reserved,
|
|
1289
|
+
available: o.total.available
|
|
1290
|
+
}
|
|
1291
|
+
};
|
|
1292
|
+
},
|
|
1293
|
+
tools: createStockAgentTools({
|
|
1294
|
+
getProvider: () => live.current.getProvider(),
|
|
1295
|
+
onChanged: () => {
|
|
1296
|
+
void live.current.load();
|
|
1297
|
+
}
|
|
1298
|
+
})
|
|
1299
|
+
}), []);
|
|
1300
|
+
useAgentSurface(contribution);
|
|
1301
|
+
const submit = async () => {
|
|
1302
|
+
if (!variantId || !type) return;
|
|
1303
|
+
const amount2 = Number(quantity);
|
|
1304
|
+
if (!amount2) {
|
|
1305
|
+
toast.error("Diga quantas unidades.");
|
|
1306
|
+
return;
|
|
1307
|
+
}
|
|
1308
|
+
setSaving(true);
|
|
1309
|
+
try {
|
|
1310
|
+
const answer = await commitMovement({
|
|
1311
|
+
variantId,
|
|
1312
|
+
movementType: type,
|
|
1313
|
+
quantity: amount2,
|
|
1314
|
+
reasonCode: reasonCode || void 0,
|
|
1315
|
+
note: note.trim() || void 0,
|
|
1316
|
+
occurredAt: occurredAtFor(occurredOn, todayISO())
|
|
1317
|
+
});
|
|
1318
|
+
if (!answer.ok) {
|
|
1319
|
+
toast.error(answer.message ?? answer.error);
|
|
1320
|
+
return;
|
|
1321
|
+
}
|
|
1322
|
+
toast.success(`Lan\xE7ado. Saldo agora: ${answer.available}.`);
|
|
1323
|
+
close();
|
|
1324
|
+
await load();
|
|
1325
|
+
} catch (err) {
|
|
1326
|
+
toast.error(errorMessage(err));
|
|
1327
|
+
} finally {
|
|
1328
|
+
setSaving(false);
|
|
1329
|
+
}
|
|
1330
|
+
};
|
|
1331
|
+
if (loading) {
|
|
1332
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
|
|
1333
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3", children: [
|
|
1334
|
+
/* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
|
|
1335
|
+
/* @__PURE__ */ jsx("div", { className: "h-4 w-24 animate-pulse rounded bg-muted" }),
|
|
1336
|
+
/* @__PURE__ */ jsx("div", { className: "h-3 w-64 animate-pulse rounded bg-muted" })
|
|
1337
|
+
] }),
|
|
1338
|
+
/* @__PURE__ */ jsx("div", { className: "h-9 w-32 animate-pulse rounded-md bg-muted" })
|
|
1339
|
+
] }),
|
|
1340
|
+
/* @__PURE__ */ jsx("ul", { className: "space-y-2", children: [0, 1].map((i) => /* @__PURE__ */ jsxs("li", { className: "flex items-center gap-3 rounded-card border bg-card p-3", children: [
|
|
1341
|
+
/* @__PURE__ */ jsx("div", { className: "h-4 flex-1 animate-pulse rounded bg-muted" }),
|
|
1342
|
+
/* @__PURE__ */ jsx("div", { className: "h-4 w-12 animate-pulse rounded bg-muted" })
|
|
1343
|
+
] }, i)) })
|
|
1344
|
+
] });
|
|
1345
|
+
}
|
|
1346
|
+
if (error) {
|
|
1347
|
+
const missing = error === LEDGER_MISSING_MESSAGE;
|
|
1348
|
+
return /* @__PURE__ */ jsx("p", { className: missing ? "rounded-lg border border-dashed px-4 py-8 text-center text-sm text-muted-foreground" : "rounded-lg border border-destructive/40 bg-destructive/5 p-4 text-sm text-destructive", children: error });
|
|
1349
|
+
}
|
|
1350
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
|
|
1351
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center justify-between gap-3", children: [
|
|
1352
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
1353
|
+
/* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold", children: "Estoque do produto" }),
|
|
1354
|
+
/* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground", children: [
|
|
1355
|
+
/* @__PURE__ */ jsx("span", { className: "tabular-nums", children: total.onHand }),
|
|
1356
|
+
" em m\xE3os \xB7",
|
|
1357
|
+
" ",
|
|
1358
|
+
/* @__PURE__ */ jsx("span", { className: "tabular-nums", children: total.reserved }),
|
|
1359
|
+
" reservado para pedidos \xB7",
|
|
1360
|
+
" ",
|
|
1361
|
+
/* @__PURE__ */ jsx("span", { className: "font-medium tabular-nums text-foreground", children: total.available }),
|
|
1362
|
+
" dispon\xEDvel. O raz\xE3o n\xE3o se edita: um erro se corrige com o movimento contr\xE1rio."
|
|
1363
|
+
] })
|
|
1364
|
+
] }),
|
|
1365
|
+
variantId && type === null && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
1366
|
+
/* @__PURE__ */ jsxs(
|
|
1367
|
+
Button,
|
|
1368
|
+
{
|
|
1369
|
+
variant: "ghost",
|
|
1370
|
+
size: "sm",
|
|
1371
|
+
onClick: () => askAI({
|
|
1372
|
+
intro: STOCK_INTRO,
|
|
1373
|
+
hint: "O que entrou ou saiu\u2026",
|
|
1374
|
+
draft: `Entrada de unidades de ${product.name}`
|
|
1375
|
+
}),
|
|
1376
|
+
children: [
|
|
1377
|
+
/* @__PURE__ */ jsx(WandSparkles, { className: "mr-1.5 h-3.5 w-3.5" }),
|
|
1378
|
+
"Com IA"
|
|
1379
|
+
]
|
|
1380
|
+
}
|
|
1381
|
+
),
|
|
1382
|
+
/* @__PURE__ */ jsxs(Button, { onClick: () => open("entrada"), children: [
|
|
1383
|
+
/* @__PURE__ */ jsx(Plus, { className: "mr-1.5 h-4 w-4" }),
|
|
1384
|
+
"Dar entrada"
|
|
1385
|
+
] })
|
|
1386
|
+
] })
|
|
1387
|
+
] }),
|
|
1388
|
+
!variantId && // Nunca desabilitar um botão sem dizer por quê. Sem variante o razão não
|
|
1389
|
+
// tem onde postar, e um "Lançar" apagado sem explicação faz o lojista
|
|
1390
|
+
// clicar três vezes e concluir que a tela está quebrada.
|
|
1391
|
+
/* @__PURE__ */ jsxs("p", { className: "rounded-lg border border-dashed px-4 py-8 text-center text-sm text-muted-foreground", children: [
|
|
1392
|
+
"Este produto ainda n\xE3o tem uma variante para o estoque apontar. Crie uma em ",
|
|
1393
|
+
/* @__PURE__ */ jsx("span", { className: "font-medium text-foreground", children: "Variantes" }),
|
|
1394
|
+
" e volte aqui."
|
|
1395
|
+
] }),
|
|
1396
|
+
variants.length > 1 && /* @__PURE__ */ jsxs("label", { className: "block max-w-xs", children: [
|
|
1397
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-muted-foreground", children: "Variante" }),
|
|
1398
|
+
/* @__PURE__ */ jsx("select", { value: variantId, onChange: (e) => setVariantId(e.target.value), className: field, children: variants.map((v) => /* @__PURE__ */ jsx("option", { value: v.id, children: Object.values(v.options ?? {}).join(" \xB7 ") || v.sku || v.id.slice(0, 8) }, v.id)) })
|
|
1399
|
+
] }),
|
|
1400
|
+
type !== null && /* @__PURE__ */ jsxs("div", { className: "rounded-card border bg-card p-4 motion-safe:animate-zoom-in", children: [
|
|
1401
|
+
/* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1", children: MOVEMENT_TYPES.map((t) => /* @__PURE__ */ jsx(
|
|
1402
|
+
"button",
|
|
1403
|
+
{
|
|
1404
|
+
type: "button",
|
|
1405
|
+
onClick: () => setType(t.value),
|
|
1406
|
+
className: `fz-tap rounded-button px-3 py-1.5 text-xs font-medium transition-colors ${type === t.value ? "bg-primary text-primary-foreground" : "text-muted-foreground hover:bg-muted hover:text-foreground"}`,
|
|
1407
|
+
children: t.label
|
|
1408
|
+
},
|
|
1409
|
+
t.value
|
|
1410
|
+
)) }),
|
|
1411
|
+
/* @__PURE__ */ jsxs("div", { className: "mt-3 flex flex-wrap items-end gap-4", children: [
|
|
1412
|
+
/* @__PURE__ */ jsxs("label", { className: "block", children: [
|
|
1413
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-muted-foreground", children: "Quantas?" }),
|
|
1414
|
+
/* @__PURE__ */ jsx(
|
|
1415
|
+
"input",
|
|
1416
|
+
{
|
|
1417
|
+
ref: quantityRef,
|
|
1418
|
+
type: "number",
|
|
1419
|
+
inputMode: "numeric",
|
|
1420
|
+
min: 1,
|
|
1421
|
+
value: quantity,
|
|
1422
|
+
onChange: (e) => setQuantity(e.target.value),
|
|
1423
|
+
onKeyDown: (e) => {
|
|
1424
|
+
if (e.key === "Enter") {
|
|
1425
|
+
e.preventDefault();
|
|
1426
|
+
void submit();
|
|
1427
|
+
}
|
|
1428
|
+
if (e.key === "Escape") {
|
|
1429
|
+
e.preventDefault();
|
|
1430
|
+
close();
|
|
1431
|
+
}
|
|
1432
|
+
},
|
|
1433
|
+
className: "mt-1 h-12 w-28 rounded-md border bg-background px-3 text-2xl font-semibold tabular-nums outline-none focus:border-primary"
|
|
1434
|
+
}
|
|
1435
|
+
)
|
|
1436
|
+
] }),
|
|
1437
|
+
/* @__PURE__ */ jsx("p", { "aria-live": "polite", className: "pb-2 text-sm text-muted-foreground", children: nextBalance === null ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1438
|
+
"Fica em ",
|
|
1439
|
+
/* @__PURE__ */ jsx("span", { className: "font-medium tabular-nums text-foreground", children: total.available })
|
|
1440
|
+
] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1441
|
+
/* @__PURE__ */ jsx("span", { className: "tabular-nums", children: total.available }),
|
|
1442
|
+
/* @__PURE__ */ jsx("span", { className: "mx-1.5", children: "\u2192" }),
|
|
1443
|
+
/* @__PURE__ */ jsx("span", { className: `text-base font-semibold tabular-nums ${nextBalance < 0 ? "text-destructive" : "text-foreground"}`, children: nextBalance }),
|
|
1444
|
+
nextBalance < 0 && /* @__PURE__ */ jsx("span", { className: "ml-2 text-destructive", children: "n\xE3o h\xE1 tudo isso" })
|
|
1445
|
+
] }) }),
|
|
1446
|
+
/* @__PURE__ */ jsx("span", { className: "flex-1" }),
|
|
1447
|
+
/* @__PURE__ */ jsxs("label", { className: "block", children: [
|
|
1448
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-muted-foreground", children: "Motivo" }),
|
|
1449
|
+
/* @__PURE__ */ jsxs("select", { value: reasonCode, onChange: (e) => setReasonCode(e.target.value), className: field, children: [
|
|
1450
|
+
/* @__PURE__ */ jsx("option", { value: "", children: "Padr\xE3o" }),
|
|
1451
|
+
reasons.map((r) => /* @__PURE__ */ jsx("option", { value: r.key, children: r.label }, r.key))
|
|
1452
|
+
] })
|
|
1453
|
+
] }),
|
|
1454
|
+
/* @__PURE__ */ jsxs("label", { className: "block", children: [
|
|
1455
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-muted-foreground", children: type === "entrada" ? "Chegou em" : "Quando" }),
|
|
1456
|
+
/* @__PURE__ */ jsx(
|
|
1457
|
+
"input",
|
|
1458
|
+
{
|
|
1459
|
+
type: "date",
|
|
1460
|
+
value: occurredOn,
|
|
1461
|
+
max: todayISO(),
|
|
1462
|
+
onChange: (e) => setOccurredOn(e.target.value || todayISO()),
|
|
1463
|
+
className: field
|
|
1464
|
+
}
|
|
1465
|
+
)
|
|
1466
|
+
] })
|
|
1467
|
+
] }),
|
|
1468
|
+
noteOpen ? /* @__PURE__ */ jsxs("label", { className: "mt-3 block", children: [
|
|
1469
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-muted-foreground", children: "Observa\xE7\xE3o" }),
|
|
1470
|
+
/* @__PURE__ */ jsx(
|
|
1471
|
+
"input",
|
|
1472
|
+
{
|
|
1473
|
+
autoFocus: true,
|
|
1474
|
+
value: note,
|
|
1475
|
+
onChange: (e) => setNote(e.target.value),
|
|
1476
|
+
className: field,
|
|
1477
|
+
placeholder: "Nota fiscal, quem entregou\u2026"
|
|
1478
|
+
}
|
|
1479
|
+
)
|
|
1480
|
+
] }) : /* @__PURE__ */ jsx(
|
|
1481
|
+
"button",
|
|
1482
|
+
{
|
|
1483
|
+
type: "button",
|
|
1484
|
+
onClick: () => setNoteOpen(true),
|
|
1485
|
+
className: "mt-2 text-xs font-medium text-muted-foreground underline-offset-2 hover:text-foreground hover:underline",
|
|
1486
|
+
children: "+ observa\xE7\xE3o"
|
|
1487
|
+
}
|
|
1488
|
+
),
|
|
1489
|
+
/* @__PURE__ */ jsxs("div", { className: "mt-4 flex items-center gap-2", children: [
|
|
1490
|
+
/* @__PURE__ */ jsx(Button, { onClick: () => void submit(), disabled: saving || amount === 0, children: saving ? "Lan\xE7ando\u2026" : "Lan\xE7ar" }),
|
|
1491
|
+
/* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", onClick: close, disabled: saving, children: "Cancelar" }),
|
|
1492
|
+
/* @__PURE__ */ jsx("span", { className: "flex-1" }),
|
|
1493
|
+
amount === 0 && // O motivo de o botão estar apagado, ao lado dele.
|
|
1494
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: "Diga quantas unidades" })
|
|
1495
|
+
] })
|
|
1496
|
+
] }),
|
|
1497
|
+
variantId && (rows.length === 0 ? /* @__PURE__ */ jsx("p", { className: "rounded-lg border border-dashed px-4 py-8 text-center text-sm text-muted-foreground", children: "Nenhuma posi\xE7\xE3o ainda \u2014 a primeira entrada abre o saldo." }) : /* @__PURE__ */ jsx("ul", { className: "space-y-2", children: rows.map((r) => /* @__PURE__ */ jsxs("li", { className: "flex items-center gap-3 rounded-card border bg-card p-3", children: [
|
|
1498
|
+
/* @__PURE__ */ jsx("span", { className: "flex-1 text-sm font-medium", children: r.location }),
|
|
1499
|
+
/* @__PURE__ */ jsxs("span", { className: "text-xs text-muted-foreground", children: [
|
|
1500
|
+
r.reserved,
|
|
1501
|
+
" reservado"
|
|
1502
|
+
] }),
|
|
1503
|
+
/* @__PURE__ */ jsx("span", { className: "w-16 text-right text-sm font-semibold tabular-nums", children: r.available })
|
|
1504
|
+
] }, r.key)) })),
|
|
1505
|
+
movements.length > 0 && /* @__PURE__ */ jsxs("section", { className: "space-y-2 pt-2", children: [
|
|
1506
|
+
/* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold", children: "\xDAltimos movimentos" }),
|
|
1507
|
+
/* @__PURE__ */ jsx("ul", { className: "space-y-2", children: movements.map((m) => /* @__PURE__ */ jsxs("li", { className: "flex items-center gap-3 rounded-card border bg-card px-3 py-2", children: [
|
|
1508
|
+
/* @__PURE__ */ jsx("span", { className: `w-14 text-sm font-semibold tabular-nums ${m.quantity < 0 ? "text-destructive" : "text-emerald-700"}`, children: m.quantity > 0 ? `+${m.quantity}` : m.quantity }),
|
|
1509
|
+
/* @__PURE__ */ jsx("span", { className: "flex-1 text-sm", children: m.reasonCode ?? m.kind }),
|
|
1510
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: new Date(m.occurredAt ?? m.createdAt).toLocaleDateString("pt-BR") })
|
|
1511
|
+
] }, m.id)) })
|
|
1512
|
+
] })
|
|
1513
|
+
] });
|
|
1514
|
+
}
|
|
725
1515
|
|
|
726
1516
|
// src/data/registry-routes.ts
|
|
727
1517
|
var SHIPPING_SETTINGS_TAB = "shop-shipping";
|
|
@@ -743,20 +1533,20 @@ function shippingRegistryIdFromHash(hash) {
|
|
|
743
1533
|
}
|
|
744
1534
|
var PACKAGES_ROUTE = shippingRegistryPath(PACKAGES_REGISTRY);
|
|
745
1535
|
function PackageSelect({ value, onChange, getProvider }) {
|
|
746
|
-
const shop =
|
|
747
|
-
const [rows, setRows] =
|
|
748
|
-
const [open, setOpen] =
|
|
749
|
-
const [creating, setCreating] =
|
|
750
|
-
const ref =
|
|
751
|
-
const load =
|
|
1536
|
+
const shop = React9.useMemo(() => getProvider?.() ?? getShopProvider(), [getProvider]);
|
|
1537
|
+
const [rows, setRows] = React9.useState(null);
|
|
1538
|
+
const [open, setOpen] = React9.useState(false);
|
|
1539
|
+
const [creating, setCreating] = React9.useState(false);
|
|
1540
|
+
const ref = React9.useRef(null);
|
|
1541
|
+
const load = React9.useCallback(() => {
|
|
752
1542
|
if (typeof shop.listShippingPackages !== "function") {
|
|
753
1543
|
setRows([]);
|
|
754
1544
|
return;
|
|
755
1545
|
}
|
|
756
1546
|
void shop.listShippingPackages().then(setRows).catch(() => setRows([]));
|
|
757
1547
|
}, [shop]);
|
|
758
|
-
|
|
759
|
-
|
|
1548
|
+
React9.useEffect(load, [load]);
|
|
1549
|
+
React9.useEffect(() => {
|
|
760
1550
|
if (!open) return;
|
|
761
1551
|
const away = (event) => {
|
|
762
1552
|
if (ref.current && !ref.current.contains(event.target)) setOpen(false);
|
|
@@ -851,8 +1641,8 @@ function PackageSelect({ value, onChange, getProvider }) {
|
|
|
851
1641
|
] });
|
|
852
1642
|
}
|
|
853
1643
|
function NewPackageModal({ onClose, onCreated }) {
|
|
854
|
-
const [error, setError] =
|
|
855
|
-
const provider =
|
|
1644
|
+
const [error, setError] = React9.useState(null);
|
|
1645
|
+
const provider = React9.useMemo(
|
|
856
1646
|
() => resolveDataProvider(packagesRegistry.entity),
|
|
857
1647
|
[]
|
|
858
1648
|
);
|
|
@@ -898,9 +1688,9 @@ function Option({ label, hint, active, onClick }) {
|
|
|
898
1688
|
);
|
|
899
1689
|
}
|
|
900
1690
|
function ShippingProfileSelect({ value, onChange, getProvider }) {
|
|
901
|
-
const shop =
|
|
902
|
-
const [rows, setRows] =
|
|
903
|
-
|
|
1691
|
+
const shop = React9.useMemo(() => getProvider?.() ?? getShopProvider(), [getProvider]);
|
|
1692
|
+
const [rows, setRows] = React9.useState(null);
|
|
1693
|
+
React9.useEffect(() => {
|
|
904
1694
|
let cancelled = false;
|
|
905
1695
|
if (typeof shop.listShippingProfiles !== "function") {
|
|
906
1696
|
setRows([]);
|
|
@@ -1077,14 +1867,27 @@ function buildShopProductEntity(currencyCode = "BRL", getProvider) {
|
|
|
1077
1867
|
searchable: true,
|
|
1078
1868
|
showInTable: false
|
|
1079
1869
|
},
|
|
1870
|
+
// Não se DIGITA saldo.
|
|
1871
|
+
//
|
|
1872
|
+
// Este campo era um número editável, e um número editável convida a
|
|
1873
|
+
// resposta errada: alguém conta a prateleira, acha 18, escreve 18, e o
|
|
1874
|
+
// razão perde POR QUE mudou — se chegaram seis ou se sumiram quatro. É
|
|
1875
|
+
// exatamente o que um razão existe para impedir, e a 0036 já tratava a
|
|
1876
|
+
// escrita direta aqui como um ajuste às cegas para não perder o movimento.
|
|
1877
|
+
//
|
|
1878
|
+
// Vira reflexo: mostra o saldo, e o ato acontece em Posições, onde ele tem
|
|
1879
|
+
// espécie, motivo e data.
|
|
1080
1880
|
{
|
|
1081
1881
|
key: "inventoryCount",
|
|
1082
1882
|
label: "Estoque",
|
|
1083
1883
|
type: "number",
|
|
1084
1884
|
group: "inventory",
|
|
1085
1885
|
min: 0,
|
|
1086
|
-
|
|
1087
|
-
|
|
1886
|
+
// `showInForm: false` e não um campo desabilitado: some do formulário de
|
|
1887
|
+
// edição, continua na tabela e na ficha. Mostra-se o saldo; digita-se o
|
|
1888
|
+
// movimento.
|
|
1889
|
+
showInForm: false,
|
|
1890
|
+
hint: "Saldo do raz\xE3o. Entrada, sa\xEDda e perda se lan\xE7am na aba Posi\xE7\xF5es.",
|
|
1088
1891
|
showInTable: true,
|
|
1089
1892
|
sortable: true,
|
|
1090
1893
|
renderCell: (v) => {
|
|
@@ -1182,7 +1985,7 @@ function buildShopProductEntity(currencyCode = "BRL", getProvider) {
|
|
|
1182
1985
|
group: "shipping",
|
|
1183
1986
|
min: 0,
|
|
1184
1987
|
placeholder: "0,4",
|
|
1185
|
-
hint: "Do produto embalado.
|
|
1988
|
+
hint: "Do produto, j\xE1 embalado. As medidas v\xEAm da embalagem escolhida acima.",
|
|
1186
1989
|
showInTable: false,
|
|
1187
1990
|
visibleWhen: (values) => values.isPhysical !== false
|
|
1188
1991
|
},
|
|
@@ -1199,9 +2002,19 @@ function buildShopProductEntity(currencyCode = "BRL", getProvider) {
|
|
|
1199
2002
|
// Em centímetros, sem seletor de unidade: a coluna é `*_cm` e a caixa da
|
|
1200
2003
|
// loja também. Duas unidades para a mesma medida é como o peso passou
|
|
1201
2004
|
// quatro anos sendo lido errado.
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
2005
|
+
// A medida é da EMBALAGEM, não do produto.
|
|
2006
|
+
//
|
|
2007
|
+
// O seletor logo acima já associa o produto a uma embalagem cadastrada da
|
|
2008
|
+
// loja (0047) — e é lá que altura, largura e comprimento vivem. Repetir os
|
|
2009
|
+
// três aqui pedia a mesma informação duas vezes por caminhos diferentes, e
|
|
2010
|
+
// as duas divergiam no dia em que alguém trocasse a caixa: a cotação lê a
|
|
2011
|
+
// embalagem, o formulário mostrava outro número.
|
|
2012
|
+
//
|
|
2013
|
+
// As colunas continuam existindo — um catálogo anterior à 0047 tem produto
|
|
2014
|
+
// medido e sem embalagem, e a cotação ainda os lê. O que sai é a pergunta.
|
|
2015
|
+
{ key: "heightCm", label: "Altura (cm)", type: "number", group: "shipping", min: 0, showInForm: false, showInTable: false },
|
|
2016
|
+
{ key: "widthCm", label: "Largura (cm)", type: "number", group: "shipping", min: 0, showInForm: false, showInTable: false },
|
|
2017
|
+
{ key: "lengthCm", label: "Comprimento (cm)", type: "number", group: "shipping", min: 0, showInForm: false, showInTable: false },
|
|
1205
2018
|
{
|
|
1206
2019
|
// Declarado aqui para poder viajar — para a transportadora, para o motoboy
|
|
1207
2020
|
// ou para quem embala. O que a loja FAZ com um carrinho refrigerado é uma
|
|
@@ -1228,6 +2041,17 @@ function buildShopProductEntity(currencyCode = "BRL", getProvider) {
|
|
|
1228
2041
|
namePlural: "Produtos",
|
|
1229
2042
|
icon: "Package",
|
|
1230
2043
|
displayField: "name",
|
|
2044
|
+
// Qual tabela esta tela É. Sem isto, `deriveEntityKey` cai no rótulo — e a
|
|
2045
|
+
// chave da entidade vira `produto`, que muda com uma tradução. Quem avisa
|
|
2046
|
+
// "escrevi aqui" nomeia a TABELA (é o que emitDataChanged invalida no cache),
|
|
2047
|
+
// então sem esta linha o aviso não encontra a lista: o barramento casa
|
|
2048
|
+
// tabela-com-tabela, e o lado do assinante estava vazio.
|
|
2049
|
+
//
|
|
2050
|
+
// Não muda o caminho do dado: o índice do plugin passa um
|
|
2051
|
+
// `dataProvider: createShopProductDataProvider(...)` explícito, que vence o
|
|
2052
|
+
// provider genérico que `data.table` acionaria — mesma forma da entidade de
|
|
2053
|
+
// cliente logo ao lado.
|
|
2054
|
+
data: { table: "plg_shop_products" },
|
|
1231
2055
|
fields,
|
|
1232
2056
|
fieldGroups: [
|
|
1233
2057
|
// imageSlot renders the scaffolding's image uploader alongside the group.
|
|
@@ -1238,7 +2062,7 @@ function buildShopProductEntity(currencyCode = "BRL", getProvider) {
|
|
|
1238
2062
|
{
|
|
1239
2063
|
id: "shipping",
|
|
1240
2064
|
label: "Envio",
|
|
1241
|
-
description: "
|
|
2065
|
+
description: "Em que caixa este produto sai e quanto ele pesa. A medida \xE9 da embalagem; o peso \xE9 do produto. Sem embalagem escolhida a transportadora \xE9 consultada com a caixa padr\xE3o da loja, e o frete cotado pode n\xE3o ser o frete pago.",
|
|
1242
2066
|
columns: 3
|
|
1243
2067
|
}
|
|
1244
2068
|
],
|
|
@@ -1262,139 +2086,21 @@ function buildShopProductEntity(currencyCode = "BRL", getProvider) {
|
|
|
1262
2086
|
icon: "Layers",
|
|
1263
2087
|
component: ProductVariantsTab,
|
|
1264
2088
|
props: { getProvider }
|
|
2089
|
+
},
|
|
2090
|
+
// Separada de Variantes de propósito: aquela edita O QUE se vende,
|
|
2091
|
+
// esta responde ONDE estão as unidades — e a ficha só sabia dizer um
|
|
2092
|
+
// número só, que não distingue a loja do depósito nem o que já está
|
|
2093
|
+
// prometido a um pedido.
|
|
2094
|
+
{
|
|
2095
|
+
id: "positions",
|
|
2096
|
+
label: "Posi\xE7\xF5es",
|
|
2097
|
+
icon: "Boxes",
|
|
2098
|
+
component: ProductPositionsTab,
|
|
2099
|
+
props: { getProvider }
|
|
1265
2100
|
}
|
|
1266
2101
|
] : void 0
|
|
1267
2102
|
};
|
|
1268
2103
|
}
|
|
1269
|
-
function money2(value) {
|
|
1270
|
-
const n = Number(value);
|
|
1271
|
-
if (!Number.isFinite(n)) return "R$ 0,00";
|
|
1272
|
-
return new Intl.NumberFormat("pt-BR", { style: "currency", currency: "BRL" }).format(n);
|
|
1273
|
-
}
|
|
1274
|
-
function buildShopCustomerEntity() {
|
|
1275
|
-
const fields = [
|
|
1276
|
-
// — Contato —
|
|
1277
|
-
{
|
|
1278
|
-
key: "firstName",
|
|
1279
|
-
label: "Nome",
|
|
1280
|
-
type: "text",
|
|
1281
|
-
required: true,
|
|
1282
|
-
group: "contact",
|
|
1283
|
-
placeholder: "Maria",
|
|
1284
|
-
searchable: true,
|
|
1285
|
-
showInTable: true,
|
|
1286
|
-
sortable: true,
|
|
1287
|
-
renderCell: (_v, row) => /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
|
|
1288
|
-
/* @__PURE__ */ jsx("p", { className: "truncate font-medium", children: [row.firstName, row.lastName].filter(Boolean).join(" ") }),
|
|
1289
|
-
row.email && /* @__PURE__ */ jsx("p", { className: "truncate text-xs text-muted-foreground", children: row.email })
|
|
1290
|
-
] })
|
|
1291
|
-
},
|
|
1292
|
-
{ key: "lastName", label: "Sobrenome", type: "text", group: "contact", placeholder: "Silva", searchable: true, showInTable: false },
|
|
1293
|
-
{
|
|
1294
|
-
key: "email",
|
|
1295
|
-
label: "E-mail",
|
|
1296
|
-
type: "text",
|
|
1297
|
-
group: "contact",
|
|
1298
|
-
placeholder: "maria@exemplo.com",
|
|
1299
|
-
searchable: true,
|
|
1300
|
-
hint: "\xC9 por ele que o pedido \xE9 vinculado ao cliente no checkout.",
|
|
1301
|
-
showInTable: false
|
|
1302
|
-
},
|
|
1303
|
-
{
|
|
1304
|
-
key: "phone",
|
|
1305
|
-
label: "Telefone",
|
|
1306
|
-
type: "text",
|
|
1307
|
-
group: "contact",
|
|
1308
|
-
placeholder: "(21) 99999-0000",
|
|
1309
|
-
searchable: true,
|
|
1310
|
-
showInTable: true,
|
|
1311
|
-
sortable: false,
|
|
1312
|
-
renderCell: (v) => /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: v || "\u2014" })
|
|
1313
|
-
},
|
|
1314
|
-
// The column, the type and the provider all carried `document` already; the
|
|
1315
|
-
// form was the only place it could not be typed. Which meant a store could
|
|
1316
|
-
// see "cliente sem CPF/CNPJ" on every Bling run and have nowhere to fix it.
|
|
1317
|
-
{
|
|
1318
|
-
key: "document",
|
|
1319
|
-
label: "CPF / CNPJ",
|
|
1320
|
-
type: "text",
|
|
1321
|
-
group: "contact",
|
|
1322
|
-
placeholder: "000.000.000-00",
|
|
1323
|
-
searchable: true,
|
|
1324
|
-
hint: "Necess\xE1rio para emitir nota \u2014 sem ele o cliente n\xE3o sobe para o ERP.",
|
|
1325
|
-
showInTable: false
|
|
1326
|
-
},
|
|
1327
|
-
// — Histórico (somente leitura) —
|
|
1328
|
-
{
|
|
1329
|
-
key: "ordersCount",
|
|
1330
|
-
label: "Pedidos",
|
|
1331
|
-
type: "number",
|
|
1332
|
-
showInForm: false,
|
|
1333
|
-
showInTable: true,
|
|
1334
|
-
sortable: true,
|
|
1335
|
-
renderCell: (v) => /* @__PURE__ */ jsx("span", { className: "block text-right", children: Number(v) || 0 })
|
|
1336
|
-
},
|
|
1337
|
-
{
|
|
1338
|
-
key: "totalSpent",
|
|
1339
|
-
label: "Total gasto",
|
|
1340
|
-
type: "number",
|
|
1341
|
-
showInForm: false,
|
|
1342
|
-
showInTable: true,
|
|
1343
|
-
sortable: true,
|
|
1344
|
-
renderCell: (v) => /* @__PURE__ */ jsx("span", { className: "block text-right font-medium", children: money2(v) })
|
|
1345
|
-
},
|
|
1346
|
-
// — Interno —
|
|
1347
|
-
{
|
|
1348
|
-
key: "notes",
|
|
1349
|
-
label: "Observa\xE7\xF5es",
|
|
1350
|
-
type: "textarea",
|
|
1351
|
-
group: "internal",
|
|
1352
|
-
span: 2,
|
|
1353
|
-
placeholder: "Prefer\xEAncias, restri\xE7\xF5es, combinados de entrega\u2026",
|
|
1354
|
-
hint: "Vis\xEDvel s\xF3 no admin. O cliente nunca v\xEA.",
|
|
1355
|
-
showInTable: false
|
|
1356
|
-
}
|
|
1357
|
-
];
|
|
1358
|
-
return {
|
|
1359
|
-
name: "Cliente",
|
|
1360
|
-
namePlural: "Clientes",
|
|
1361
|
-
icon: "Users",
|
|
1362
|
-
displayField: "firstName",
|
|
1363
|
-
// `person` layout brings the shared archetype tabs — including Endereços,
|
|
1364
|
-
// which reads the core addresses table and therefore works in every app.
|
|
1365
|
-
layout: "person",
|
|
1366
|
-
// Without archetypeKind the shell shows every person tab, including the
|
|
1367
|
-
// staff-only Agenda (its visibleFor filter only runs when a kind is set).
|
|
1368
|
-
data: { table: "plg_shop_customers", archetypeKind: "customer" },
|
|
1369
|
-
fields,
|
|
1370
|
-
// The SAME tab and the SAME provider beauty-saas uses. Since the shop's
|
|
1371
|
-
// orders now live in public.orders with party_id set, the canonical provider
|
|
1372
|
-
// already returns them — a shop-specific adapter would be a second
|
|
1373
|
-
// implementation of "list this person's orders", which is how two apps end
|
|
1374
|
-
// up with two different answers to the same question.
|
|
1375
|
-
detailTabs: [{
|
|
1376
|
-
id: "orders",
|
|
1377
|
-
label: "Pedidos",
|
|
1378
|
-
icon: "ShoppingBag",
|
|
1379
|
-
aliases: ["appointments", "quotes"],
|
|
1380
|
-
component: ClientOrdersTab,
|
|
1381
|
-
props: {
|
|
1382
|
-
provider: createClientOrdersProvider(),
|
|
1383
|
-
currency: { code: "BRL", locale: "pt-BR" },
|
|
1384
|
-
// The one part that legitimately differs per app: where a document
|
|
1385
|
-
// leads. Beauty sends a booking to the agenda and an invoice to
|
|
1386
|
-
// receivables; a shop order goes to the order screen.
|
|
1387
|
-
navigator: {
|
|
1388
|
-
onNavigate: (doc) => adminNavigateTo(`/shop/orders/${doc.id}`)
|
|
1389
|
-
}
|
|
1390
|
-
}
|
|
1391
|
-
}],
|
|
1392
|
-
fieldGroups: [
|
|
1393
|
-
{ id: "contact", label: "Contato", columns: 2 },
|
|
1394
|
-
{ id: "internal", label: "Notas internas", columns: 1 }
|
|
1395
|
-
]
|
|
1396
|
-
};
|
|
1397
|
-
}
|
|
1398
2104
|
var STATUS_TONE2 = {
|
|
1399
2105
|
active: "bg-emerald-100 text-emerald-800",
|
|
1400
2106
|
draft: "bg-gray-100 text-gray-700",
|
|
@@ -1556,6 +2262,34 @@ function buildShopDiscountEntity() {
|
|
|
1556
2262
|
facets: [{ field: "status", allLabel: "Todos" }]
|
|
1557
2263
|
};
|
|
1558
2264
|
}
|
|
2265
|
+
|
|
2266
|
+
// src/views/discount-agent.ts
|
|
2267
|
+
var DISCOUNT_CREATE_INSTRUCTIONS = `
|
|
2268
|
+
Esta lista \xE9 a de Descontos da loja. Um registro aqui \xE9 um CUPOM que o comprador digita no carrinho.
|
|
2269
|
+
|
|
2270
|
+
O c\xF3digo \xE9 seu para escrever:
|
|
2271
|
+
\xB7 Se o lojista n\xE3o ditar um, invente a partir da campanha: MAI\xDASCULAS, s\xF3 letras A-Z e n\xFAmeros, de 4 a 16 caracteres, sem espa\xE7o, acento, h\xEDfen ou pontua\xE7\xE3o.
|
|
2272
|
+
\xB7 Curto e dit\xE1vel ao telefone. Evite 0/O e 1/I juntos, evite palavra que o comprador precise soletrar.
|
|
2273
|
+
\xB7 Nunca repita um c\xF3digo que j\xE1 aparece em \`taken.code\`. Se a cria\xE7\xE3o voltar \`duplicate_value\`, escolha outro e chame de novo \u2014 sem perguntar nada ao lojista.
|
|
2274
|
+
\xB7 \`title\` \xE9 o nome interno, s\xF3 o lojista v\xEA: escreva a campanha em portugu\xEAs ("Frete gr\xE1tis fim de semana"), n\xE3o repita o c\xF3digo.
|
|
2275
|
+
|
|
2276
|
+
A regra:
|
|
2277
|
+
\xB7 "frete gr\xE1tis" \u2192 \`type: free_shipping\` e SEM \`value\` \u2014 frete gr\xE1tis n\xE3o desconta o subtotal.
|
|
2278
|
+
\xB7 "10%", "10 por cento" \u2192 \`type: percentage\`, \`value: 10\`.
|
|
2279
|
+
\xB7 "R$ 50", "50 reais de desconto" \u2192 \`type: fixed_amount\`, \`value: 50\`.
|
|
2280
|
+
\xB7 "acima de R$ 200", "a partir de 200" \u2192 \`minSubtotal: 200\`.
|
|
2281
|
+
\xB7 "s\xF3 a primeira compra", "uma vez por cliente" \u2192 \`oncePerCustomer: true\`.
|
|
2282
|
+
\xB7 "os 100 primeiros" \u2192 \`usageLimit: 100\`.
|
|
2283
|
+
\xB7 Prazo vira data em \`endsAt\`: "at\xE9 o fim dessa semana" \xE9 o domingo desta semana, "at\xE9 o fim do m\xEAs" \xE9 o \xFAltimo dia do m\xEAs. Sem prazo dito, deixe vazio \u2014 cupom sem fim \xE9 uma escolha, n\xE3o um esquecimento.
|
|
2284
|
+
|
|
2285
|
+
Status:
|
|
2286
|
+
\xB7 \`active\` por padr\xE3o. O lojista est\xE1 pedindo uma promo\xE7\xE3o para divulgar hoje; um cupom que nasce dormindo \xE9 um cupom que o cliente tenta usar e n\xE3o funciona.
|
|
2287
|
+
\xB7 \`draft\` s\xF3 quando ele disser que \xE9 para deixar pronto para depois.
|
|
2288
|
+
|
|
2289
|
+
O que voc\xEA N\xC3O inventa: o tamanho do desconto. Se ele disser "desconto para clientes VIP" sem dizer quanto, essa \xE9 a \xFAnica pergunta a fazer \u2014 e fa\xE7a s\xF3 ela.
|
|
2290
|
+
|
|
2291
|
+
Depois de criar, diga em uma linha o c\xF3digo, o efeito e at\xE9 quando vale, para ele j\xE1 poder divulgar.
|
|
2292
|
+
`.trim();
|
|
1559
2293
|
function buildShopCategoryEntity() {
|
|
1560
2294
|
const fields = [
|
|
1561
2295
|
{
|
|
@@ -1744,7 +2478,7 @@ var STATUS_LABEL2 = {
|
|
|
1744
2478
|
disabled: "Desativado"
|
|
1745
2479
|
};
|
|
1746
2480
|
function buildShopGiftCardEntity(currencyCode = "BRL") {
|
|
1747
|
-
const
|
|
2481
|
+
const money2 = (v) => new Intl.NumberFormat("pt-BR", { style: "currency", currency: currencyCode }).format(Number(v) || 0);
|
|
1748
2482
|
const fields = [
|
|
1749
2483
|
{
|
|
1750
2484
|
key: "code",
|
|
@@ -1772,7 +2506,7 @@ function buildShopGiftCardEntity(currencyCode = "BRL") {
|
|
|
1772
2506
|
hint: "Valor de face do cart\xE3o. N\xE3o muda depois de emitido.",
|
|
1773
2507
|
showInTable: true,
|
|
1774
2508
|
sortable: true,
|
|
1775
|
-
renderCell: (v) => /* @__PURE__ */ jsx("span", { className: "block text-right text-muted-foreground", children:
|
|
2509
|
+
renderCell: (v) => /* @__PURE__ */ jsx("span", { className: "block text-right text-muted-foreground", children: money2(v) })
|
|
1776
2510
|
},
|
|
1777
2511
|
{
|
|
1778
2512
|
key: "balance",
|
|
@@ -1786,7 +2520,7 @@ function buildShopGiftCardEntity(currencyCode = "BRL") {
|
|
|
1786
2520
|
hint: "O que ainda pode ser gasto. Cai a cada resgate.",
|
|
1787
2521
|
showInTable: true,
|
|
1788
2522
|
sortable: true,
|
|
1789
|
-
renderCell: (v) => /* @__PURE__ */ jsx("span", { className: "block text-right font-medium", children:
|
|
2523
|
+
renderCell: (v) => /* @__PURE__ */ jsx("span", { className: "block text-right font-medium", children: money2(v) })
|
|
1790
2524
|
},
|
|
1791
2525
|
{
|
|
1792
2526
|
key: "status",
|
|
@@ -1921,20 +2655,20 @@ function buildShopCustomerGroupEntity() {
|
|
|
1921
2655
|
fieldGroups: [{ id: "basic", label: "Grupo", columns: 2 }]
|
|
1922
2656
|
};
|
|
1923
2657
|
}
|
|
1924
|
-
function describeSegment(s,
|
|
2658
|
+
function describeSegment(s, money2) {
|
|
1925
2659
|
const parts = [];
|
|
1926
2660
|
if (s.minOrders != null && s.maxOrders != null) parts.push(`${s.minOrders}\u2013${s.maxOrders} pedidos`);
|
|
1927
2661
|
else if (s.minOrders != null) parts.push(`${s.minOrders}+ pedidos`);
|
|
1928
2662
|
else if (s.maxOrders != null) parts.push(`at\xE9 ${s.maxOrders} pedidos`);
|
|
1929
|
-
if (s.minSpent != null && s.maxSpent != null) parts.push(`gastou ${
|
|
1930
|
-
else if (s.minSpent != null) parts.push(`gastou ${
|
|
1931
|
-
else if (s.maxSpent != null) parts.push(`gastou at\xE9 ${
|
|
2663
|
+
if (s.minSpent != null && s.maxSpent != null) parts.push(`gastou ${money2(s.minSpent)}\u2013${money2(s.maxSpent)}`);
|
|
2664
|
+
else if (s.minSpent != null) parts.push(`gastou ${money2(s.minSpent)}+`);
|
|
2665
|
+
else if (s.maxSpent != null) parts.push(`gastou at\xE9 ${money2(s.maxSpent)}`);
|
|
1932
2666
|
if (s.joinedAfter) parts.push(`entrou ap\xF3s ${new Date(s.joinedAfter).toLocaleDateString("pt-BR")}`);
|
|
1933
2667
|
if (s.joinedBefore) parts.push(`entrou antes de ${new Date(s.joinedBefore).toLocaleDateString("pt-BR")}`);
|
|
1934
2668
|
return parts.length ? parts.join(" \xB7 ") : "Todos os clientes";
|
|
1935
2669
|
}
|
|
1936
2670
|
function buildShopCustomerSegmentEntity(currencyCode = "BRL") {
|
|
1937
|
-
const
|
|
2671
|
+
const money2 = (v) => new Intl.NumberFormat("pt-BR", { style: "currency", currency: currencyCode }).format(v);
|
|
1938
2672
|
const fields = [
|
|
1939
2673
|
{
|
|
1940
2674
|
key: "name",
|
|
@@ -2011,7 +2745,7 @@ function buildShopCustomerSegmentEntity(currencyCode = "BRL") {
|
|
|
2011
2745
|
showInForm: false,
|
|
2012
2746
|
showInTable: true,
|
|
2013
2747
|
sortable: false,
|
|
2014
|
-
renderCell: (_v, row) => /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: describeSegment(row,
|
|
2748
|
+
renderCell: (_v, row) => /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: describeSegment(row, money2) })
|
|
2015
2749
|
},
|
|
2016
2750
|
{
|
|
2017
2751
|
key: "matchCount",
|
|
@@ -2341,17 +3075,17 @@ function createShopCustomerSegmentDataProvider(provider) {
|
|
|
2341
3075
|
}
|
|
2342
3076
|
var DEBOUNCE_MS = 400;
|
|
2343
3077
|
function useStoreSettings(provider, defaults = {}) {
|
|
2344
|
-
const shop =
|
|
3078
|
+
const shop = React9.useMemo(
|
|
2345
3079
|
() => provider ? resolve(provider) : getShopProvider(),
|
|
2346
3080
|
[provider]
|
|
2347
3081
|
);
|
|
2348
3082
|
const supported = typeof shop.getStoreSettings === "function" && typeof shop.updateStoreSettings === "function";
|
|
2349
|
-
const [stored, setStored] =
|
|
2350
|
-
const [loading, setLoading] =
|
|
2351
|
-
const [saving, setSaving] =
|
|
2352
|
-
const [error, setError] =
|
|
2353
|
-
const [loadError, setLoadError] =
|
|
2354
|
-
|
|
3083
|
+
const [stored, setStored] = React9.useState(null);
|
|
3084
|
+
const [loading, setLoading] = React9.useState(supported);
|
|
3085
|
+
const [saving, setSaving] = React9.useState(false);
|
|
3086
|
+
const [error, setError] = React9.useState(null);
|
|
3087
|
+
const [loadError, setLoadError] = React9.useState(null);
|
|
3088
|
+
React9.useEffect(() => {
|
|
2355
3089
|
if (!supported) return;
|
|
2356
3090
|
let cancelled = false;
|
|
2357
3091
|
setLoading(true);
|
|
@@ -2367,12 +3101,12 @@ function useStoreSettings(provider, defaults = {}) {
|
|
|
2367
3101
|
cancelled = true;
|
|
2368
3102
|
};
|
|
2369
3103
|
}, [shop, supported]);
|
|
2370
|
-
const pendingRef =
|
|
2371
|
-
const timerRef =
|
|
2372
|
-
|
|
3104
|
+
const pendingRef = React9.useRef({});
|
|
3105
|
+
const timerRef = React9.useRef(null);
|
|
3106
|
+
React9.useEffect(() => () => {
|
|
2373
3107
|
if (timerRef.current) clearTimeout(timerRef.current);
|
|
2374
3108
|
}, []);
|
|
2375
|
-
const set =
|
|
3109
|
+
const set = React9.useCallback(
|
|
2376
3110
|
(patch) => {
|
|
2377
3111
|
setStored((current) => ({ ...current ?? {}, ...patch }));
|
|
2378
3112
|
if (!supported) return;
|
|
@@ -2388,7 +3122,7 @@ function useStoreSettings(provider, defaults = {}) {
|
|
|
2388
3122
|
},
|
|
2389
3123
|
[shop, supported]
|
|
2390
3124
|
);
|
|
2391
|
-
const value =
|
|
3125
|
+
const value = React9.useMemo(() => ({ ...defaults, ...stored ?? {} }), [defaults, stored]);
|
|
2392
3126
|
return { value, stored, set, loading, saving, error, loadError, supported };
|
|
2393
3127
|
}
|
|
2394
3128
|
function SettingsState({
|
|
@@ -2617,25 +3351,25 @@ function PaymentsSettings({ provider }) {
|
|
|
2617
3351
|
const settings = useStoreSettings(provider);
|
|
2618
3352
|
const { value, set } = settings;
|
|
2619
3353
|
const { connectors } = useConnectors({ hostPluginId: "shop", includeGrants: false });
|
|
2620
|
-
const [adding, setAdding] =
|
|
2621
|
-
const [selected, setSelected] =
|
|
3354
|
+
const [adding, setAdding] = React9.useState(false);
|
|
3355
|
+
const [selected, setSelected] = React9.useState(null);
|
|
2622
3356
|
const panelAvailable = useConnectorsPanelAvailable();
|
|
2623
|
-
const openJourney =
|
|
3357
|
+
const openJourney = React9.useCallback(
|
|
2624
3358
|
(connector) => {
|
|
2625
3359
|
if (panelAvailable) openConnectorsPanel(connector);
|
|
2626
3360
|
else setSelected(connector);
|
|
2627
3361
|
},
|
|
2628
3362
|
[panelAvailable]
|
|
2629
3363
|
);
|
|
2630
|
-
const payment =
|
|
3364
|
+
const payment = React9.useMemo(
|
|
2631
3365
|
() => (connectors ?? []).filter((c) => c.category && PAYMENT_CATEGORIES.includes(c.category)),
|
|
2632
3366
|
[connectors]
|
|
2633
3367
|
);
|
|
2634
|
-
const live =
|
|
3368
|
+
const live = React9.useMemo(() => payment.filter((c) => c.state === "connected"), [payment]);
|
|
2635
3369
|
const chosen = value.paymentMethods ?? [];
|
|
2636
3370
|
const staleProvider = !!value.paymentProvider && !live.some((c) => c.slug === value.paymentProvider) && live.length === 0;
|
|
2637
|
-
const movedFrom =
|
|
2638
|
-
|
|
3371
|
+
const movedFrom = React9.useRef(null);
|
|
3372
|
+
React9.useEffect(() => {
|
|
2639
3373
|
if (!settings.supported || settings.loading) return;
|
|
2640
3374
|
const next = resolvePaymentProvider(value.paymentProvider, live.map((c) => c.slug));
|
|
2641
3375
|
if (!next) return;
|
|
@@ -2841,17 +3575,17 @@ function CheckoutSettings({ provider }) {
|
|
|
2841
3575
|
{
|
|
2842
3576
|
title: "Campos do cliente",
|
|
2843
3577
|
description: "O que o comprador v\xEA e o que ele precisa preencher. Campos travados s\xE3o os que a entrega n\xE3o dispensa.",
|
|
2844
|
-
children: FIELDS.map((
|
|
3578
|
+
children: FIELDS.map((field2) => /* @__PURE__ */ jsx(
|
|
2845
3579
|
SelectRow,
|
|
2846
3580
|
{
|
|
2847
|
-
label:
|
|
2848
|
-
description:
|
|
2849
|
-
value: toRequirement(fields[
|
|
2850
|
-
options:
|
|
2851
|
-
onChange: (choice) => setField(
|
|
2852
|
-
disabled:
|
|
3581
|
+
label: field2.label,
|
|
3582
|
+
description: field2.hint,
|
|
3583
|
+
value: toRequirement(fields[field2.key], !!field2.locked),
|
|
3584
|
+
options: field2.locked ? [{ value: "required", label: "Pedir (obrigat\xF3rio)" }] : REQUIREMENT,
|
|
3585
|
+
onChange: (choice) => setField(field2.key, choice),
|
|
3586
|
+
disabled: field2.locked
|
|
2853
3587
|
},
|
|
2854
|
-
|
|
3588
|
+
field2.key
|
|
2855
3589
|
))
|
|
2856
3590
|
}
|
|
2857
3591
|
)
|
|
@@ -2867,11 +3601,11 @@ function PackagesSettings({ basePath = shippingRegistryPath(PACKAGES_REGISTRY) }
|
|
|
2867
3601
|
] });
|
|
2868
3602
|
}
|
|
2869
3603
|
function HandlingSettings({ settings, provider }) {
|
|
2870
|
-
const shop =
|
|
3604
|
+
const shop = React9.useMemo(() => provider ? resolve(provider) : getShopProvider(), [provider]);
|
|
2871
3605
|
const { value, set, loading, supported } = settings;
|
|
2872
|
-
const [noteDraft, setNoteDraft] =
|
|
2873
|
-
const [profiles, setProfiles] =
|
|
2874
|
-
|
|
3606
|
+
const [noteDraft, setNoteDraft] = React9.useState(null);
|
|
3607
|
+
const [profiles, setProfiles] = React9.useState(null);
|
|
3608
|
+
React9.useEffect(() => {
|
|
2875
3609
|
let cancelled = false;
|
|
2876
3610
|
void shop.listShippingProfiles?.().then((list) => {
|
|
2877
3611
|
if (!cancelled) setProfiles(list);
|
|
@@ -3001,46 +3735,46 @@ var FREIGHT_SOURCE = [
|
|
|
3001
3735
|
}
|
|
3002
3736
|
];
|
|
3003
3737
|
function ShippingSettings({ provider }) {
|
|
3004
|
-
const [registryId, setRegistryId] =
|
|
3005
|
-
|
|
3738
|
+
const [registryId, setRegistryId] = React9.useState(shippingRegistryIdFromHash);
|
|
3739
|
+
React9.useEffect(() => {
|
|
3006
3740
|
const handler = () => setRegistryId(shippingRegistryIdFromHash());
|
|
3007
3741
|
window.addEventListener("hashchange", handler);
|
|
3008
3742
|
return () => window.removeEventListener("hashchange", handler);
|
|
3009
3743
|
}, []);
|
|
3010
|
-
const shop =
|
|
3011
|
-
const registries =
|
|
3744
|
+
const shop = React9.useMemo(() => provider ? resolve(provider) : getShopProvider(), [provider]);
|
|
3745
|
+
const registries = React9.useMemo(
|
|
3012
3746
|
() => buildShopRegistries(provider ?? getShopProvider),
|
|
3013
3747
|
[provider]
|
|
3014
3748
|
);
|
|
3015
3749
|
const storeSettings = useStoreSettings(provider);
|
|
3016
3750
|
const supported = typeof shop.getShippingSettings === "function" && typeof shop.updateShippingSettings === "function";
|
|
3017
|
-
const [row, setRow] =
|
|
3018
|
-
const [zones, setZones] =
|
|
3019
|
-
const [locations, setLocations] =
|
|
3020
|
-
const [loading, setLoading] =
|
|
3021
|
-
const [saving, setSaving] =
|
|
3022
|
-
const [error, setError] =
|
|
3023
|
-
const [loadError, setLoadError] =
|
|
3024
|
-
const [flatDraft, setFlatDraft] =
|
|
3025
|
-
const [freeDraft, setFreeDraft] =
|
|
3026
|
-
const [flatBad, setFlatBad] =
|
|
3027
|
-
const [freeBad, setFreeBad] =
|
|
3751
|
+
const [row, setRow] = React9.useState(null);
|
|
3752
|
+
const [zones, setZones] = React9.useState(null);
|
|
3753
|
+
const [locations, setLocations] = React9.useState(null);
|
|
3754
|
+
const [loading, setLoading] = React9.useState(supported);
|
|
3755
|
+
const [saving, setSaving] = React9.useState(false);
|
|
3756
|
+
const [error, setError] = React9.useState(null);
|
|
3757
|
+
const [loadError, setLoadError] = React9.useState(null);
|
|
3758
|
+
const [flatDraft, setFlatDraft] = React9.useState("");
|
|
3759
|
+
const [freeDraft, setFreeDraft] = React9.useState("");
|
|
3760
|
+
const [flatBad, setFlatBad] = React9.useState(false);
|
|
3761
|
+
const [freeBad, setFreeBad] = React9.useState(false);
|
|
3028
3762
|
const { connectors } = useConnectors({ hostPluginId: "shop", includeGrants: false });
|
|
3029
|
-
const [selected, setSelected] =
|
|
3763
|
+
const [selected, setSelected] = React9.useState(null);
|
|
3030
3764
|
const panelAvailable = useConnectorsPanelAvailable();
|
|
3031
|
-
const openJourney =
|
|
3765
|
+
const openJourney = React9.useCallback(
|
|
3032
3766
|
(connector) => {
|
|
3033
3767
|
if (panelAvailable) openConnectorsPanel(connector);
|
|
3034
3768
|
else setSelected(connector);
|
|
3035
3769
|
},
|
|
3036
3770
|
[panelAvailable]
|
|
3037
3771
|
);
|
|
3038
|
-
const carriers =
|
|
3772
|
+
const carriers = React9.useMemo(
|
|
3039
3773
|
() => (connectors ?? []).filter((c) => c.category === "logistics" || c.category === "shipping"),
|
|
3040
3774
|
[connectors]
|
|
3041
3775
|
);
|
|
3042
3776
|
const carriersConnected = carriers.some((c) => c.state !== "ready");
|
|
3043
|
-
|
|
3777
|
+
React9.useEffect(() => {
|
|
3044
3778
|
let cancelled = false;
|
|
3045
3779
|
if (supported) {
|
|
3046
3780
|
setLoading(true);
|
|
@@ -3069,7 +3803,7 @@ function ShippingSettings({ provider }) {
|
|
|
3069
3803
|
cancelled = true;
|
|
3070
3804
|
};
|
|
3071
3805
|
}, [shop, supported]);
|
|
3072
|
-
const save =
|
|
3806
|
+
const save = React9.useCallback(
|
|
3073
3807
|
(patch) => {
|
|
3074
3808
|
if (!supported) return;
|
|
3075
3809
|
setSaving(true);
|
|
@@ -4040,6 +4774,10 @@ var stripeConnectConnectorDef = {
|
|
|
4040
4774
|
};
|
|
4041
4775
|
var VINDI_CONNECTOR_ID = "vindi";
|
|
4042
4776
|
var VINDI_LATENCY_BUDGET_MS = 6e3;
|
|
4777
|
+
var VINDI_AUTHENTICATION_URLS = {
|
|
4778
|
+
sandbox: "https://tc-intermediador-sandbox.yapay.com.br/authentication?consumer_key=",
|
|
4779
|
+
production: "https://tc.intermediador.yapay.com.br/authentication?consumer_key="
|
|
4780
|
+
};
|
|
4043
4781
|
var VINDI_FIELDS = [
|
|
4044
4782
|
{
|
|
4045
4783
|
key: "tokenAccount",
|
|
@@ -4047,6 +4785,27 @@ var VINDI_FIELDS = [
|
|
|
4047
4785
|
type: "password",
|
|
4048
4786
|
secret: true,
|
|
4049
4787
|
placeholder: "Vindi Pagamentos \u2192 Minha Conta \u2192 Token"
|
|
4788
|
+
},
|
|
4789
|
+
{
|
|
4790
|
+
key: "consumerKey",
|
|
4791
|
+
label: "Chave de consumidor (para links de pagamento)",
|
|
4792
|
+
type: "password",
|
|
4793
|
+
secret: true,
|
|
4794
|
+
placeholder: "consumer_key"
|
|
4795
|
+
},
|
|
4796
|
+
{
|
|
4797
|
+
key: "consumerSecret",
|
|
4798
|
+
label: "Segredo de consumidor",
|
|
4799
|
+
type: "password",
|
|
4800
|
+
secret: true,
|
|
4801
|
+
placeholder: "consumer_secret"
|
|
4802
|
+
},
|
|
4803
|
+
{
|
|
4804
|
+
key: "authorizationCode",
|
|
4805
|
+
label: "CODE de autoriza\xE7\xE3o",
|
|
4806
|
+
type: "password",
|
|
4807
|
+
secret: true,
|
|
4808
|
+
placeholder: "copiado da barra de endere\xE7o, uma vez por ambiente"
|
|
4050
4809
|
}
|
|
4051
4810
|
];
|
|
4052
4811
|
var VINDI_ENVIRONMENT_SETTING = {
|
|
@@ -4145,11 +4904,14 @@ function connectionDetail(settings) {
|
|
|
4145
4904
|
}
|
|
4146
4905
|
function VindiExtraPanel() {
|
|
4147
4906
|
const [connected, setConnected] = useState(false);
|
|
4907
|
+
const [environment, setEnvironment] = useState("");
|
|
4148
4908
|
const [loaded, setLoaded] = useState(false);
|
|
4149
4909
|
useEffect(() => {
|
|
4150
4910
|
let cancelled = false;
|
|
4151
4911
|
void store5.get().then((c) => {
|
|
4152
|
-
if (
|
|
4912
|
+
if (cancelled) return;
|
|
4913
|
+
setConnected(!!c?.active);
|
|
4914
|
+
setEnvironment(String(c?.settings?.environment ?? ""));
|
|
4153
4915
|
}).catch(() => {
|
|
4154
4916
|
}).finally(() => {
|
|
4155
4917
|
if (!cancelled) setLoaded(true);
|
|
@@ -4159,16 +4921,53 @@ function VindiExtraPanel() {
|
|
|
4159
4921
|
};
|
|
4160
4922
|
}, []);
|
|
4161
4923
|
if (!loaded) return null;
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
/* @__PURE__ */ jsxs(
|
|
4165
|
-
"
|
|
4166
|
-
/* @__PURE__ */
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4924
|
+
const authenticationUrl = VINDI_AUTHENTICATION_URLS[environment];
|
|
4925
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-2 border-t pt-4 text-xs text-muted-foreground", children: [
|
|
4926
|
+
connected ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
4927
|
+
/* @__PURE__ */ jsx("p", { children: "Pix e boleto acontecem dentro da sua loja: o cliente v\xEA o QR Code ou a linha digit\xE1vel sem sair da p\xE1gina. O cart\xE3o \xE9 diferente \u2014 ele \xE9 digitado na p\xE1gina segura da Vindi, e o cliente volta para a loja em seguida. \xC9 assim de prop\xF3sito: nenhum n\xFAmero de cart\xE3o passa pelo seu site." }),
|
|
4928
|
+
/* @__PURE__ */ jsxs("p", { children: [
|
|
4929
|
+
"Voc\xEA tamb\xE9m pode gerar um ",
|
|
4930
|
+
/* @__PURE__ */ jsx("strong", { children: "link de pagamento" }),
|
|
4931
|
+
" em Pedidos \u2192 Novo e mandar no WhatsApp. O cliente escolhe como pagar na p\xE1gina da Vindi, e o pedido aparece aqui sozinho quando o dinheiro cair."
|
|
4932
|
+
] }),
|
|
4933
|
+
/* @__PURE__ */ jsx("p", { children: "Sandbox e produ\xE7\xE3o s\xE3o contas diferentes, com tokens diferentes. Se voc\xEA trocar o ambiente aqui, troque tamb\xE9m as credenciais \u2014 as de teste n\xE3o cobram em produ\xE7\xE3o e as de produ\xE7\xE3o n\xE3o funcionam no sandbox." }),
|
|
4934
|
+
/* @__PURE__ */ jsx("p", { children: 'O valor cobrado \xE9 sempre recalculado aqui no servidor a partir do pedido. Pre\xE7o alterado no navegador n\xE3o \xE9 aceito, e clicar duas vezes em "pagar" gera uma cobran\xE7a s\xF3.' })
|
|
4935
|
+
] }) : /* @__PURE__ */ jsx("p", { children: "O token da conta voc\xEA pega no painel da Vindi Pagamentos, em Minha Conta. Confira se ele \xE9 o do mesmo ambiente que voc\xEA vai escolher aqui \u2014 o token de sandbox n\xE3o cobra em produ\xE7\xE3o." }),
|
|
4936
|
+
/* @__PURE__ */ jsxs("div", { className: "rounded-md border border-dashed p-3", children: [
|
|
4937
|
+
/* @__PURE__ */ jsx("p", { className: "font-medium text-foreground", children: "Para gerar links de pagamento" }),
|
|
4938
|
+
/* @__PURE__ */ jsxs("p", { className: "mt-1", children: [
|
|
4939
|
+
"O token da conta n\xE3o emite link \u2014 a Vindi exige uma autoriza\xE7\xE3o \xE0 parte. Pe\xE7a a",
|
|
4940
|
+
" ",
|
|
4941
|
+
/* @__PURE__ */ jsx("strong", { children: "chave" }),
|
|
4942
|
+
" e o ",
|
|
4943
|
+
/* @__PURE__ */ jsx("strong", { children: "segredo de consumidor" }),
|
|
4944
|
+
" no painel da Vindi e preencha os campos acima. Falta ainda o ",
|
|
4945
|
+
/* @__PURE__ */ jsx("strong", { children: "CODE" }),
|
|
4946
|
+
":"
|
|
4947
|
+
] }),
|
|
4948
|
+
/* @__PURE__ */ jsxs("ol", { className: "mt-2 list-decimal space-y-1 pl-4", children: [
|
|
4949
|
+
/* @__PURE__ */ jsx("li", { children: authenticationUrl ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
4950
|
+
"Abra no navegador",
|
|
4951
|
+
" ",
|
|
4952
|
+
/* @__PURE__ */ jsxs("code", { className: "break-all rounded bg-muted px-1 py-0.5", children: [
|
|
4953
|
+
authenticationUrl,
|
|
4954
|
+
"SUA_CHAVE"
|
|
4955
|
+
] }),
|
|
4956
|
+
" ",
|
|
4957
|
+
"trocando ",
|
|
4958
|
+
/* @__PURE__ */ jsx("code", { children: "SUA_CHAVE" }),
|
|
4959
|
+
" pela sua chave de consumidor."
|
|
4960
|
+
] }) : /* @__PURE__ */ jsx(Fragment, { children: "Escolha o ambiente acima e o endere\xE7o para buscar o CODE aparece aqui." }) }),
|
|
4961
|
+
/* @__PURE__ */ jsx("li", { children: "A Vindi redireciona. O CODE fica na barra de endere\xE7o \u2014 copie de l\xE1." }),
|
|
4962
|
+
/* @__PURE__ */ jsxs("li", { children: [
|
|
4963
|
+
"Cole no campo ",
|
|
4964
|
+
/* @__PURE__ */ jsx("strong", { children: "CODE de autoriza\xE7\xE3o" }),
|
|
4965
|
+
" e salve."
|
|
4966
|
+
] })
|
|
4967
|
+
] }),
|
|
4968
|
+
/* @__PURE__ */ jsx("p", { className: "mt-2", children: "\xC9 uma vez por ambiente. A partir da\xED a renova\xE7\xE3o di\xE1ria acontece sozinha." })
|
|
4969
|
+
] })
|
|
4970
|
+
] });
|
|
4172
4971
|
}
|
|
4173
4972
|
var vindiConnectorDef = {
|
|
4174
4973
|
id: VINDI_CONNECTOR_ID,
|
|
@@ -4267,6 +5066,188 @@ var vindiConnectorDef = {
|
|
|
4267
5066
|
disconnect: () => store5.disconnect(),
|
|
4268
5067
|
ExtraPanel: VindiExtraPanel
|
|
4269
5068
|
};
|
|
5069
|
+
var MERCADOPAGO_CONNECTOR_ID = "mercadopago";
|
|
5070
|
+
var MERCADOPAGO_LATENCY_BUDGET_MS = 6e3;
|
|
5071
|
+
var MERCADOPAGO_FIELDS = [
|
|
5072
|
+
{
|
|
5073
|
+
key: "accessToken",
|
|
5074
|
+
label: "Access token",
|
|
5075
|
+
type: "password",
|
|
5076
|
+
secret: true,
|
|
5077
|
+
placeholder: "APP_USR-\u2026 (Suas integra\xE7\xF5es \u2192 sua aplica\xE7\xE3o \u2192 Credenciais)"
|
|
5078
|
+
},
|
|
5079
|
+
{
|
|
5080
|
+
key: "publicKey",
|
|
5081
|
+
label: "Public key",
|
|
5082
|
+
// Not masked: it travels to the browser anyway, and a merchant who cannot
|
|
5083
|
+
// see it cannot tell the test key from the production one at a glance.
|
|
5084
|
+
type: "text",
|
|
5085
|
+
secret: true,
|
|
5086
|
+
placeholder: "APP_USR-\u2026"
|
|
5087
|
+
}
|
|
5088
|
+
];
|
|
5089
|
+
var store6 = createConnectionStore(MERCADOPAGO_CONNECTOR_ID, {
|
|
5090
|
+
fields: MERCADOPAGO_FIELDS
|
|
5091
|
+
});
|
|
5092
|
+
function connectionDetail2(settings) {
|
|
5093
|
+
const key = String(settings?.publicKey ?? "").trim();
|
|
5094
|
+
if (!key) return "Pix e cart\xE3o";
|
|
5095
|
+
return /^TEST-/i.test(key) ? "Sandbox \xB7 Pix e cart\xE3o" : "Produ\xE7\xE3o \xB7 Pix e cart\xE3o";
|
|
5096
|
+
}
|
|
5097
|
+
function MercadoPagoExtraPanel() {
|
|
5098
|
+
const [connected, setConnected] = useState(false);
|
|
5099
|
+
const [loaded, setLoaded] = useState(false);
|
|
5100
|
+
useEffect(() => {
|
|
5101
|
+
let cancelled = false;
|
|
5102
|
+
void store6.get().then((c) => {
|
|
5103
|
+
if (!cancelled) setConnected(!!c?.active);
|
|
5104
|
+
}).catch(() => {
|
|
5105
|
+
}).finally(() => {
|
|
5106
|
+
if (!cancelled) setLoaded(true);
|
|
5107
|
+
});
|
|
5108
|
+
return () => {
|
|
5109
|
+
cancelled = true;
|
|
5110
|
+
};
|
|
5111
|
+
}, []);
|
|
5112
|
+
if (!loaded) return null;
|
|
5113
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-2 border-t pt-4 text-xs text-muted-foreground", children: [
|
|
5114
|
+
connected ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
5115
|
+
/* @__PURE__ */ jsxs("p", { children: [
|
|
5116
|
+
"Pix e cart\xE3o acontecem ",
|
|
5117
|
+
/* @__PURE__ */ jsx("strong", { children: "dentro da sua loja" }),
|
|
5118
|
+
". O cliente v\xEA o QR Code ou digita o cart\xE3o sem sair da p\xE1gina, e volta para a confirma\xE7\xE3o no mesmo lugar. Diferente das outras adquirentes, aqui o cart\xE3o n\xE3o redireciona."
|
|
5119
|
+
] }),
|
|
5120
|
+
/* @__PURE__ */ jsx("p", { children: "O n\xFAmero do cart\xE3o n\xE3o passa pelo seu site: ele \xE9 digitado dentro de um formul\xE1rio do pr\xF3prio Mercado Pago e vira um c\xF3digo de uso \xFAnico antes de chegar at\xE9 n\xF3s. \xC9 por isso que d\xE1 para receber cart\xE3o na sua p\xE1gina sem a auditoria que isso normalmente exige." }),
|
|
5121
|
+
/* @__PURE__ */ jsx("p", { children: 'O valor cobrado \xE9 sempre recalculado aqui no servidor a partir do pedido. Pre\xE7o alterado no navegador n\xE3o \xE9 aceito, e clicar duas vezes em "pagar" gera uma cobran\xE7a s\xF3.' }),
|
|
5122
|
+
/* @__PURE__ */ jsx("p", { children: "O Pix vale por 30 minutos. Se o cliente fechar a aba depois de pagar, o pedido aparece aqui do mesmo jeito \u2014 o Mercado Pago avisa a loja assim que o dinheiro cai." })
|
|
5123
|
+
] }) : /* @__PURE__ */ jsxs("p", { children: [
|
|
5124
|
+
"As duas chaves ficam no painel do Mercado Pago, em ",
|
|
5125
|
+
/* @__PURE__ */ jsx("strong", { children: "Suas integra\xE7\xF5es" }),
|
|
5126
|
+
" \u2192 sua aplica\xE7\xE3o \u2192 ",
|
|
5127
|
+
/* @__PURE__ */ jsx("strong", { children: "Credenciais de produ\xE7\xE3o" }),
|
|
5128
|
+
". Copie o ",
|
|
5129
|
+
/* @__PURE__ */ jsx("em", { children: "access token" }),
|
|
5130
|
+
" e a",
|
|
5131
|
+
" ",
|
|
5132
|
+
/* @__PURE__ */ jsx("em", { children: "public key" }),
|
|
5133
|
+
" da ",
|
|
5134
|
+
/* @__PURE__ */ jsx("strong", { children: "mesma" }),
|
|
5135
|
+
" aplica\xE7\xE3o: elas s\xE3o um par, e uma sem a outra n\xE3o fecha uma venda."
|
|
5136
|
+
] }),
|
|
5137
|
+
/* @__PURE__ */ jsxs("div", { className: "rounded-md border border-dashed p-3", children: [
|
|
5138
|
+
/* @__PURE__ */ jsx("p", { className: "font-medium text-foreground", children: "Para testar sem cobrar de verdade" }),
|
|
5139
|
+
/* @__PURE__ */ jsxs("p", { className: "mt-1", children: [
|
|
5140
|
+
"As chaves que come\xE7am com ",
|
|
5141
|
+
/* @__PURE__ */ jsx("code", { className: "rounded bg-muted px-1 py-0.5", children: "TEST-" }),
|
|
5142
|
+
" ",
|
|
5143
|
+
/* @__PURE__ */ jsx("strong", { children: "n\xE3o funcionam" }),
|
|
5144
|
+
" aqui \u2014 o Mercado Pago as recusa nesta API. O teste \xE9 feito com ",
|
|
5145
|
+
/* @__PURE__ */ jsx("strong", { children: "usu\xE1rios de teste" }),
|
|
5146
|
+
": crie um vendedor e um comprador em Suas integra\xE7\xF5es \u2192 Contas de teste, e use as chaves de ",
|
|
5147
|
+
/* @__PURE__ */ jsx("em", { children: "produ\xE7\xE3o" }),
|
|
5148
|
+
" do vendedor de teste. N\xE3o \xE9 dinheiro real, e \xE9 assim que o pr\xF3prio Mercado Pago recomenda."
|
|
5149
|
+
] })
|
|
5150
|
+
] })
|
|
5151
|
+
] });
|
|
5152
|
+
}
|
|
5153
|
+
var mercadoPagoConnectorDef = {
|
|
5154
|
+
id: MERCADOPAGO_CONNECTOR_ID,
|
|
5155
|
+
hostPluginId: "shop",
|
|
5156
|
+
name: "Mercado Pago",
|
|
5157
|
+
description: "Pix e cart\xE3o pelo Mercado Pago, os dois pagos dentro da loja \u2014 o cart\xE3o \xE9 digitado sem o cliente sair da p\xE1gina.",
|
|
5158
|
+
icon: "CreditCard",
|
|
5159
|
+
// Declared rather than borrowed from the catalogue, for the same reason Vindi
|
|
5160
|
+
// declares its own: the catalogue row is a change in another repo that has to
|
|
5161
|
+
// be seeded before it exists anywhere, and until then the shelf renders a grey
|
|
5162
|
+
// plug. The absolute URL answers from any origin — a tenant app is served from
|
|
5163
|
+
// its own domain, so a path would not.
|
|
5164
|
+
logoUrl: "https://assets.fayz.ai/connectors/mercadopago.png",
|
|
5165
|
+
category: "payments",
|
|
5166
|
+
// Pasted keys today. Unlike Vindi — whose consent screen only LOOKS like OAuth
|
|
5167
|
+
// — Mercado Pago has a real merchant OAuth, and it is the right end state: one
|
|
5168
|
+
// platform application, each seller authorizing it, `marketplace_fee` taken on
|
|
5169
|
+
// the charge instead of invoiced afterwards. What it needs is the redirect URI
|
|
5170
|
+
// registered against the platform broker, which is a change in ymaia and not
|
|
5171
|
+
// here. The same slug is already declared by plugin-financial with
|
|
5172
|
+
// `authKind: 'oauth'`, and because a credential is keyed by slug + tenant, a
|
|
5173
|
+
// store that authorizes there is redeemable from here the day that lands.
|
|
5174
|
+
authKind: "api-key",
|
|
5175
|
+
// The charge is opened in the buyer's request path; the notification and the
|
|
5176
|
+
// refund are outbound sync work.
|
|
5177
|
+
planes: ["request", "sync"],
|
|
5178
|
+
direction: "outbound",
|
|
5179
|
+
docsUrl: "https://www.mercadopago.com.br/developers/pt/docs/checkout-api/landing",
|
|
5180
|
+
providerInstall: { url: "https://www.mercadopago.com.br/developers/panel/app" },
|
|
5181
|
+
// Grounded in what the request plane does, and padded with nothing: the total
|
|
5182
|
+
// is recomputed server-side from the order before the charge is opened, which
|
|
5183
|
+
// is a read of the order and never a write of it; the charge and its
|
|
5184
|
+
// confirmation are the write.
|
|
5185
|
+
permissions: [
|
|
5186
|
+
{
|
|
5187
|
+
kind: "data",
|
|
5188
|
+
resource: "order",
|
|
5189
|
+
actions: ["read"],
|
|
5190
|
+
reason: "Ler o pedido para recalcular o valor no servidor antes de abrir a cobran\xE7a \u2014 o total nunca vem do navegador do cliente.",
|
|
5191
|
+
sensitive: true
|
|
5192
|
+
},
|
|
5193
|
+
{
|
|
5194
|
+
kind: "data",
|
|
5195
|
+
resource: "payment",
|
|
5196
|
+
actions: ["write"],
|
|
5197
|
+
reason: "Registrar aqui a cobran\xE7a aberta e a confirma\xE7\xE3o do pagamento. O n\xFAmero do cart\xE3o n\xE3o passa por este app \u2014 ele \xE9 digitado num formul\xE1rio do pr\xF3prio Mercado Pago.",
|
|
5198
|
+
sensitive: true
|
|
5199
|
+
}
|
|
5200
|
+
],
|
|
5201
|
+
latencyBudgetMs: MERCADOPAGO_LATENCY_BUDGET_MS,
|
|
5202
|
+
// A gateway that does not answer must stop the sale: an order that reads as
|
|
5203
|
+
// confirmed without a payment is worse than a refused checkout. Same verdict
|
|
5204
|
+
// as the other four gateways, opposite to the carrier next door.
|
|
5205
|
+
onFailure: "block",
|
|
5206
|
+
fields: MERCADOPAGO_FIELDS,
|
|
5207
|
+
invokedAt: [
|
|
5208
|
+
{
|
|
5209
|
+
key: "shop.checkout.session_open",
|
|
5210
|
+
label: "O cliente finaliza a compra e a cobran\xE7a \xE9 aberta no Mercado Pago (QR do Pix ou cart\xE3o digitado na pr\xF3pria loja)",
|
|
5211
|
+
touches: ["order", "payment"]
|
|
5212
|
+
},
|
|
5213
|
+
{
|
|
5214
|
+
key: "shop.payment.capture",
|
|
5215
|
+
label: "O Mercado Pago avisa que o pagamento caiu, a cobran\xE7a \xE9 reconsultada e o pedido \xE9 liberado",
|
|
5216
|
+
plane: "sync",
|
|
5217
|
+
trigger: "webhook",
|
|
5218
|
+
touches: ["payment"]
|
|
5219
|
+
}
|
|
5220
|
+
],
|
|
5221
|
+
// Read from the row the list already batched, so a screen with N connectors
|
|
5222
|
+
// costs one plg_connections request rather than N.
|
|
5223
|
+
//
|
|
5224
|
+
// No `settingsStatus`: this connector has nothing to configure, so a
|
|
5225
|
+
// connection either has its credential pair or does not. "Conectado, falta
|
|
5226
|
+
// configurar" would be a state that can never be true here.
|
|
5227
|
+
...connectionStatus(
|
|
5228
|
+
MERCADOPAGO_CONNECTOR_ID,
|
|
5229
|
+
(connection) => ({
|
|
5230
|
+
connected: !!connection?.active,
|
|
5231
|
+
detail: connectionDetail2(connection?.settings),
|
|
5232
|
+
plane: "request"
|
|
5233
|
+
}),
|
|
5234
|
+
{ store: () => store6 }
|
|
5235
|
+
),
|
|
5236
|
+
...connectionSettings(store6),
|
|
5237
|
+
// Reached only AFTER the credential sink accepted the pair — that ordering is
|
|
5238
|
+
// what makes `active = true` mean "credentials were placed for this store"
|
|
5239
|
+
// rather than "somebody pressed a button". The secret never arrives here: the
|
|
5240
|
+
// hub split it out before calling this.
|
|
5241
|
+
//
|
|
5242
|
+
// No `testConnection`: the probe is handed the non-secret half, and a public
|
|
5243
|
+
// key alone cannot open an order. Testing belongs to the edge function that
|
|
5244
|
+
// holds the credential.
|
|
5245
|
+
saveConnection: async () => {
|
|
5246
|
+
await store6.save({ active: true });
|
|
5247
|
+
},
|
|
5248
|
+
disconnect: () => store6.disconnect(),
|
|
5249
|
+
ExtraPanel: MercadoPagoExtraPanel
|
|
5250
|
+
};
|
|
4270
5251
|
var KIND_META = {
|
|
4271
5252
|
online_store: { label: "Loja Online", icon: Globe, hint: "Seu storefront fayz \u2014 a vitrine p\xFAblica da loja.", placeholder: "https://minhaloja.com.br/" },
|
|
4272
5253
|
whatsapp: { label: "WhatsApp", icon: MessageCircle, hint: "Link direto para o WhatsApp Business.", placeholder: "https://wa.me/5521999999999" },
|
|
@@ -4284,12 +5265,12 @@ function notifyChannelsChanged() {
|
|
|
4284
5265
|
if (typeof window !== "undefined") window.dispatchEvent(new Event("fayz:channels-changed"));
|
|
4285
5266
|
}
|
|
4286
5267
|
function ChannelsPage({ resolveProvider }) {
|
|
4287
|
-
const provider =
|
|
5268
|
+
const provider = React9.useMemo(() => resolve2(resolveProvider), [resolveProvider]);
|
|
4288
5269
|
const supported = typeof provider.listSalesChannels === "function";
|
|
4289
|
-
const [channels, setChannels] =
|
|
4290
|
-
const [loading, setLoading] =
|
|
4291
|
-
const [adding, setAdding] =
|
|
4292
|
-
const reload =
|
|
5270
|
+
const [channels, setChannels] = React9.useState([]);
|
|
5271
|
+
const [loading, setLoading] = React9.useState(true);
|
|
5272
|
+
const [adding, setAdding] = React9.useState(null);
|
|
5273
|
+
const reload = React9.useCallback(async () => {
|
|
4293
5274
|
if (!provider.listSalesChannels) {
|
|
4294
5275
|
setLoading(false);
|
|
4295
5276
|
return;
|
|
@@ -4307,7 +5288,7 @@ function ChannelsPage({ resolveProvider }) {
|
|
|
4307
5288
|
setLoading(false);
|
|
4308
5289
|
}
|
|
4309
5290
|
}, [provider]);
|
|
4310
|
-
|
|
5291
|
+
React9.useEffect(() => {
|
|
4311
5292
|
void reload();
|
|
4312
5293
|
}, [reload]);
|
|
4313
5294
|
const patch = async (id, input) => {
|
|
@@ -4363,7 +5344,7 @@ function ChannelsPage({ resolveProvider }) {
|
|
|
4363
5344
|
] });
|
|
4364
5345
|
}
|
|
4365
5346
|
function OnlineStoreCard({ channel, onSave, onToggle }) {
|
|
4366
|
-
const [url, setUrl] =
|
|
5347
|
+
const [url, setUrl] = React9.useState(channel.url ?? "");
|
|
4367
5348
|
const dirty = (channel.url ?? "") !== url;
|
|
4368
5349
|
const meta = KIND_META.online_store;
|
|
4369
5350
|
return /* @__PURE__ */ jsx("div", { className: "rounded-card border border-border bg-card p-5", children: /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-3", children: [
|
|
@@ -4389,8 +5370,8 @@ function OnlineStoreCard({ channel, onSave, onToggle }) {
|
|
|
4389
5370
|
] }) });
|
|
4390
5371
|
}
|
|
4391
5372
|
function ChannelRow({ channel, onSave, onToggle, onRemove }) {
|
|
4392
|
-
const [url, setUrl] =
|
|
4393
|
-
const [editing, setEditing] =
|
|
5373
|
+
const [url, setUrl] = React9.useState(channel.url ?? "");
|
|
5374
|
+
const [editing, setEditing] = React9.useState(false);
|
|
4394
5375
|
const meta = KIND_META[channel.kind] ?? KIND_META.custom;
|
|
4395
5376
|
const Icon = meta.icon;
|
|
4396
5377
|
return /* @__PURE__ */ jsx("div", { className: "rounded-card border border-border bg-card p-3", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
|
|
@@ -4418,9 +5399,9 @@ function ChannelRow({ channel, onSave, onToggle, onRemove }) {
|
|
|
4418
5399
|
}
|
|
4419
5400
|
function AddChannelForm({ kind, onKind, onCancel, onAdd }) {
|
|
4420
5401
|
const meta = KIND_META[kind];
|
|
4421
|
-
const [label, setLabel] =
|
|
4422
|
-
const [url, setUrl] =
|
|
4423
|
-
|
|
5402
|
+
const [label, setLabel] = React9.useState(meta.label);
|
|
5403
|
+
const [url, setUrl] = React9.useState("");
|
|
5404
|
+
React9.useEffect(() => {
|
|
4424
5405
|
setLabel(KIND_META[kind].label);
|
|
4425
5406
|
}, [kind]);
|
|
4426
5407
|
return /* @__PURE__ */ jsxs("div", { className: "mt-3 rounded-card border border-border bg-card p-4", children: [
|
|
@@ -4472,17 +5453,17 @@ function formatSize(bytes) {
|
|
|
4472
5453
|
return `${(bytes / 1024 ** i).toFixed(i === 0 ? 0 : 1)} ${units[i]}`;
|
|
4473
5454
|
}
|
|
4474
5455
|
function ContentPage({ resolveProvider }) {
|
|
4475
|
-
const provider =
|
|
5456
|
+
const provider = React9.useMemo(() => resolve3(resolveProvider), [resolveProvider]);
|
|
4476
5457
|
const supported = typeof provider.listMediaFiles === "function";
|
|
4477
|
-
const [files, setFiles] =
|
|
4478
|
-
const [tab, setTab] =
|
|
4479
|
-
const [uploading, setUploading] =
|
|
4480
|
-
const [pendingDelete, setPendingDelete] =
|
|
4481
|
-
const [deleting, setDeleting] =
|
|
4482
|
-
const [replacing, setReplacing] =
|
|
4483
|
-
const [openId, setOpenId] =
|
|
4484
|
-
const [dimensions, setDimensions] =
|
|
4485
|
-
const reload =
|
|
5458
|
+
const [files, setFiles] = React9.useState(null);
|
|
5459
|
+
const [tab, setTab] = React9.useState("all");
|
|
5460
|
+
const [uploading, setUploading] = React9.useState(false);
|
|
5461
|
+
const [pendingDelete, setPendingDelete] = React9.useState(null);
|
|
5462
|
+
const [deleting, setDeleting] = React9.useState(false);
|
|
5463
|
+
const [replacing, setReplacing] = React9.useState(false);
|
|
5464
|
+
const [openId, setOpenId] = React9.useState(null);
|
|
5465
|
+
const [dimensions, setDimensions] = React9.useState({});
|
|
5466
|
+
const reload = React9.useCallback(async () => {
|
|
4486
5467
|
if (!provider.listMediaFiles) {
|
|
4487
5468
|
setFiles([]);
|
|
4488
5469
|
return;
|
|
@@ -4494,10 +5475,10 @@ function ContentPage({ resolveProvider }) {
|
|
|
4494
5475
|
setFiles([]);
|
|
4495
5476
|
}
|
|
4496
5477
|
}, [provider]);
|
|
4497
|
-
|
|
5478
|
+
React9.useEffect(() => {
|
|
4498
5479
|
void reload();
|
|
4499
5480
|
}, [reload]);
|
|
4500
|
-
const measure =
|
|
5481
|
+
const measure = React9.useCallback((id, d) => {
|
|
4501
5482
|
setDimensions((prev) => prev[id] ? prev : { ...prev, [id]: d });
|
|
4502
5483
|
}, []);
|
|
4503
5484
|
const handleUpload = async (picked) => {
|
|
@@ -4687,7 +5668,7 @@ function SectionTitle({ title, count }) {
|
|
|
4687
5668
|
] });
|
|
4688
5669
|
}
|
|
4689
5670
|
function UploadButton({ uploading, onPick }) {
|
|
4690
|
-
const inputRef =
|
|
5671
|
+
const inputRef = React9.useRef(null);
|
|
4691
5672
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
4692
5673
|
/* @__PURE__ */ jsx(
|
|
4693
5674
|
"input",
|
|
@@ -4767,8 +5748,8 @@ function MediaDetail({
|
|
|
4767
5748
|
onDelete,
|
|
4768
5749
|
replacing
|
|
4769
5750
|
}) {
|
|
4770
|
-
const replaceRef =
|
|
4771
|
-
|
|
5751
|
+
const replaceRef = React9.useRef(null);
|
|
5752
|
+
React9.useEffect(() => {
|
|
4772
5753
|
function onKey(e) {
|
|
4773
5754
|
if (e.key === "ArrowLeft") onNavigate(-1);
|
|
4774
5755
|
if (e.key === "ArrowRight") onNavigate(1);
|
|
@@ -4995,7 +5976,7 @@ async function pendingCustomers() {
|
|
|
4995
5976
|
if (!tenantId || !supabase) return null;
|
|
4996
5977
|
const { data: linked } = await supabase.from("plg_shop_customer_links").select("customer_id").eq("tenant_id", tenantId).eq("source", "bling");
|
|
4997
5978
|
const already = new Set((linked ?? []).map((row) => row.customer_id));
|
|
4998
|
-
const { data, error } = await supabase.from("
|
|
5979
|
+
const { data, error } = await supabase.from("v_shop_customers").select("id").eq("tenant_id", tenantId).gt("orders_count", 0);
|
|
4999
5980
|
if (error) return null;
|
|
5000
5981
|
return (data ?? []).filter((row) => !already.has(row.id)).length;
|
|
5001
5982
|
}
|
|
@@ -5005,7 +5986,7 @@ async function pendingOrders() {
|
|
|
5005
5986
|
if (!tenantId || !supabase) return null;
|
|
5006
5987
|
const { data: linked } = await supabase.from("plg_shop_order_links").select("order_id").eq("tenant_id", tenantId).eq("source", "bling").eq("kind", "sales_order");
|
|
5007
5988
|
const already = new Set((linked ?? []).map((row) => row.order_id));
|
|
5008
|
-
const { data, error } = await supabase.from("
|
|
5989
|
+
const { data, error } = await supabase.from("v_shop_orders").select("id").eq("tenant_id", tenantId).neq("status", "cancelled");
|
|
5009
5990
|
if (error) return null;
|
|
5010
5991
|
return (data ?? []).filter((row) => !already.has(row.id)).length;
|
|
5011
5992
|
}
|
|
@@ -5013,7 +5994,7 @@ async function listOrderDeliveries(limit = 10) {
|
|
|
5013
5994
|
const tenantId = getShopTenantId();
|
|
5014
5995
|
const supabase = getSupabaseClientOptional();
|
|
5015
5996
|
if (!tenantId || !supabase) return [];
|
|
5016
|
-
const { data: orders } = await supabase.from("
|
|
5997
|
+
const { data: orders } = await supabase.from("v_shop_orders").select("id,order_number,customer_name,customer_document,paid_at,created_at").eq("tenant_id", tenantId).neq("status", "cancelled").order("created_at", { ascending: false }).limit(limit);
|
|
5017
5998
|
const rows = orders ?? [];
|
|
5018
5999
|
if (rows.length === 0) return [];
|
|
5019
6000
|
const { data: filed } = await supabase.from("plg_shop_order_links").select("order_id,external_id,external_number,synced_at").eq("tenant_id", tenantId).eq("source", "bling").eq("kind", "sales_order").in("order_id", rows.map((row) => row.id));
|
|
@@ -5537,7 +6518,7 @@ async function pendingOrders2() {
|
|
|
5537
6518
|
if (!tenantId || !supabase) return null;
|
|
5538
6519
|
const { data: linked } = await supabase.from("plg_shop_order_links").select("order_id").eq("tenant_id", tenantId).eq("source", "neochef").eq("kind", "kitchen_order");
|
|
5539
6520
|
const already = new Set((linked ?? []).map((row) => row.order_id));
|
|
5540
|
-
const { data, error } = await supabase.from("
|
|
6521
|
+
const { data, error } = await supabase.from("v_shop_orders").select("id").eq("tenant_id", tenantId).neq("status", "cancelled");
|
|
5541
6522
|
if (error) return null;
|
|
5542
6523
|
return (data ?? []).filter((row) => !already.has(row.id)).length;
|
|
5543
6524
|
}
|
|
@@ -5545,7 +6526,7 @@ async function listOrderDeliveries2(limit = 10) {
|
|
|
5545
6526
|
const tenantId = getShopTenantId();
|
|
5546
6527
|
const supabase = getSupabaseClientOptional();
|
|
5547
6528
|
if (!tenantId || !supabase) return [];
|
|
5548
|
-
const { data: orders } = await supabase.from("
|
|
6529
|
+
const { data: orders } = await supabase.from("v_shop_orders").select("id,order_number,paid_at,created_at,customer_name,status").eq("tenant_id", tenantId).order("created_at", { ascending: false }).limit(limit);
|
|
5549
6530
|
const rows = orders ?? [];
|
|
5550
6531
|
if (rows.length === 0) return [];
|
|
5551
6532
|
const { data: filed } = await supabase.from("plg_shop_order_links").select("order_id,external_id,external_number,synced_at").eq("tenant_id", tenantId).eq("source", "neochef").eq("kind", "kitchen_order").in("order_id", rows.map((row) => row.id));
|
|
@@ -6623,7 +7604,7 @@ function buildShopAnalyticsCards() {
|
|
|
6623
7604
|
}
|
|
6624
7605
|
|
|
6625
7606
|
// src/index.ts
|
|
6626
|
-
var ShopPage =
|
|
7607
|
+
var ShopPage = React9.lazy(() => import('./ShopPage-QFIM2HDU.js').then((m) => ({ default: m.ShopPage })));
|
|
6627
7608
|
function createShopPlugin(options) {
|
|
6628
7609
|
const curr = { code: "BRL", locale: "pt-BR", symbol: "R$", ...options?.currency };
|
|
6629
7610
|
setShopTenantResolver(() => useOrganizationStore.getState().currentOrg?.id);
|
|
@@ -6632,10 +7613,10 @@ function createShopPlugin(options) {
|
|
|
6632
7613
|
registerEntityPath("shop_customer", "/orders/customers");
|
|
6633
7614
|
registerPersonLookup("shop_customer", shopCustomerLookup(() => resolveProvider()));
|
|
6634
7615
|
const pageFor = (section) => {
|
|
6635
|
-
const Wrapped = () =>
|
|
6636
|
-
|
|
7616
|
+
const Wrapped = () => React9.createElement(
|
|
7617
|
+
React9.Suspense,
|
|
6637
7618
|
{ fallback: null },
|
|
6638
|
-
|
|
7619
|
+
React9.createElement(
|
|
6639
7620
|
ShopPage,
|
|
6640
7621
|
{ provider: options?.provider, section }
|
|
6641
7622
|
)
|
|
@@ -6761,6 +7742,29 @@ function createShopPlugin(options) {
|
|
|
6761
7742
|
const DiscountsCrudPage = createCrudPage(buildShopDiscountEntity(), {
|
|
6762
7743
|
dataProvider: createShopDiscountDataProvider(resolveProvider),
|
|
6763
7744
|
feature: "shop",
|
|
7745
|
+
// O cupom vive atrás do provider de commerce, sem tabela que as primitivas
|
|
7746
|
+
// de dado do agente alcancem — então a lista empresta o próprio store, como
|
|
7747
|
+
// a de produtos faz (ver CrudAgentSurface).
|
|
7748
|
+
aiCreate: {
|
|
7749
|
+
hint: "Descreva o cupom\u2026",
|
|
7750
|
+
description: "Descreva a promo\xE7\xE3o e eu crio o cupom.",
|
|
7751
|
+
// O código não se pergunta: ver DISCOUNT_CREATE_INSTRUCTIONS. `code` é
|
|
7752
|
+
// único no banco (0001:208), então a lista publica os que já existem e a
|
|
7753
|
+
// recusa do índice volta legível se dois turnos escolherem o mesmo.
|
|
7754
|
+
instructions: DISCOUNT_CREATE_INSTRUCTIONS,
|
|
7755
|
+
uniqueFields: ["code"],
|
|
7756
|
+
intro: {
|
|
7757
|
+
role: "operations",
|
|
7758
|
+
persona: "Cupons e promo\xE7\xF5es",
|
|
7759
|
+
pitch: "Diga a promo\xE7\xE3o que voc\xEA quer fazer.",
|
|
7760
|
+
description: "Frete gr\xE1tis, percentual ou valor fixo \u2014 diga a regra do jeito que voc\xEA falaria com um cliente, e eu escrevo o c\xF3digo, o tipo, o m\xEDnimo e a validade. S\xF3 pergunto o tamanho do desconto quando voc\xEA n\xE3o disser.",
|
|
7761
|
+
suggestions: [
|
|
7762
|
+
{ label: "Frete gr\xE1tis at\xE9 o fim da semana" },
|
|
7763
|
+
{ label: "10% na primeira compra, uma vez por cliente" },
|
|
7764
|
+
{ label: "R$ 50 off acima de R$ 300, limite de 100 usos" }
|
|
7765
|
+
]
|
|
7766
|
+
}
|
|
7767
|
+
},
|
|
6764
7768
|
stats: (items, total) => [
|
|
6765
7769
|
{ id: "total", label: "Total de cupons", value: total, icon: Tag },
|
|
6766
7770
|
{ id: "active", label: "Ativos agora", value: items?.filter((d) => d.status === "active").length ?? 0, icon: Eye },
|
|
@@ -6791,7 +7795,7 @@ function createShopPlugin(options) {
|
|
|
6791
7795
|
dataProvider: createShopGiftCardDataProvider(resolveProvider),
|
|
6792
7796
|
feature: "shop",
|
|
6793
7797
|
stats: (items, total) => {
|
|
6794
|
-
const
|
|
7798
|
+
const money2 = (v) => new Intl.NumberFormat(curr.locale, { style: "currency", currency: curr.code }).format(v);
|
|
6795
7799
|
const active = items?.filter((g) => g.status === "active") ?? [];
|
|
6796
7800
|
return [
|
|
6797
7801
|
{ id: "total", label: "Cart\xF5es emitidos", value: total, icon: CreditCard },
|
|
@@ -6802,20 +7806,20 @@ function createShopPlugin(options) {
|
|
|
6802
7806
|
id: "outstanding",
|
|
6803
7807
|
label: "Saldo em aberto",
|
|
6804
7808
|
icon: CircleDollarSign,
|
|
6805
|
-
value:
|
|
7809
|
+
value: money2(active.reduce((sum, g) => sum + (g.balance ?? 0), 0))
|
|
6806
7810
|
},
|
|
6807
7811
|
{
|
|
6808
7812
|
id: "issued",
|
|
6809
7813
|
label: "Valor emitido",
|
|
6810
7814
|
icon: TicketCheck,
|
|
6811
|
-
value:
|
|
7815
|
+
value: money2(items?.reduce((sum, g) => sum + (g.initialBalance ?? 0), 0) ?? 0)
|
|
6812
7816
|
}
|
|
6813
7817
|
];
|
|
6814
7818
|
}
|
|
6815
7819
|
});
|
|
6816
|
-
const ContentPageWrapped = () =>
|
|
7820
|
+
const ContentPageWrapped = () => React9.createElement(ContentPage, { resolveProvider: options?.provider });
|
|
6817
7821
|
ContentPageWrapped.displayName = "ContentPage";
|
|
6818
|
-
const ChannelsPageWrapped = () =>
|
|
7822
|
+
const ChannelsPageWrapped = () => React9.createElement(ChannelsPage, { resolveProvider: options?.provider });
|
|
6819
7823
|
ChannelsPageWrapped.displayName = "ChannelsPage";
|
|
6820
7824
|
const navSection = options?.navSection ?? "main";
|
|
6821
7825
|
const navPosition = options?.navPosition ?? 6;
|
|
@@ -6905,7 +7909,7 @@ function createShopPlugin(options) {
|
|
|
6905
7909
|
];
|
|
6906
7910
|
const redirectTo = (target) => {
|
|
6907
7911
|
const Redirect = () => {
|
|
6908
|
-
|
|
7912
|
+
React9.useEffect(() => {
|
|
6909
7913
|
adminNavigateTo(target);
|
|
6910
7914
|
}, []);
|
|
6911
7915
|
return null;
|
|
@@ -6937,6 +7941,10 @@ function createShopPlugin(options) {
|
|
|
6937
7941
|
return {
|
|
6938
7942
|
id: "shop",
|
|
6939
7943
|
defaultAgentRole: "operations",
|
|
7944
|
+
// O que a loja pede de um tenant novo, na ordem em que faz diferença.
|
|
7945
|
+
// Vira linha de tarefa, aparece no checklist da trilha e é o roteiro que o
|
|
7946
|
+
// assistente segue — ver ./lib/onboarding.ts.
|
|
7947
|
+
onboarding: { program: buildShopOnboarding() },
|
|
6940
7948
|
name: options?.navLabel ?? "Shop",
|
|
6941
7949
|
icon: "ShoppingBag",
|
|
6942
7950
|
version: "1.0.0",
|
|
@@ -7029,7 +8037,7 @@ function createShopPlugin(options) {
|
|
|
7029
8037
|
label: label("store", "Loja"),
|
|
7030
8038
|
icon: "ShoppingBag",
|
|
7031
8039
|
group: COMMERCE_GROUP,
|
|
7032
|
-
component: settingsTab("StoreGeneralSettings", () =>
|
|
8040
|
+
component: settingsTab("StoreGeneralSettings", () => React9.createElement(StoreGeneralSettings, { currency: curr, provider: options?.provider })),
|
|
7033
8041
|
order: 30,
|
|
7034
8042
|
permission: READ
|
|
7035
8043
|
},
|
|
@@ -7038,7 +8046,7 @@ function createShopPlugin(options) {
|
|
|
7038
8046
|
label: "Pagamentos",
|
|
7039
8047
|
icon: "CreditCard",
|
|
7040
8048
|
group: COMMERCE_GROUP,
|
|
7041
|
-
component: settingsTab("PaymentsSettings", () =>
|
|
8049
|
+
component: settingsTab("PaymentsSettings", () => React9.createElement(PaymentsSettings, { provider: options?.provider })),
|
|
7042
8050
|
order: 31,
|
|
7043
8051
|
permission: READ
|
|
7044
8052
|
},
|
|
@@ -7047,7 +8055,7 @@ function createShopPlugin(options) {
|
|
|
7047
8055
|
label: "Checkout",
|
|
7048
8056
|
icon: "ShoppingCart",
|
|
7049
8057
|
group: COMMERCE_GROUP,
|
|
7050
|
-
component: settingsTab("CheckoutSettings", () =>
|
|
8058
|
+
component: settingsTab("CheckoutSettings", () => React9.createElement(CheckoutSettings, { provider: options?.provider })),
|
|
7051
8059
|
order: 32,
|
|
7052
8060
|
permission: READ
|
|
7053
8061
|
},
|
|
@@ -7056,7 +8064,7 @@ function createShopPlugin(options) {
|
|
|
7056
8064
|
label: label("shipping", "Entrega"),
|
|
7057
8065
|
icon: "Truck",
|
|
7058
8066
|
group: COMMERCE_GROUP,
|
|
7059
|
-
component: settingsTab("ShippingSettings", () =>
|
|
8067
|
+
component: settingsTab("ShippingSettings", () => React9.createElement(ShippingSettings, { provider: options?.provider })),
|
|
7060
8068
|
order: 33,
|
|
7061
8069
|
permission: READ
|
|
7062
8070
|
},
|
|
@@ -7065,7 +8073,7 @@ function createShopPlugin(options) {
|
|
|
7065
8073
|
label: "Fulfillment",
|
|
7066
8074
|
icon: "PackageCheck",
|
|
7067
8075
|
group: COMMERCE_GROUP,
|
|
7068
|
-
component: settingsTab("FulfillmentSettings", () =>
|
|
8076
|
+
component: settingsTab("FulfillmentSettings", () => React9.createElement(FulfillmentSettings, { provider: options?.provider })),
|
|
7069
8077
|
order: 34,
|
|
7070
8078
|
permission: READ
|
|
7071
8079
|
}
|
|
@@ -7078,27 +8086,33 @@ function createShopPlugin(options) {
|
|
|
7078
8086
|
// ones — nobody is waiting on them, and what they declare instead is a call
|
|
7079
8087
|
// budget and a cursor.
|
|
7080
8088
|
//
|
|
7081
|
-
//
|
|
7082
|
-
//
|
|
7083
|
-
//
|
|
7084
|
-
//
|
|
8089
|
+
// Four gateways on the shelf is the point: the store picks one, and each
|
|
8090
|
+
// one arriving changed no contract — only how it authorizes. Mercado Pago
|
|
8091
|
+
// pushes that furthest, because it is the first whose CARD is paid inside
|
|
8092
|
+
// the store: it tokenises in the browser, so there is no PAN for an edge
|
|
8093
|
+
// function to hold and no redirect to send the buyer through. The shelf
|
|
8094
|
+
// absorbed that too, as a capability the checkout reads rather than a branch
|
|
8095
|
+
// it hard-codes. NeoChef makes the same point about the back office, and one
|
|
8096
|
+
// better: its provider is another Fayz app, and it still fits without a
|
|
8097
|
+
// special case.
|
|
7085
8098
|
connectors: [
|
|
7086
8099
|
melhorEnvioConnectorDef,
|
|
7087
8100
|
appmaxConnectorDef,
|
|
7088
8101
|
abacatePayConnectorDef,
|
|
7089
8102
|
stripeConnectConnectorDef,
|
|
7090
8103
|
vindiConnectorDef,
|
|
8104
|
+
mercadoPagoConnectorDef,
|
|
7091
8105
|
blingConnectorDef,
|
|
7092
8106
|
neoChefConnectorDef
|
|
7093
8107
|
],
|
|
7094
|
-
// What having a store makes plugable — the
|
|
8108
|
+
// What having a store makes plugable — the ones above plus the ones still
|
|
7095
8109
|
// to be built. Carried over from the Fayz plugin page's "Integra com" row,
|
|
7096
8110
|
// minus Nuvemshop: the platform catalogue has no row for it, and a slug the
|
|
7097
8111
|
// directory cannot back is refused rather than shown.
|
|
7098
|
-
integrations: ["shopify", "stripe"
|
|
8112
|
+
integrations: ["shopify", "stripe"]
|
|
7099
8113
|
};
|
|
7100
8114
|
}
|
|
7101
8115
|
|
|
7102
|
-
export { ABACATEPAY_CONNECTOR_ID, ABACATEPAY_LATENCY_BUDGET_MS, APPMAX_CONNECTOR_ID, APPMAX_LATENCY_BUDGET_MS, BLING_CONNECTOR_ID, BLING_MARKETPLACE_SOURCE, BLING_MAX_WINDOW_MS, BLING_PAGE_SIZE, BLING_RATE_LIMIT, BLING_SAMPLE_SIZE, BLING_SIGNATURE_HEADER, BLING_TOKEN_LEAD_MS, BLING_WEBHOOK_DEADLINE_MS, BLING_WINDOW_OVERLAP_MS, MELHOR_ENVIO_CONNECTOR_ID, MELHOR_ENVIO_LATENCY_BUDGET_MS, NEOCHEF_CONNECTOR_ID, NEOCHEF_CURSOR_OVERLAP_MS, NEOCHEF_FUNCTION, NEOCHEF_MARK, NEOCHEF_PAGE_SIZE, NEOCHEF_RATE_LIMIT, NEOCHEF_SAMPLE_SIZE, NEOCHEF_TOKEN_LEAD_MS, STRIPE_CONNECT_CONNECTOR_ID, STRIPE_CONNECT_LATENCY_BUDGET_MS, VINDI_CONNECTOR_ID, VINDI_LATENCY_BUDGET_MS, abacatePayConnectorDef, acceptWebhook, advanceNeoChefCursor, appmaxConnectorDef, blingConnectionStore, blingConnectorDef, buildShopAnalyticsCards, createShopPlugin, firstWindow, listOrderDeliveries2 as listOrderDeliveries, melhorEnvioConnectorDef, neoChefConnectionStore, neoChefConnectorDef, orphanConnection, pendingOrders2 as pendingOrders, pullCatalogue2 as pullCatalogue, pullProducts, pushOrders2 as pushOrders, reconcile2 as reconcile, routeCallback, sampleProducts, stripeConnectConnectorDef, testNeoChef, toMirroredNeoChefProduct, toMirroredProduct, toNeoChefOrder, vindiConnectorDef, windowFor };
|
|
8116
|
+
export { ABACATEPAY_CONNECTOR_ID, ABACATEPAY_LATENCY_BUDGET_MS, APPMAX_CONNECTOR_ID, APPMAX_LATENCY_BUDGET_MS, BLING_CONNECTOR_ID, BLING_MARKETPLACE_SOURCE, BLING_MAX_WINDOW_MS, BLING_PAGE_SIZE, BLING_RATE_LIMIT, BLING_SAMPLE_SIZE, BLING_SIGNATURE_HEADER, BLING_TOKEN_LEAD_MS, BLING_WEBHOOK_DEADLINE_MS, BLING_WINDOW_OVERLAP_MS, MELHOR_ENVIO_CONNECTOR_ID, MELHOR_ENVIO_LATENCY_BUDGET_MS, MERCADOPAGO_CONNECTOR_ID, MERCADOPAGO_LATENCY_BUDGET_MS, NEOCHEF_CONNECTOR_ID, NEOCHEF_CURSOR_OVERLAP_MS, NEOCHEF_FUNCTION, NEOCHEF_MARK, NEOCHEF_PAGE_SIZE, NEOCHEF_RATE_LIMIT, NEOCHEF_SAMPLE_SIZE, NEOCHEF_TOKEN_LEAD_MS, STRIPE_CONNECT_CONNECTOR_ID, STRIPE_CONNECT_LATENCY_BUDGET_MS, VINDI_CONNECTOR_ID, VINDI_LATENCY_BUDGET_MS, abacatePayConnectorDef, acceptWebhook, advanceNeoChefCursor, appmaxConnectorDef, blingConnectionStore, blingConnectorDef, buildShopAnalyticsCards, createShopPlugin, firstWindow, listOrderDeliveries2 as listOrderDeliveries, melhorEnvioConnectorDef, mercadoPagoConnectorDef, neoChefConnectionStore, neoChefConnectorDef, orphanConnection, pendingOrders2 as pendingOrders, pullCatalogue2 as pullCatalogue, pullProducts, pushOrders2 as pushOrders, reconcile2 as reconcile, routeCallback, sampleProducts, stripeConnectConnectorDef, testNeoChef, toMirroredNeoChefProduct, toMirroredProduct, toNeoChefOrder, vindiConnectorDef, windowFor };
|
|
7103
8117
|
//# sourceMappingURL=index.js.map
|
|
7104
8118
|
//# sourceMappingURL=index.js.map
|