@fayz-ai/plugin-shop 0.11.2 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/dist/{ShopPage-YB5ZWECJ.js → ShopPage-QFIM2HDU.js} +1516 -406
  2. package/dist/ShopPage-QFIM2HDU.js.map +1 -0
  3. package/dist/{chunk-HAKXHHAG.js → chunk-2VMTPRGC.js} +202 -11
  4. package/dist/chunk-2VMTPRGC.js.map +1 -0
  5. package/dist/index.d.ts +2 -0
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +1814 -442
  8. package/dist/index.js.map +1 -1
  9. package/dist/integrations/abacatepay/connectorDef.d.ts.map +1 -1
  10. package/dist/integrations/abacatepay/data.d.ts +40 -0
  11. package/dist/integrations/abacatepay/data.d.ts.map +1 -0
  12. package/dist/integrations/abacatepay/limits.d.ts +26 -0
  13. package/dist/integrations/abacatepay/limits.d.ts.map +1 -0
  14. package/dist/integrations/abacatepay/sync.d.ts +88 -0
  15. package/dist/integrations/abacatepay/sync.d.ts.map +1 -0
  16. package/dist/integrations/bling/data.d.ts.map +1 -1
  17. package/dist/integrations/mercadopago/connectorDef.d.ts +41 -0
  18. package/dist/integrations/mercadopago/connectorDef.d.ts.map +1 -0
  19. package/dist/integrations/neochef/connectorDef.d.ts +12 -0
  20. package/dist/integrations/neochef/connectorDef.d.ts.map +1 -0
  21. package/dist/integrations/neochef/data.d.ts +54 -0
  22. package/dist/integrations/neochef/data.d.ts.map +1 -0
  23. package/dist/integrations/neochef/index.d.ts +8 -0
  24. package/dist/integrations/neochef/index.d.ts.map +1 -0
  25. package/dist/integrations/neochef/limits.d.ts +26 -0
  26. package/dist/integrations/neochef/limits.d.ts.map +1 -0
  27. package/dist/integrations/neochef/mapping.d.ts +31 -0
  28. package/dist/integrations/neochef/mapping.d.ts.map +1 -0
  29. package/dist/integrations/neochef/mark.d.ts +21 -0
  30. package/dist/integrations/neochef/mark.d.ts.map +1 -0
  31. package/dist/integrations/neochef/types.d.ts +71 -0
  32. package/dist/integrations/neochef/types.d.ts.map +1 -0
  33. package/dist/integrations/vindi/connectorDef.d.ts +67 -35
  34. package/dist/integrations/vindi/connectorDef.d.ts.map +1 -1
  35. package/dist/views/ContentPage.d.ts.map +1 -1
  36. package/dist/views/CustomerModal.d.ts +33 -0
  37. package/dist/views/CustomerModal.d.ts.map +1 -0
  38. package/dist/views/NewOrderCharge.d.ts +39 -3
  39. package/dist/views/NewOrderCharge.d.ts.map +1 -1
  40. package/dist/views/NewOrderPage.d.ts +10 -1
  41. package/dist/views/NewOrderPage.d.ts.map +1 -1
  42. package/dist/views/OrderDetailView.d.ts.map +1 -1
  43. package/dist/views/OrdersAgentSurface.d.ts +10 -0
  44. package/dist/views/OrdersAgentSurface.d.ts.map +1 -0
  45. package/dist/views/ProductPositionsTab.d.ts +7 -0
  46. package/dist/views/ProductPositionsTab.d.ts.map +1 -0
  47. package/dist/views/ProductStockTab.d.ts +84 -0
  48. package/dist/views/ProductStockTab.d.ts.map +1 -0
  49. package/dist/views/ShopPage.d.ts.map +1 -1
  50. package/dist/views/StockEntryModal.d.ts +32 -0
  51. package/dist/views/StockEntryModal.d.ts.map +1 -0
  52. package/dist/views/customerEntity.d.ts.map +1 -1
  53. package/dist/views/discount-agent.d.ts +2 -0
  54. package/dist/views/discount-agent.d.ts.map +1 -0
  55. package/dist/views/new-order-rules.d.ts +23 -1
  56. package/dist/views/new-order-rules.d.ts.map +1 -1
  57. package/dist/views/orders-agent.d.ts +11 -0
  58. package/dist/views/orders-agent.d.ts.map +1 -0
  59. package/dist/views/product-matching.d.ts +38 -0
  60. package/dist/views/product-matching.d.ts.map +1 -0
  61. package/dist/views/productEntity.d.ts.map +1 -1
  62. package/dist/views/stock-agent.d.ts +82 -0
  63. package/dist/views/stock-agent.d.ts.map +1 -0
  64. package/package.json +8 -6
  65. package/dist/ShopPage-YB5ZWECJ.js.map +0 -1
  66. package/dist/chunk-HAKXHHAG.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,11 +1,13 @@
1
- import { shopCustomerLookup, resolve, invalidateProductLists, listProductsDetailed, applyQuery, errorMessage, packageMeasureLabel, PACKAGES_REGISTRY, packagesRegistry, SHIPPING_PROFILES_REGISTRY, SHIPPING_ZONES_REGISTRY, buildShopRegistries, locationSummary, LOCATIONS_REGISTRY } from './chunk-HAKXHHAG.js';
2
- import React8, { useState, useEffect, useRef } from 'react';
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, 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, ClientOrdersTab, createClientOrdersProvider, 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, ChevronLeft, ChevronRight, Sparkles, 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
 
11
13
  // src/integrations/melhor-envio/labelProvider.ts
@@ -170,20 +172,20 @@ function ProductVariantsTab({ item, getProvider }) {
170
172
  const product = item;
171
173
  const shop = getProvider();
172
174
  const supported = typeof shop.getProductVariants === "function" && typeof shop.saveProductOptions === "function" && typeof shop.createProductVariant === "function" && typeof shop.updateProductVariant === "function";
173
- const [axes, setAxes] = React8.useState([]);
174
- const [variants, setVariants] = React8.useState([]);
175
- const [busy, setBusy] = React8.useState(false);
176
- const [error, setError] = React8.useState(null);
177
- const [selected, setSelected] = React8.useState(/* @__PURE__ */ new Set());
178
- const [dirty, setDirty] = React8.useState(false);
179
- const reload = React8.useCallback(async () => {
175
+ const [axes, setAxes] = React9.useState([]);
176
+ const [variants, setVariants] = React9.useState([]);
177
+ const [busy, setBusy] = React9.useState(false);
178
+ const [error, setError] = React9.useState(null);
179
+ const [selected, setSelected] = React9.useState(/* @__PURE__ */ new Set());
180
+ const [dirty, setDirty] = React9.useState(false);
181
+ const reload = React9.useCallback(async () => {
180
182
  const set = await getProvider().getProductVariants(product.id, { includeArchived: true });
181
183
  setAxes(axesFromOptions(set.options));
182
184
  setVariants(set.variants);
183
185
  setSelected(/* @__PURE__ */ new Set());
184
186
  setDirty(false);
185
187
  }, [getProvider, product.id]);
186
- React8.useEffect(() => {
188
+ React9.useEffect(() => {
187
189
  if (!supported) return;
188
190
  void reload().catch((e) => setError(message(e)));
189
191
  }, [reload, supported]);
@@ -199,7 +201,7 @@ function ProductVariantsTab({ item, getProvider }) {
199
201
  setBusy(false);
200
202
  }
201
203
  }
202
- const plan = React8.useMemo(() => planVariantMatrix(axes, variants), [axes, variants]);
204
+ const plan = React9.useMemo(() => planVariantMatrix(axes, variants), [axes, variants]);
203
205
  const axisError = validateVariantAxes(axes);
204
206
  const onlyDefault = hasOnlyDefaultVariant(variants);
205
207
  const rows = plan.rows;
@@ -300,7 +302,7 @@ function ProductVariantsTab({ item, getProvider }) {
300
302
  ] });
301
303
  }
302
304
  function AxisEditor({ axis, index, count, disabled, onChange, onMove, onRemove }) {
303
- const [draft, setDraft] = React8.useState("");
305
+ const [draft, setDraft] = React9.useState("");
304
306
  function commit(raw) {
305
307
  const values = raw.split(",").map((v) => v.trim()).filter(Boolean);
306
308
  if (values.length === 0) return;
@@ -309,7 +311,7 @@ function AxisEditor({ axis, index, count, disabled, onChange, onMove, onRemove }
309
311
  onChange({ ...axis, values: merged });
310
312
  setDraft("");
311
313
  }
312
- return /* @__PURE__ */ jsxs("li", { className: "rounded-lg border bg-card p-3", children: [
314
+ return /* @__PURE__ */ jsxs("li", { className: "rounded-card border bg-card p-3", children: [
313
315
  /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
314
316
  /* @__PURE__ */ jsx(
315
317
  "input",
@@ -447,9 +449,9 @@ function VariantRow({ row, axes, product, busy, checked, onToggle, onSave, onSto
447
449
  const variant = row.variant;
448
450
  const pending = !variant;
449
451
  const label = formatVariantCombination(row.options, axes);
450
- const commit = (field, value, previous) => {
452
+ const commit = (field2, value, previous) => {
451
453
  if (!variant || value === previous) return;
452
- onSave(variant.id, { [field]: value });
454
+ onSave(variant.id, { [field2]: value });
453
455
  };
454
456
  return /* @__PURE__ */ jsxs("tr", { className: pending ? "bg-muted/20 text-muted-foreground" : void 0, children: [
455
457
  /* @__PURE__ */ jsx("td", { className: "px-2 py-1.5", children: /* @__PURE__ */ jsx(
@@ -573,9 +575,9 @@ function VariantRow({ row, axes, product, busy, checked, onToggle, onSave, onSto
573
575
  ] });
574
576
  }
575
577
  function BulkBar({ count, disabled, onApply, onClear }) {
576
- const [price, setPrice] = React8.useState("");
577
- const [weight, setWeight] = React8.useState("");
578
- const [unit, setUnit] = React8.useState("");
578
+ const [price, setPrice] = React9.useState("");
579
+ const [weight, setWeight] = React9.useState("");
580
+ const [unit, setUnit] = React9.useState("");
579
581
  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
582
  /* @__PURE__ */ jsxs("span", { className: "font-medium", children: [
581
583
  count,
@@ -654,8 +656,8 @@ function parseNumber(raw) {
654
656
  return Number.isFinite(n) && n >= 0 ? n : null;
655
657
  }
656
658
  function NumberCell({ value, disabled, placeholder, onCommit, width = "w-20", integer }) {
657
- const [draft, setDraft] = React8.useState(value == null ? "" : String(value));
658
- React8.useEffect(() => {
659
+ const [draft, setDraft] = React9.useState(value == null ? "" : String(value));
660
+ React9.useEffect(() => {
659
661
  setDraft(value == null ? "" : String(value));
660
662
  }, [value]);
661
663
  return /* @__PURE__ */ jsx(
@@ -674,8 +676,8 @@ function NumberCell({ value, disabled, placeholder, onCommit, width = "w-20", in
674
676
  );
675
677
  }
676
678
  function TextCell({ value, disabled, placeholder, onCommit }) {
677
- const [draft, setDraft] = React8.useState(value);
678
- React8.useEffect(() => {
679
+ const [draft, setDraft] = React9.useState(value);
680
+ React9.useEffect(() => {
679
681
  setDraft(value);
680
682
  }, [value]);
681
683
  return /* @__PURE__ */ jsx(
@@ -722,6 +724,613 @@ function describePlan(plan) {
722
724
  function message(error) {
723
725
  return error instanceof Error ? error.message : String(error);
724
726
  }
727
+ var KIND_OF = {
728
+ // `sign: 0` = o razão aceita os dois lados (ajuste corrige para cima e para baixo).
729
+ entry: { kind: "receipt", sign: 1, reason: "purchase" },
730
+ exit: { kind: "adjustment", sign: -1, reason: "correction" },
731
+ adjustment: { kind: "adjustment", sign: 0, reason: "correction" },
732
+ loss: { kind: "loss", sign: -1, reason: "breakage" },
733
+ // Transferência é DOIS movimentos amarrados, com origem e destino. Metade de
734
+ // uma transferência é estoque sumindo de um lugar e não aparecendo no outro.
735
+ transfer: { kind: "transfer_out", sign: -1, reason: "transfer" }
736
+ };
737
+ function planMovement(rawType, quantity, reasonCode) {
738
+ const type = normalizeMovementType(rawType);
739
+ if (!type) {
740
+ return { error: "unknown_movement_type", message: "Use entrada, sa\xEDda, ajuste ou perda." };
741
+ }
742
+ if (type === "transfer") {
743
+ return {
744
+ error: "transfer_unsupported",
745
+ message: "Transfer\xEAncia entre locais precisa de origem e destino \u2014 ainda n\xE3o fa\xE7o por aqui."
746
+ };
747
+ }
748
+ const magnitude = Math.abs(Number(quantity) || 0);
749
+ if (magnitude === 0) return { error: "zero_quantity", message: "Movimento de zero n\xE3o \xE9 movimento." };
750
+ const fallback = KIND_OF[type];
751
+ const declared = reasonCode ? listStockReasonCodes().find((r) => r.key === reasonCode) : void 0;
752
+ const kind = declared?.movementKind ?? fallback.kind;
753
+ const sign = fallback.sign === 0 ? Number(quantity) < 0 ? -1 : 1 : fallback.sign;
754
+ return { kind, quantity: magnitude * sign, reasonCode: declared?.key ?? fallback.reason };
755
+ }
756
+ var WHERE_IS = {
757
+ id: "shop.stock.position",
758
+ name: "getStockPosition",
759
+ 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.',
760
+ icon: "Boxes",
761
+ mode: "read",
762
+ execution: { plane: "client" },
763
+ category: "Shop",
764
+ role: "operations",
765
+ parameters: {
766
+ type: "object",
767
+ properties: { term: { type: "string", description: "The product, in the merchant's words." } },
768
+ required: ["term"]
769
+ }
770
+ };
771
+ var RECORD = {
772
+ id: "shop.stock.record",
773
+ name: "recordStockMovement",
774
+ 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.",
775
+ icon: "ArrowDownToLine",
776
+ mode: "persist",
777
+ execution: { plane: "client" },
778
+ category: "Shop",
779
+ role: "operations",
780
+ permission: { feature: "shop", action: "update" },
781
+ parameters: {
782
+ type: "object",
783
+ properties: {
784
+ variantId: { type: "string", description: "From getStockPosition." },
785
+ productName: { type: "string", description: "So the merchant reads the card without ids." },
786
+ movementType: {
787
+ type: "string",
788
+ description: "entrada | sa\xEDda | ajuste | perda",
789
+ enum: ["entrada", "saida", "ajuste", "perda"]
790
+ },
791
+ quantity: { type: "number", description: "How many, as a positive number." },
792
+ reasonCode: { type: "string", description: "Reason key when the merchant gave one (purchase, breakage, expiry, theft\u2026)." },
793
+ locationId: { type: "string", description: "Which location. Omitted means the store default." },
794
+ note: { type: "string", description: "Anything the merchant said that is not a field \u2014 the invoice number, who delivered." },
795
+ occurredAt: { type: "string", description: "ISO date when it actually moved. Omitted means now." }
796
+ },
797
+ required: ["variantId", "productName", "movementType", "quantity"]
798
+ }
799
+ };
800
+ function isLedgerMissing(error) {
801
+ const raw = error instanceof Error ? error.message : String(error?.message ?? error);
802
+ return /plg_shop_stock_|\btracking\b.*does not exist/i.test(raw);
803
+ }
804
+ 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.";
805
+ function occurredAtFor(chosenDay, today) {
806
+ if (!chosenDay || chosenDay === today) return void 0;
807
+ const parsed = /* @__PURE__ */ new Date(`${chosenDay}T12:00:00`);
808
+ return Number.isNaN(parsed.getTime()) ? void 0 : parsed.toISOString();
809
+ }
810
+ async function commitMovement(args) {
811
+ const plan = planMovement(args.movementType, args.quantity, args.reasonCode);
812
+ if ("error" in plan) return { ok: false, ...plan };
813
+ const store7 = stockStore();
814
+ if (plan.quantity < 0) {
815
+ const available = await store7.getAvailable(args.variantId);
816
+ if (Math.abs(plan.quantity) > available) {
817
+ return {
818
+ ok: false,
819
+ error: "insufficient_stock",
820
+ available,
821
+ message: `S\xF3 h\xE1 ${available} em estoque \u2014 a sa\xEDda de ${Math.abs(plan.quantity)} deixaria negativo.`
822
+ };
823
+ }
824
+ }
825
+ const movement = await store7.recordMovement({
826
+ variantId: args.variantId,
827
+ kind: plan.kind,
828
+ quantity: plan.quantity,
829
+ reasonCode: plan.reasonCode,
830
+ ...args.locationId ? { locationId: args.locationId } : {},
831
+ ...args.note ? { note: args.note } : {},
832
+ ...args.occurredAt ? { occurredAt: args.occurredAt } : {}
833
+ });
834
+ for (const table of [
835
+ "plg_shop_products",
836
+ // a Lista, e é a que o usuário estava vendo
837
+ "plg_shop_product_variants",
838
+ // a matriz de variantes
839
+ "plg_shop_stock_balances",
840
+ // as Posições
841
+ "products"
842
+ // o arquétipo, que as telas de Estoque leem
843
+ ]) {
844
+ emitDataChanged({ table, op: "update" });
845
+ }
846
+ return {
847
+ ok: true,
848
+ movementId: movement.id,
849
+ kind: plan.kind,
850
+ quantity: plan.quantity,
851
+ reasonCode: plan.reasonCode,
852
+ available: await store7.getAvailable(args.variantId)
853
+ };
854
+ }
855
+ var text = (value) => typeof value === "string" ? value.trim() : "";
856
+ var stockStore = () => resolveStockStore(getActiveTenantId() ?? "", () => createSupabaseStockStore());
857
+ function summarize(balances, locations) {
858
+ const nameOf = (id) => locations.find((l) => l.id === id)?.name ?? id;
859
+ const total = balances.reduce(
860
+ (acc, b) => ({
861
+ onHand: acc.onHand + b.onHand,
862
+ reserved: acc.reserved + b.reserved,
863
+ available: acc.available + b.available
864
+ }),
865
+ { onHand: 0, reserved: 0, available: 0 }
866
+ );
867
+ const byLocation = /* @__PURE__ */ new Map();
868
+ for (const b of balances) {
869
+ const row = byLocation.get(b.locationId) ?? { location: nameOf(b.locationId), onHand: 0, available: 0 };
870
+ row.onHand += b.onHand;
871
+ row.available += b.available;
872
+ byLocation.set(b.locationId, row);
873
+ }
874
+ return { total, byLocation: [...byLocation.values()] };
875
+ }
876
+ function createStockAgentTools({ getProvider, onChanged }) {
877
+ const failed = (error) => isLedgerMissing(error) ? { ok: false, error: "ledger_not_installed", message: LEDGER_MISSING_MESSAGE } : { ok: false, error: errorMessage(error) };
878
+ const variantsOf = async (product) => {
879
+ try {
880
+ const set = await getProvider().getProductVariants?.(product.id);
881
+ const ids = (set?.variants ?? []).map((v) => v.id);
882
+ if (ids.length > 0) return ids;
883
+ } catch {
884
+ }
885
+ return [];
886
+ };
887
+ return [
888
+ {
889
+ tool: WHERE_IS,
890
+ execute: async (args) => {
891
+ const term = text(args.term);
892
+ if (!term) return { ok: false, error: "no_term" };
893
+ try {
894
+ const catalog = await listProductsCached(getProvider, { status: "active", limit: 500 });
895
+ const answer = resolveTerm(term, catalog);
896
+ if (!answer.resolved) {
897
+ if (!answer.options?.length) return { ok: true, term, notFound: true };
898
+ return {
899
+ ok: true,
900
+ kind: "record_choice",
901
+ choices: [{
902
+ id: "stock-product",
903
+ question: `Qual "${term}"?`,
904
+ options: answer.options.map((c) => ({ id: c.productId, label: c.name, meta: candidateMeta(c) }))
905
+ }],
906
+ term,
907
+ needsChoice: true
908
+ };
909
+ }
910
+ const product = catalog.find((p) => p.id === answer.resolved.productId);
911
+ const variantIds = await variantsOf(product);
912
+ const store7 = stockStore();
913
+ const [balances, locations] = await Promise.all([
914
+ store7.listBalances(variantIds.length > 0 ? { variantIds } : {}),
915
+ store7.listLocations()
916
+ ]);
917
+ const mine = variantIds.length > 0 ? balances : balances.filter(() => false);
918
+ const { total, byLocation } = summarize(mine, locations);
919
+ return {
920
+ ok: true,
921
+ product: { productId: product.id, name: product.name, sku: product.sku },
922
+ // A variante é o que o lançamento precisa; sem ela o razão não tem
923
+ // onde postar, e é melhor dizer isso do que gravar no lugar errado.
924
+ variantId: variantIds[0] ?? null,
925
+ variants: variantIds,
926
+ total,
927
+ byLocation,
928
+ locations: locations.map((l) => ({ locationId: l.id, name: l.name, isDefault: l.isDefault }))
929
+ };
930
+ } catch (error) {
931
+ return failed(error);
932
+ }
933
+ }
934
+ },
935
+ {
936
+ tool: RECORD,
937
+ execute: async (args) => {
938
+ const variantId = text(args.variantId);
939
+ if (!variantId) return { ok: false, error: "no_variant", message: "Preciso da variante \u2014 pergunte a posi\xE7\xE3o primeiro." };
940
+ try {
941
+ const answer = await commitMovement({
942
+ variantId,
943
+ movementType: text(args.movementType),
944
+ quantity: Number(args.quantity),
945
+ reasonCode: text(args.reasonCode) || void 0,
946
+ locationId: text(args.locationId) || void 0,
947
+ note: text(args.note) || void 0,
948
+ occurredAt: text(args.occurredAt) || void 0
949
+ });
950
+ if (answer.ok) onChanged?.();
951
+ return answer;
952
+ } catch (error) {
953
+ return failed(error);
954
+ }
955
+ }
956
+ }
957
+ ];
958
+ }
959
+ var STOCK_INSTRUCTIONS = `
960
+ 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.
961
+
962
+ \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.
963
+ \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.
964
+ \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.
965
+ \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.
966
+ \xB7 O raz\xE3o n\xE3o se edita. Errou, lan\xE7a o movimento contr\xE1rio \u2014 nunca prometa "corrigir" o lan\xE7amento.
967
+ \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.
968
+ `.trim();
969
+ var stockStore2 = () => resolveStockStore(getActiveTenantId() ?? "", () => createSupabaseStockStore());
970
+ var MOVEMENT_TYPES = [
971
+ { value: "entrada", label: "Entrada" },
972
+ { value: "saida", label: "Sa\xEDda" },
973
+ { value: "ajuste", label: "Ajuste" },
974
+ { value: "perda", label: "Perda" }
975
+ ];
976
+ var KIND_FOR = {
977
+ entrada: "receipt",
978
+ saida: "adjustment",
979
+ ajuste: "adjustment",
980
+ perda: "loss"
981
+ };
982
+ var todayISO = () => (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
983
+ var field = "mt-1 w-full rounded-md border bg-background px-2.5 py-1.5 text-sm outline-none focus:border-primary";
984
+ var STOCK_INTRO = {
985
+ role: "operations",
986
+ persona: "Estoquista",
987
+ pitch: "Diga o que entrou ou saiu que eu lan\xE7o.",
988
+ 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.",
989
+ suggestions: [
990
+ { label: "Chegaram 20 unidades" },
991
+ { label: "Quebraram 2" }
992
+ ]
993
+ };
994
+ function ProductPositionsTab({ item, getProvider }) {
995
+ const product = item;
996
+ const [variants, setVariants] = useState([]);
997
+ const [balances, setBalances] = useState([]);
998
+ const [locations, setLocations] = useState([]);
999
+ const [movements, setMovements] = useState([]);
1000
+ const [loading, setLoading] = useState(true);
1001
+ const [error, setError] = useState(null);
1002
+ const [variantId, setVariantId] = useState("");
1003
+ const [type, setType] = useState(null);
1004
+ const [quantity, setQuantity] = useState("");
1005
+ const [reasonCode, setReasonCode] = useState("");
1006
+ const [note, setNote] = useState("");
1007
+ const [noteOpen, setNoteOpen] = useState(false);
1008
+ const [occurredOn, setOccurredOn] = useState(() => todayISO());
1009
+ const [saving, setSaving] = useState(false);
1010
+ const quantityRef = React9.useRef(null);
1011
+ const load = useCallback(async () => {
1012
+ setLoading(true);
1013
+ try {
1014
+ const set = await getProvider().getProductVariants?.(product.id);
1015
+ const list = set?.variants ?? [];
1016
+ setVariants(list);
1017
+ const ids = list.map((v) => v.id);
1018
+ setVariantId((current) => current || ids[0] || "");
1019
+ const store7 = stockStore2();
1020
+ const [b, l] = await Promise.all([
1021
+ ids.length > 0 ? store7.listBalances({ variantIds: ids }) : Promise.resolve([]),
1022
+ store7.listLocations()
1023
+ ]);
1024
+ setBalances(b);
1025
+ setLocations(l);
1026
+ setMovements(ids[0] ? await store7.listMovements({ variantId: ids[0], limit: 10 }) : []);
1027
+ setError(null);
1028
+ } catch (err) {
1029
+ setError(isLedgerMissing(err) ? LEDGER_MISSING_MESSAGE : errorMessage(err));
1030
+ } finally {
1031
+ setLoading(false);
1032
+ }
1033
+ }, [getProvider, product.id]);
1034
+ useEffect(() => {
1035
+ void load();
1036
+ }, [load]);
1037
+ const nameOf = useCallback(
1038
+ (id) => locations.find((l) => l.id === id)?.name ?? id,
1039
+ [locations]
1040
+ );
1041
+ const rows = useMemo(() => {
1042
+ const mine = variantId ? balances.filter((b) => b.variantId === variantId) : balances;
1043
+ return mine.map((b) => ({
1044
+ key: b.id,
1045
+ location: nameOf(b.locationId),
1046
+ onHand: b.onHand,
1047
+ reserved: b.reserved,
1048
+ available: b.available
1049
+ }));
1050
+ }, [balances, variantId, nameOf]);
1051
+ const total = useMemo(
1052
+ () => rows.reduce(
1053
+ (acc, r) => ({
1054
+ onHand: acc.onHand + r.onHand,
1055
+ reserved: acc.reserved + r.reserved,
1056
+ available: acc.available + r.available
1057
+ }),
1058
+ { onHand: 0, reserved: 0, available: 0 }
1059
+ ),
1060
+ [rows]
1061
+ );
1062
+ const reasons = useMemo(
1063
+ () => listStockReasonCodes().filter(
1064
+ (r) => !r.movementKind || r.movementKind === KIND_FOR[type ?? "entrada"]
1065
+ ),
1066
+ [type]
1067
+ );
1068
+ useEffect(() => {
1069
+ setReasonCode("");
1070
+ }, [type]);
1071
+ const open = (next) => {
1072
+ setType(next);
1073
+ setQuantity("");
1074
+ setNote("");
1075
+ setNoteOpen(false);
1076
+ setOccurredOn(todayISO());
1077
+ window.setTimeout(() => quantityRef.current?.focus(), 0);
1078
+ };
1079
+ const close = () => {
1080
+ setType(null);
1081
+ setQuantity("");
1082
+ setNote("");
1083
+ setNoteOpen(false);
1084
+ setOccurredOn(todayISO());
1085
+ };
1086
+ const amount = Math.abs(Number(quantity) || 0);
1087
+ const outgoing = type === "saida" || type === "perda";
1088
+ const nextBalance = amount > 0 ? outgoing ? total.available - amount : total.available + amount : null;
1089
+ const live = React9.useRef({ product, variantId, total, getProvider, load });
1090
+ live.current = { product, variantId, total, getProvider, load };
1091
+ const contribution = useMemo(() => ({
1092
+ id: "shop.product.positions",
1093
+ pluginId: "shop",
1094
+ role: "operations",
1095
+ instructions: STOCK_INSTRUCTIONS,
1096
+ describe: () => {
1097
+ const o = live.current;
1098
+ return {
1099
+ title: `Estoque \u2014 ${o.product.name}`,
1100
+ record: { entity: "shop_product", id: o.product.id, label: o.product.name },
1101
+ state: {
1102
+ screen: "/products/:id#posicoes",
1103
+ productId: o.product.id,
1104
+ productName: o.product.name,
1105
+ variantId: o.variantId || null,
1106
+ onHand: o.total.onHand,
1107
+ reserved: o.total.reserved,
1108
+ available: o.total.available
1109
+ }
1110
+ };
1111
+ },
1112
+ tools: createStockAgentTools({
1113
+ getProvider: () => live.current.getProvider(),
1114
+ onChanged: () => {
1115
+ void live.current.load();
1116
+ }
1117
+ })
1118
+ }), []);
1119
+ useAgentSurface(contribution);
1120
+ const submit = async () => {
1121
+ if (!variantId || !type) return;
1122
+ const amount2 = Number(quantity);
1123
+ if (!amount2) {
1124
+ toast.error("Diga quantas unidades.");
1125
+ return;
1126
+ }
1127
+ setSaving(true);
1128
+ try {
1129
+ const answer = await commitMovement({
1130
+ variantId,
1131
+ movementType: type,
1132
+ quantity: amount2,
1133
+ reasonCode: reasonCode || void 0,
1134
+ note: note.trim() || void 0,
1135
+ occurredAt: occurredAtFor(occurredOn, todayISO())
1136
+ });
1137
+ if (!answer.ok) {
1138
+ toast.error(answer.message ?? answer.error);
1139
+ return;
1140
+ }
1141
+ toast.success(`Lan\xE7ado. Saldo agora: ${answer.available}.`);
1142
+ close();
1143
+ await load();
1144
+ } catch (err) {
1145
+ toast.error(errorMessage(err));
1146
+ } finally {
1147
+ setSaving(false);
1148
+ }
1149
+ };
1150
+ if (loading) {
1151
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
1152
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3", children: [
1153
+ /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
1154
+ /* @__PURE__ */ jsx("div", { className: "h-4 w-24 animate-pulse rounded bg-muted" }),
1155
+ /* @__PURE__ */ jsx("div", { className: "h-3 w-64 animate-pulse rounded bg-muted" })
1156
+ ] }),
1157
+ /* @__PURE__ */ jsx("div", { className: "h-9 w-32 animate-pulse rounded-md bg-muted" })
1158
+ ] }),
1159
+ /* @__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: [
1160
+ /* @__PURE__ */ jsx("div", { className: "h-4 flex-1 animate-pulse rounded bg-muted" }),
1161
+ /* @__PURE__ */ jsx("div", { className: "h-4 w-12 animate-pulse rounded bg-muted" })
1162
+ ] }, i)) })
1163
+ ] });
1164
+ }
1165
+ if (error) {
1166
+ const missing = error === LEDGER_MISSING_MESSAGE;
1167
+ 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 });
1168
+ }
1169
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
1170
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center justify-between gap-3", children: [
1171
+ /* @__PURE__ */ jsxs("div", { children: [
1172
+ /* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold", children: "Estoque do produto" }),
1173
+ /* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground", children: [
1174
+ /* @__PURE__ */ jsx("span", { className: "tabular-nums", children: total.onHand }),
1175
+ " em m\xE3os \xB7",
1176
+ " ",
1177
+ /* @__PURE__ */ jsx("span", { className: "tabular-nums", children: total.reserved }),
1178
+ " reservado para pedidos \xB7",
1179
+ " ",
1180
+ /* @__PURE__ */ jsx("span", { className: "font-medium tabular-nums text-foreground", children: total.available }),
1181
+ " dispon\xEDvel. O raz\xE3o n\xE3o se edita: um erro se corrige com o movimento contr\xE1rio."
1182
+ ] })
1183
+ ] }),
1184
+ variantId && type === null && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
1185
+ /* @__PURE__ */ jsxs(
1186
+ Button,
1187
+ {
1188
+ variant: "ghost",
1189
+ size: "sm",
1190
+ onClick: () => askAI({
1191
+ intro: STOCK_INTRO,
1192
+ hint: "O que entrou ou saiu\u2026",
1193
+ draft: `Entrada de unidades de ${product.name}`
1194
+ }),
1195
+ children: [
1196
+ /* @__PURE__ */ jsx(WandSparkles, { className: "mr-1.5 h-3.5 w-3.5" }),
1197
+ "Com IA"
1198
+ ]
1199
+ }
1200
+ ),
1201
+ /* @__PURE__ */ jsxs(Button, { onClick: () => open("entrada"), children: [
1202
+ /* @__PURE__ */ jsx(Plus, { className: "mr-1.5 h-4 w-4" }),
1203
+ "Dar entrada"
1204
+ ] })
1205
+ ] })
1206
+ ] }),
1207
+ !variantId && // Nunca desabilitar um botão sem dizer por quê. Sem variante o razão não
1208
+ // tem onde postar, e um "Lançar" apagado sem explicação faz o lojista
1209
+ // clicar três vezes e concluir que a tela está quebrada.
1210
+ /* @__PURE__ */ jsxs("p", { className: "rounded-lg border border-dashed px-4 py-8 text-center text-sm text-muted-foreground", children: [
1211
+ "Este produto ainda n\xE3o tem uma variante para o estoque apontar. Crie uma em ",
1212
+ /* @__PURE__ */ jsx("span", { className: "font-medium text-foreground", children: "Variantes" }),
1213
+ " e volte aqui."
1214
+ ] }),
1215
+ variants.length > 1 && /* @__PURE__ */ jsxs("label", { className: "block max-w-xs", children: [
1216
+ /* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-muted-foreground", children: "Variante" }),
1217
+ /* @__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)) })
1218
+ ] }),
1219
+ type !== null && /* @__PURE__ */ jsxs("div", { className: "rounded-card border bg-card p-4 motion-safe:animate-zoom-in", children: [
1220
+ /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1", children: MOVEMENT_TYPES.map((t) => /* @__PURE__ */ jsx(
1221
+ "button",
1222
+ {
1223
+ type: "button",
1224
+ onClick: () => setType(t.value),
1225
+ 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"}`,
1226
+ children: t.label
1227
+ },
1228
+ t.value
1229
+ )) }),
1230
+ /* @__PURE__ */ jsxs("div", { className: "mt-3 flex flex-wrap items-end gap-4", children: [
1231
+ /* @__PURE__ */ jsxs("label", { className: "block", children: [
1232
+ /* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-muted-foreground", children: "Quantas?" }),
1233
+ /* @__PURE__ */ jsx(
1234
+ "input",
1235
+ {
1236
+ ref: quantityRef,
1237
+ type: "number",
1238
+ inputMode: "numeric",
1239
+ min: 1,
1240
+ value: quantity,
1241
+ onChange: (e) => setQuantity(e.target.value),
1242
+ onKeyDown: (e) => {
1243
+ if (e.key === "Enter") {
1244
+ e.preventDefault();
1245
+ void submit();
1246
+ }
1247
+ if (e.key === "Escape") {
1248
+ e.preventDefault();
1249
+ close();
1250
+ }
1251
+ },
1252
+ 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"
1253
+ }
1254
+ )
1255
+ ] }),
1256
+ /* @__PURE__ */ jsx("p", { "aria-live": "polite", className: "pb-2 text-sm text-muted-foreground", children: nextBalance === null ? /* @__PURE__ */ jsxs(Fragment, { children: [
1257
+ "Fica em ",
1258
+ /* @__PURE__ */ jsx("span", { className: "font-medium tabular-nums text-foreground", children: total.available })
1259
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
1260
+ /* @__PURE__ */ jsx("span", { className: "tabular-nums", children: total.available }),
1261
+ /* @__PURE__ */ jsx("span", { className: "mx-1.5", children: "\u2192" }),
1262
+ /* @__PURE__ */ jsx("span", { className: `text-base font-semibold tabular-nums ${nextBalance < 0 ? "text-destructive" : "text-foreground"}`, children: nextBalance }),
1263
+ nextBalance < 0 && /* @__PURE__ */ jsx("span", { className: "ml-2 text-destructive", children: "n\xE3o h\xE1 tudo isso" })
1264
+ ] }) }),
1265
+ /* @__PURE__ */ jsx("span", { className: "flex-1" }),
1266
+ /* @__PURE__ */ jsxs("label", { className: "block", children: [
1267
+ /* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-muted-foreground", children: "Motivo" }),
1268
+ /* @__PURE__ */ jsxs("select", { value: reasonCode, onChange: (e) => setReasonCode(e.target.value), className: field, children: [
1269
+ /* @__PURE__ */ jsx("option", { value: "", children: "Padr\xE3o" }),
1270
+ reasons.map((r) => /* @__PURE__ */ jsx("option", { value: r.key, children: r.label }, r.key))
1271
+ ] })
1272
+ ] }),
1273
+ /* @__PURE__ */ jsxs("label", { className: "block", children: [
1274
+ /* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-muted-foreground", children: type === "entrada" ? "Chegou em" : "Quando" }),
1275
+ /* @__PURE__ */ jsx(
1276
+ "input",
1277
+ {
1278
+ type: "date",
1279
+ value: occurredOn,
1280
+ max: todayISO(),
1281
+ onChange: (e) => setOccurredOn(e.target.value || todayISO()),
1282
+ className: field
1283
+ }
1284
+ )
1285
+ ] })
1286
+ ] }),
1287
+ noteOpen ? /* @__PURE__ */ jsxs("label", { className: "mt-3 block", children: [
1288
+ /* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-muted-foreground", children: "Observa\xE7\xE3o" }),
1289
+ /* @__PURE__ */ jsx(
1290
+ "input",
1291
+ {
1292
+ autoFocus: true,
1293
+ value: note,
1294
+ onChange: (e) => setNote(e.target.value),
1295
+ className: field,
1296
+ placeholder: "Nota fiscal, quem entregou\u2026"
1297
+ }
1298
+ )
1299
+ ] }) : /* @__PURE__ */ jsx(
1300
+ "button",
1301
+ {
1302
+ type: "button",
1303
+ onClick: () => setNoteOpen(true),
1304
+ className: "mt-2 text-xs font-medium text-muted-foreground underline-offset-2 hover:text-foreground hover:underline",
1305
+ children: "+ observa\xE7\xE3o"
1306
+ }
1307
+ ),
1308
+ /* @__PURE__ */ jsxs("div", { className: "mt-4 flex items-center gap-2", children: [
1309
+ /* @__PURE__ */ jsx(Button, { onClick: () => void submit(), disabled: saving || amount === 0, children: saving ? "Lan\xE7ando\u2026" : "Lan\xE7ar" }),
1310
+ /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", onClick: close, disabled: saving, children: "Cancelar" }),
1311
+ /* @__PURE__ */ jsx("span", { className: "flex-1" }),
1312
+ amount === 0 && // O motivo de o botão estar apagado, ao lado dele.
1313
+ /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: "Diga quantas unidades" })
1314
+ ] })
1315
+ ] }),
1316
+ 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: [
1317
+ /* @__PURE__ */ jsx("span", { className: "flex-1 text-sm font-medium", children: r.location }),
1318
+ /* @__PURE__ */ jsxs("span", { className: "text-xs text-muted-foreground", children: [
1319
+ r.reserved,
1320
+ " reservado"
1321
+ ] }),
1322
+ /* @__PURE__ */ jsx("span", { className: "w-16 text-right text-sm font-semibold tabular-nums", children: r.available })
1323
+ ] }, r.key)) })),
1324
+ movements.length > 0 && /* @__PURE__ */ jsxs("section", { className: "space-y-2 pt-2", children: [
1325
+ /* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold", children: "\xDAltimos movimentos" }),
1326
+ /* @__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: [
1327
+ /* @__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 }),
1328
+ /* @__PURE__ */ jsx("span", { className: "flex-1 text-sm", children: m.reasonCode ?? m.kind }),
1329
+ /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: new Date(m.occurredAt ?? m.createdAt).toLocaleDateString("pt-BR") })
1330
+ ] }, m.id)) })
1331
+ ] })
1332
+ ] });
1333
+ }
725
1334
 
726
1335
  // src/data/registry-routes.ts
727
1336
  var SHIPPING_SETTINGS_TAB = "shop-shipping";
@@ -743,20 +1352,20 @@ function shippingRegistryIdFromHash(hash) {
743
1352
  }
744
1353
  var PACKAGES_ROUTE = shippingRegistryPath(PACKAGES_REGISTRY);
745
1354
  function PackageSelect({ value, onChange, getProvider }) {
746
- const shop = React8.useMemo(() => getProvider?.() ?? getShopProvider(), [getProvider]);
747
- const [rows, setRows] = React8.useState(null);
748
- const [open, setOpen] = React8.useState(false);
749
- const [creating, setCreating] = React8.useState(false);
750
- const ref = React8.useRef(null);
751
- const load = React8.useCallback(() => {
1355
+ const shop = React9.useMemo(() => getProvider?.() ?? getShopProvider(), [getProvider]);
1356
+ const [rows, setRows] = React9.useState(null);
1357
+ const [open, setOpen] = React9.useState(false);
1358
+ const [creating, setCreating] = React9.useState(false);
1359
+ const ref = React9.useRef(null);
1360
+ const load = React9.useCallback(() => {
752
1361
  if (typeof shop.listShippingPackages !== "function") {
753
1362
  setRows([]);
754
1363
  return;
755
1364
  }
756
1365
  void shop.listShippingPackages().then(setRows).catch(() => setRows([]));
757
1366
  }, [shop]);
758
- React8.useEffect(load, [load]);
759
- React8.useEffect(() => {
1367
+ React9.useEffect(load, [load]);
1368
+ React9.useEffect(() => {
760
1369
  if (!open) return;
761
1370
  const away = (event) => {
762
1371
  if (ref.current && !ref.current.contains(event.target)) setOpen(false);
@@ -851,8 +1460,8 @@ function PackageSelect({ value, onChange, getProvider }) {
851
1460
  ] });
852
1461
  }
853
1462
  function NewPackageModal({ onClose, onCreated }) {
854
- const [error, setError] = React8.useState(null);
855
- const provider = React8.useMemo(
1463
+ const [error, setError] = React9.useState(null);
1464
+ const provider = React9.useMemo(
856
1465
  () => resolveDataProvider(packagesRegistry.entity),
857
1466
  []
858
1467
  );
@@ -898,9 +1507,9 @@ function Option({ label, hint, active, onClick }) {
898
1507
  );
899
1508
  }
900
1509
  function ShippingProfileSelect({ value, onChange, getProvider }) {
901
- const shop = React8.useMemo(() => getProvider?.() ?? getShopProvider(), [getProvider]);
902
- const [rows, setRows] = React8.useState(null);
903
- React8.useEffect(() => {
1510
+ const shop = React9.useMemo(() => getProvider?.() ?? getShopProvider(), [getProvider]);
1511
+ const [rows, setRows] = React9.useState(null);
1512
+ React9.useEffect(() => {
904
1513
  let cancelled = false;
905
1514
  if (typeof shop.listShippingProfiles !== "function") {
906
1515
  setRows([]);
@@ -1077,14 +1686,27 @@ function buildShopProductEntity(currencyCode = "BRL", getProvider) {
1077
1686
  searchable: true,
1078
1687
  showInTable: false
1079
1688
  },
1689
+ // Não se DIGITA saldo.
1690
+ //
1691
+ // Este campo era um número editável, e um número editável convida a
1692
+ // resposta errada: alguém conta a prateleira, acha 18, escreve 18, e o
1693
+ // razão perde POR QUE mudou — se chegaram seis ou se sumiram quatro. É
1694
+ // exatamente o que um razão existe para impedir, e a 0036 já tratava a
1695
+ // escrita direta aqui como um ajuste às cegas para não perder o movimento.
1696
+ //
1697
+ // Vira reflexo: mostra o saldo, e o ato acontece em Posições, onde ele tem
1698
+ // espécie, motivo e data.
1080
1699
  {
1081
1700
  key: "inventoryCount",
1082
1701
  label: "Estoque",
1083
1702
  type: "number",
1084
1703
  group: "inventory",
1085
1704
  min: 0,
1086
- placeholder: "0",
1087
- hint: "Chega a zero e o produto aparece como esgotado na loja.",
1705
+ // `showInForm: false` e não um campo desabilitado: some do formulário de
1706
+ // edição, continua na tabela e na ficha. Mostra-se o saldo; digita-se o
1707
+ // movimento.
1708
+ showInForm: false,
1709
+ hint: "Saldo do raz\xE3o. Entrada, sa\xEDda e perda se lan\xE7am na aba Posi\xE7\xF5es.",
1088
1710
  showInTable: true,
1089
1711
  sortable: true,
1090
1712
  renderCell: (v) => {
@@ -1182,7 +1804,7 @@ function buildShopProductEntity(currencyCode = "BRL", getProvider) {
1182
1804
  group: "shipping",
1183
1805
  min: 0,
1184
1806
  placeholder: "0,4",
1185
- hint: "Do produto embalado. A caixa vem do cadastro acima.",
1807
+ hint: "Do produto, j\xE1 embalado. As medidas v\xEAm da embalagem escolhida acima.",
1186
1808
  showInTable: false,
1187
1809
  visibleWhen: (values) => values.isPhysical !== false
1188
1810
  },
@@ -1199,9 +1821,19 @@ function buildShopProductEntity(currencyCode = "BRL", getProvider) {
1199
1821
  // Em centímetros, sem seletor de unidade: a coluna é `*_cm` e a caixa da
1200
1822
  // loja também. Duas unidades para a mesma medida é como o peso passou
1201
1823
  // quatro anos sendo lido errado.
1202
- { key: "heightCm", label: "Altura (cm)", type: "number", group: "shipping", min: 0, placeholder: "10", showInTable: false, visibleWhen: physical },
1203
- { key: "widthCm", label: "Largura (cm)", type: "number", group: "shipping", min: 0, placeholder: "20", showInTable: false, visibleWhen: physical },
1204
- { key: "lengthCm", label: "Comprimento (cm)", type: "number", group: "shipping", min: 0, placeholder: "30", showInTable: false, visibleWhen: physical },
1824
+ // A medida é da EMBALAGEM, não do produto.
1825
+ //
1826
+ // O seletor logo acima já associa o produto a uma embalagem cadastrada da
1827
+ // loja (0047) — e é lá que altura, largura e comprimento vivem. Repetir os
1828
+ // três aqui pedia a mesma informação duas vezes por caminhos diferentes, e
1829
+ // as duas divergiam no dia em que alguém trocasse a caixa: a cotação lê a
1830
+ // embalagem, o formulário mostrava outro número.
1831
+ //
1832
+ // As colunas continuam existindo — um catálogo anterior à 0047 tem produto
1833
+ // medido e sem embalagem, e a cotação ainda os lê. O que sai é a pergunta.
1834
+ { key: "heightCm", label: "Altura (cm)", type: "number", group: "shipping", min: 0, showInForm: false, showInTable: false },
1835
+ { key: "widthCm", label: "Largura (cm)", type: "number", group: "shipping", min: 0, showInForm: false, showInTable: false },
1836
+ { key: "lengthCm", label: "Comprimento (cm)", type: "number", group: "shipping", min: 0, showInForm: false, showInTable: false },
1205
1837
  {
1206
1838
  // Declarado aqui para poder viajar — para a transportadora, para o motoboy
1207
1839
  // ou para quem embala. O que a loja FAZ com um carrinho refrigerado é uma
@@ -1228,6 +1860,17 @@ function buildShopProductEntity(currencyCode = "BRL", getProvider) {
1228
1860
  namePlural: "Produtos",
1229
1861
  icon: "Package",
1230
1862
  displayField: "name",
1863
+ // Qual tabela esta tela É. Sem isto, `deriveEntityKey` cai no rótulo — e a
1864
+ // chave da entidade vira `produto`, que muda com uma tradução. Quem avisa
1865
+ // "escrevi aqui" nomeia a TABELA (é o que emitDataChanged invalida no cache),
1866
+ // então sem esta linha o aviso não encontra a lista: o barramento casa
1867
+ // tabela-com-tabela, e o lado do assinante estava vazio.
1868
+ //
1869
+ // Não muda o caminho do dado: o índice do plugin passa um
1870
+ // `dataProvider: createShopProductDataProvider(...)` explícito, que vence o
1871
+ // provider genérico que `data.table` acionaria — mesma forma da entidade de
1872
+ // cliente logo ao lado.
1873
+ data: { table: "plg_shop_products" },
1231
1874
  fields,
1232
1875
  fieldGroups: [
1233
1876
  // imageSlot renders the scaffolding's image uploader alongside the group.
@@ -1238,7 +1881,7 @@ function buildShopProductEntity(currencyCode = "BRL", getProvider) {
1238
1881
  {
1239
1882
  id: "shipping",
1240
1883
  label: "Envio",
1241
- description: "Peso e medidas do produto embalado. Sem eles a transportadora \xE9 consultada com a caixa padr\xE3o da loja, e o frete cotado pode n\xE3o ser o frete pago.",
1884
+ 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
1885
  columns: 3
1243
1886
  }
1244
1887
  ],
@@ -1262,139 +1905,21 @@ function buildShopProductEntity(currencyCode = "BRL", getProvider) {
1262
1905
  icon: "Layers",
1263
1906
  component: ProductVariantsTab,
1264
1907
  props: { getProvider }
1908
+ },
1909
+ // Separada de Variantes de propósito: aquela edita O QUE se vende,
1910
+ // esta responde ONDE estão as unidades — e a ficha só sabia dizer um
1911
+ // número só, que não distingue a loja do depósito nem o que já está
1912
+ // prometido a um pedido.
1913
+ {
1914
+ id: "positions",
1915
+ label: "Posi\xE7\xF5es",
1916
+ icon: "Boxes",
1917
+ component: ProductPositionsTab,
1918
+ props: { getProvider }
1265
1919
  }
1266
1920
  ] : void 0
1267
1921
  };
1268
1922
  }
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
1923
  var STATUS_TONE2 = {
1399
1924
  active: "bg-emerald-100 text-emerald-800",
1400
1925
  draft: "bg-gray-100 text-gray-700",
@@ -1556,6 +2081,34 @@ function buildShopDiscountEntity() {
1556
2081
  facets: [{ field: "status", allLabel: "Todos" }]
1557
2082
  };
1558
2083
  }
2084
+
2085
+ // src/views/discount-agent.ts
2086
+ var DISCOUNT_CREATE_INSTRUCTIONS = `
2087
+ Esta lista \xE9 a de Descontos da loja. Um registro aqui \xE9 um CUPOM que o comprador digita no carrinho.
2088
+
2089
+ O c\xF3digo \xE9 seu para escrever:
2090
+ \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.
2091
+ \xB7 Curto e dit\xE1vel ao telefone. Evite 0/O e 1/I juntos, evite palavra que o comprador precise soletrar.
2092
+ \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.
2093
+ \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.
2094
+
2095
+ A regra:
2096
+ \xB7 "frete gr\xE1tis" \u2192 \`type: free_shipping\` e SEM \`value\` \u2014 frete gr\xE1tis n\xE3o desconta o subtotal.
2097
+ \xB7 "10%", "10 por cento" \u2192 \`type: percentage\`, \`value: 10\`.
2098
+ \xB7 "R$ 50", "50 reais de desconto" \u2192 \`type: fixed_amount\`, \`value: 50\`.
2099
+ \xB7 "acima de R$ 200", "a partir de 200" \u2192 \`minSubtotal: 200\`.
2100
+ \xB7 "s\xF3 a primeira compra", "uma vez por cliente" \u2192 \`oncePerCustomer: true\`.
2101
+ \xB7 "os 100 primeiros" \u2192 \`usageLimit: 100\`.
2102
+ \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.
2103
+
2104
+ Status:
2105
+ \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.
2106
+ \xB7 \`draft\` s\xF3 quando ele disser que \xE9 para deixar pronto para depois.
2107
+
2108
+ 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.
2109
+
2110
+ Depois de criar, diga em uma linha o c\xF3digo, o efeito e at\xE9 quando vale, para ele j\xE1 poder divulgar.
2111
+ `.trim();
1559
2112
  function buildShopCategoryEntity() {
1560
2113
  const fields = [
1561
2114
  {
@@ -1744,7 +2297,7 @@ var STATUS_LABEL2 = {
1744
2297
  disabled: "Desativado"
1745
2298
  };
1746
2299
  function buildShopGiftCardEntity(currencyCode = "BRL") {
1747
- const money3 = (v) => new Intl.NumberFormat("pt-BR", { style: "currency", currency: currencyCode }).format(Number(v) || 0);
2300
+ const money2 = (v) => new Intl.NumberFormat("pt-BR", { style: "currency", currency: currencyCode }).format(Number(v) || 0);
1748
2301
  const fields = [
1749
2302
  {
1750
2303
  key: "code",
@@ -1772,7 +2325,7 @@ function buildShopGiftCardEntity(currencyCode = "BRL") {
1772
2325
  hint: "Valor de face do cart\xE3o. N\xE3o muda depois de emitido.",
1773
2326
  showInTable: true,
1774
2327
  sortable: true,
1775
- renderCell: (v) => /* @__PURE__ */ jsx("span", { className: "block text-right text-muted-foreground", children: money3(v) })
2328
+ renderCell: (v) => /* @__PURE__ */ jsx("span", { className: "block text-right text-muted-foreground", children: money2(v) })
1776
2329
  },
1777
2330
  {
1778
2331
  key: "balance",
@@ -1786,7 +2339,7 @@ function buildShopGiftCardEntity(currencyCode = "BRL") {
1786
2339
  hint: "O que ainda pode ser gasto. Cai a cada resgate.",
1787
2340
  showInTable: true,
1788
2341
  sortable: true,
1789
- renderCell: (v) => /* @__PURE__ */ jsx("span", { className: "block text-right font-medium", children: money3(v) })
2342
+ renderCell: (v) => /* @__PURE__ */ jsx("span", { className: "block text-right font-medium", children: money2(v) })
1790
2343
  },
1791
2344
  {
1792
2345
  key: "status",
@@ -1921,20 +2474,20 @@ function buildShopCustomerGroupEntity() {
1921
2474
  fieldGroups: [{ id: "basic", label: "Grupo", columns: 2 }]
1922
2475
  };
1923
2476
  }
1924
- function describeSegment(s, money3) {
2477
+ function describeSegment(s, money2) {
1925
2478
  const parts = [];
1926
2479
  if (s.minOrders != null && s.maxOrders != null) parts.push(`${s.minOrders}\u2013${s.maxOrders} pedidos`);
1927
2480
  else if (s.minOrders != null) parts.push(`${s.minOrders}+ pedidos`);
1928
2481
  else if (s.maxOrders != null) parts.push(`at\xE9 ${s.maxOrders} pedidos`);
1929
- if (s.minSpent != null && s.maxSpent != null) parts.push(`gastou ${money3(s.minSpent)}\u2013${money3(s.maxSpent)}`);
1930
- else if (s.minSpent != null) parts.push(`gastou ${money3(s.minSpent)}+`);
1931
- else if (s.maxSpent != null) parts.push(`gastou at\xE9 ${money3(s.maxSpent)}`);
2482
+ if (s.minSpent != null && s.maxSpent != null) parts.push(`gastou ${money2(s.minSpent)}\u2013${money2(s.maxSpent)}`);
2483
+ else if (s.minSpent != null) parts.push(`gastou ${money2(s.minSpent)}+`);
2484
+ else if (s.maxSpent != null) parts.push(`gastou at\xE9 ${money2(s.maxSpent)}`);
1932
2485
  if (s.joinedAfter) parts.push(`entrou ap\xF3s ${new Date(s.joinedAfter).toLocaleDateString("pt-BR")}`);
1933
2486
  if (s.joinedBefore) parts.push(`entrou antes de ${new Date(s.joinedBefore).toLocaleDateString("pt-BR")}`);
1934
2487
  return parts.length ? parts.join(" \xB7 ") : "Todos os clientes";
1935
2488
  }
1936
2489
  function buildShopCustomerSegmentEntity(currencyCode = "BRL") {
1937
- const money3 = (v) => new Intl.NumberFormat("pt-BR", { style: "currency", currency: currencyCode }).format(v);
2490
+ const money2 = (v) => new Intl.NumberFormat("pt-BR", { style: "currency", currency: currencyCode }).format(v);
1938
2491
  const fields = [
1939
2492
  {
1940
2493
  key: "name",
@@ -2011,7 +2564,7 @@ function buildShopCustomerSegmentEntity(currencyCode = "BRL") {
2011
2564
  showInForm: false,
2012
2565
  showInTable: true,
2013
2566
  sortable: false,
2014
- renderCell: (_v, row) => /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: describeSegment(row, money3) })
2567
+ renderCell: (_v, row) => /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: describeSegment(row, money2) })
2015
2568
  },
2016
2569
  {
2017
2570
  key: "matchCount",
@@ -2341,17 +2894,17 @@ function createShopCustomerSegmentDataProvider(provider) {
2341
2894
  }
2342
2895
  var DEBOUNCE_MS = 400;
2343
2896
  function useStoreSettings(provider, defaults = {}) {
2344
- const shop = React8.useMemo(
2897
+ const shop = React9.useMemo(
2345
2898
  () => provider ? resolve(provider) : getShopProvider(),
2346
2899
  [provider]
2347
2900
  );
2348
2901
  const supported = typeof shop.getStoreSettings === "function" && typeof shop.updateStoreSettings === "function";
2349
- const [stored, setStored] = React8.useState(null);
2350
- const [loading, setLoading] = React8.useState(supported);
2351
- const [saving, setSaving] = React8.useState(false);
2352
- const [error, setError] = React8.useState(null);
2353
- const [loadError, setLoadError] = React8.useState(null);
2354
- React8.useEffect(() => {
2902
+ const [stored, setStored] = React9.useState(null);
2903
+ const [loading, setLoading] = React9.useState(supported);
2904
+ const [saving, setSaving] = React9.useState(false);
2905
+ const [error, setError] = React9.useState(null);
2906
+ const [loadError, setLoadError] = React9.useState(null);
2907
+ React9.useEffect(() => {
2355
2908
  if (!supported) return;
2356
2909
  let cancelled = false;
2357
2910
  setLoading(true);
@@ -2367,12 +2920,12 @@ function useStoreSettings(provider, defaults = {}) {
2367
2920
  cancelled = true;
2368
2921
  };
2369
2922
  }, [shop, supported]);
2370
- const pendingRef = React8.useRef({});
2371
- const timerRef = React8.useRef(null);
2372
- React8.useEffect(() => () => {
2923
+ const pendingRef = React9.useRef({});
2924
+ const timerRef = React9.useRef(null);
2925
+ React9.useEffect(() => () => {
2373
2926
  if (timerRef.current) clearTimeout(timerRef.current);
2374
2927
  }, []);
2375
- const set = React8.useCallback(
2928
+ const set = React9.useCallback(
2376
2929
  (patch) => {
2377
2930
  setStored((current) => ({ ...current ?? {}, ...patch }));
2378
2931
  if (!supported) return;
@@ -2388,7 +2941,7 @@ function useStoreSettings(provider, defaults = {}) {
2388
2941
  },
2389
2942
  [shop, supported]
2390
2943
  );
2391
- const value = React8.useMemo(() => ({ ...defaults, ...stored ?? {} }), [defaults, stored]);
2944
+ const value = React9.useMemo(() => ({ ...defaults, ...stored ?? {} }), [defaults, stored]);
2392
2945
  return { value, stored, set, loading, saving, error, loadError, supported };
2393
2946
  }
2394
2947
  function SettingsState({
@@ -2617,25 +3170,25 @@ function PaymentsSettings({ provider }) {
2617
3170
  const settings = useStoreSettings(provider);
2618
3171
  const { value, set } = settings;
2619
3172
  const { connectors } = useConnectors({ hostPluginId: "shop", includeGrants: false });
2620
- const [adding, setAdding] = React8.useState(false);
2621
- const [selected, setSelected] = React8.useState(null);
3173
+ const [adding, setAdding] = React9.useState(false);
3174
+ const [selected, setSelected] = React9.useState(null);
2622
3175
  const panelAvailable = useConnectorsPanelAvailable();
2623
- const openJourney = React8.useCallback(
3176
+ const openJourney = React9.useCallback(
2624
3177
  (connector) => {
2625
3178
  if (panelAvailable) openConnectorsPanel(connector);
2626
3179
  else setSelected(connector);
2627
3180
  },
2628
3181
  [panelAvailable]
2629
3182
  );
2630
- const payment = React8.useMemo(
3183
+ const payment = React9.useMemo(
2631
3184
  () => (connectors ?? []).filter((c) => c.category && PAYMENT_CATEGORIES.includes(c.category)),
2632
3185
  [connectors]
2633
3186
  );
2634
- const live = React8.useMemo(() => payment.filter((c) => c.state === "connected"), [payment]);
3187
+ const live = React9.useMemo(() => payment.filter((c) => c.state === "connected"), [payment]);
2635
3188
  const chosen = value.paymentMethods ?? [];
2636
3189
  const staleProvider = !!value.paymentProvider && !live.some((c) => c.slug === value.paymentProvider) && live.length === 0;
2637
- const movedFrom = React8.useRef(null);
2638
- React8.useEffect(() => {
3190
+ const movedFrom = React9.useRef(null);
3191
+ React9.useEffect(() => {
2639
3192
  if (!settings.supported || settings.loading) return;
2640
3193
  const next = resolvePaymentProvider(value.paymentProvider, live.map((c) => c.slug));
2641
3194
  if (!next) return;
@@ -2676,7 +3229,7 @@ function PaymentsSettings({ provider }) {
2676
3229
  ] }),
2677
3230
  /* @__PURE__ */ jsx(AddConnectorButton, { onClick: () => setAdding(true) })
2678
3231
  ] }),
2679
- /* @__PURE__ */ jsx("div", { className: "rounded-lg border bg-card shadow-sm", children: payment.length === 0 ? /* @__PURE__ */ jsx("p", { className: "p-4 text-sm text-muted-foreground", children: "Nenhum provedor de pagamento dispon\xEDvel para este app ainda." }) : /* @__PURE__ */ jsxs("ul", { className: "divide-y", role: "radiogroup", "aria-label": "Quem cobra", children: [
3232
+ /* @__PURE__ */ jsx("div", { className: "rounded-card border bg-card shadow-sm", children: payment.length === 0 ? /* @__PURE__ */ jsx("p", { className: "p-4 text-sm text-muted-foreground", children: "Nenhum provedor de pagamento dispon\xEDvel para este app ainda." }) : /* @__PURE__ */ jsxs("ul", { className: "divide-y", role: "radiogroup", "aria-label": "Quem cobra", children: [
2680
3233
  payment.map((connector) => /* @__PURE__ */ jsx(
2681
3234
  PaymentProviderRow,
2682
3235
  {
@@ -2841,17 +3394,17 @@ function CheckoutSettings({ provider }) {
2841
3394
  {
2842
3395
  title: "Campos do cliente",
2843
3396
  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((field) => /* @__PURE__ */ jsx(
3397
+ children: FIELDS.map((field2) => /* @__PURE__ */ jsx(
2845
3398
  SelectRow,
2846
3399
  {
2847
- label: field.label,
2848
- description: field.hint,
2849
- value: toRequirement(fields[field.key], !!field.locked),
2850
- options: field.locked ? [{ value: "required", label: "Pedir (obrigat\xF3rio)" }] : REQUIREMENT,
2851
- onChange: (choice) => setField(field.key, choice),
2852
- disabled: field.locked
3400
+ label: field2.label,
3401
+ description: field2.hint,
3402
+ value: toRequirement(fields[field2.key], !!field2.locked),
3403
+ options: field2.locked ? [{ value: "required", label: "Pedir (obrigat\xF3rio)" }] : REQUIREMENT,
3404
+ onChange: (choice) => setField(field2.key, choice),
3405
+ disabled: field2.locked
2853
3406
  },
2854
- field.key
3407
+ field2.key
2855
3408
  ))
2856
3409
  }
2857
3410
  )
@@ -2867,11 +3420,11 @@ function PackagesSettings({ basePath = shippingRegistryPath(PACKAGES_REGISTRY) }
2867
3420
  ] });
2868
3421
  }
2869
3422
  function HandlingSettings({ settings, provider }) {
2870
- const shop = React8.useMemo(() => provider ? resolve(provider) : getShopProvider(), [provider]);
3423
+ const shop = React9.useMemo(() => provider ? resolve(provider) : getShopProvider(), [provider]);
2871
3424
  const { value, set, loading, supported } = settings;
2872
- const [noteDraft, setNoteDraft] = React8.useState(null);
2873
- const [profiles, setProfiles] = React8.useState(null);
2874
- React8.useEffect(() => {
3425
+ const [noteDraft, setNoteDraft] = React9.useState(null);
3426
+ const [profiles, setProfiles] = React9.useState(null);
3427
+ React9.useEffect(() => {
2875
3428
  let cancelled = false;
2876
3429
  void shop.listShippingProfiles?.().then((list) => {
2877
3430
  if (!cancelled) setProfiles(list);
@@ -2894,7 +3447,7 @@ function HandlingSettings({ settings, provider }) {
2894
3447
  ] }),
2895
3448
  /* @__PURE__ */ jsx("p", { className: "mt-0.5 text-[10px] text-muted-foreground", children: "Um perfil \xE9 um grupo de produtos com zonas pr\xF3prias \u2014 refrigerados, fr\xE1geis, volumosos, sob encomenda. Um perfil com s\xF3 as suas zonas e o gateway desligado \xE9 como se diz \xABisto n\xE3o vai de transportadora\xBB. Produto sem perfil usa o geral, que \xE9 o de todos hoje." })
2896
3449
  ] }),
2897
- /* @__PURE__ */ jsx("div", { className: "rounded-lg border bg-card px-5 shadow-sm", children: /* @__PURE__ */ jsxs(
3450
+ /* @__PURE__ */ jsx("div", { className: "rounded-card border bg-card px-5 shadow-sm", children: /* @__PURE__ */ jsxs(
2898
3451
  "button",
2899
3452
  {
2900
3453
  type: "button",
@@ -3001,46 +3554,46 @@ var FREIGHT_SOURCE = [
3001
3554
  }
3002
3555
  ];
3003
3556
  function ShippingSettings({ provider }) {
3004
- const [registryId, setRegistryId] = React8.useState(shippingRegistryIdFromHash);
3005
- React8.useEffect(() => {
3557
+ const [registryId, setRegistryId] = React9.useState(shippingRegistryIdFromHash);
3558
+ React9.useEffect(() => {
3006
3559
  const handler = () => setRegistryId(shippingRegistryIdFromHash());
3007
3560
  window.addEventListener("hashchange", handler);
3008
3561
  return () => window.removeEventListener("hashchange", handler);
3009
3562
  }, []);
3010
- const shop = React8.useMemo(() => provider ? resolve(provider) : getShopProvider(), [provider]);
3011
- const registries = React8.useMemo(
3563
+ const shop = React9.useMemo(() => provider ? resolve(provider) : getShopProvider(), [provider]);
3564
+ const registries = React9.useMemo(
3012
3565
  () => buildShopRegistries(provider ?? getShopProvider),
3013
3566
  [provider]
3014
3567
  );
3015
3568
  const storeSettings = useStoreSettings(provider);
3016
3569
  const supported = typeof shop.getShippingSettings === "function" && typeof shop.updateShippingSettings === "function";
3017
- const [row, setRow] = React8.useState(null);
3018
- const [zones, setZones] = React8.useState(null);
3019
- const [locations, setLocations] = React8.useState(null);
3020
- const [loading, setLoading] = React8.useState(supported);
3021
- const [saving, setSaving] = React8.useState(false);
3022
- const [error, setError] = React8.useState(null);
3023
- const [loadError, setLoadError] = React8.useState(null);
3024
- const [flatDraft, setFlatDraft] = React8.useState("");
3025
- const [freeDraft, setFreeDraft] = React8.useState("");
3026
- const [flatBad, setFlatBad] = React8.useState(false);
3027
- const [freeBad, setFreeBad] = React8.useState(false);
3570
+ const [row, setRow] = React9.useState(null);
3571
+ const [zones, setZones] = React9.useState(null);
3572
+ const [locations, setLocations] = React9.useState(null);
3573
+ const [loading, setLoading] = React9.useState(supported);
3574
+ const [saving, setSaving] = React9.useState(false);
3575
+ const [error, setError] = React9.useState(null);
3576
+ const [loadError, setLoadError] = React9.useState(null);
3577
+ const [flatDraft, setFlatDraft] = React9.useState("");
3578
+ const [freeDraft, setFreeDraft] = React9.useState("");
3579
+ const [flatBad, setFlatBad] = React9.useState(false);
3580
+ const [freeBad, setFreeBad] = React9.useState(false);
3028
3581
  const { connectors } = useConnectors({ hostPluginId: "shop", includeGrants: false });
3029
- const [selected, setSelected] = React8.useState(null);
3582
+ const [selected, setSelected] = React9.useState(null);
3030
3583
  const panelAvailable = useConnectorsPanelAvailable();
3031
- const openJourney = React8.useCallback(
3584
+ const openJourney = React9.useCallback(
3032
3585
  (connector) => {
3033
3586
  if (panelAvailable) openConnectorsPanel(connector);
3034
3587
  else setSelected(connector);
3035
3588
  },
3036
3589
  [panelAvailable]
3037
3590
  );
3038
- const carriers = React8.useMemo(
3591
+ const carriers = React9.useMemo(
3039
3592
  () => (connectors ?? []).filter((c) => c.category === "logistics" || c.category === "shipping"),
3040
3593
  [connectors]
3041
3594
  );
3042
3595
  const carriersConnected = carriers.some((c) => c.state !== "ready");
3043
- React8.useEffect(() => {
3596
+ React9.useEffect(() => {
3044
3597
  let cancelled = false;
3045
3598
  if (supported) {
3046
3599
  setLoading(true);
@@ -3069,7 +3622,7 @@ function ShippingSettings({ provider }) {
3069
3622
  cancelled = true;
3070
3623
  };
3071
3624
  }, [shop, supported]);
3072
- const save = React8.useCallback(
3625
+ const save = React9.useCallback(
3073
3626
  (patch) => {
3074
3627
  if (!supported) return;
3075
3628
  setSaving(true);
@@ -3180,7 +3733,7 @@ function ShippingSettings({ provider }) {
3180
3733
  /* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold", children: "De onde voc\xEA despacha" }),
3181
3734
  /* @__PURE__ */ jsx("p", { className: "mt-0.5 text-[10px] text-muted-foreground", children: "Os locais que guardam estoque e despacham pedidos. O CEP do local padr\xE3o \xE9 a origem que a transportadora usa para cotar o frete \u2014 \xE9 aqui que ele se cadastra, uma vez. Cada local pode ter o seu pr\xF3prio despachante ou seguir a decis\xE3o da loja." })
3182
3735
  ] }),
3183
- /* @__PURE__ */ jsx("div", { className: "rounded-lg border bg-card px-5 shadow-sm", children: /* @__PURE__ */ jsxs(
3736
+ /* @__PURE__ */ jsx("div", { className: "rounded-card border bg-card px-5 shadow-sm", children: /* @__PURE__ */ jsxs(
3184
3737
  "button",
3185
3738
  {
3186
3739
  type: "button",
@@ -3208,7 +3761,7 @@ function ShippingSettings({ provider }) {
3208
3761
  /* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold", children: "Zonas de entrega" }),
3209
3762
  /* @__PURE__ */ jsx("p", { className: "mt-0.5 text-[10px] text-muted-foreground", children: "Faixas de CEP com pre\xE7o, prazo e frete gr\xE1tis pr\xF3prios. Um CEP fora de todas as zonas cai no pre\xE7o base acima; se a loja n\xE3o tiver pre\xE7o base nem zona que o cubra, o checkout diz que n\xE3o entrega ali. Uma faixa tamb\xE9m pode ser uma recusa \u2014 \xABn\xE3o entrego aqui\xBB \u2014 e ela vence qualquer zona e qualquer gateway." })
3210
3763
  ] }),
3211
- /* @__PURE__ */ jsx("div", { className: "rounded-lg border bg-card px-5 shadow-sm", children: /* @__PURE__ */ jsxs(
3764
+ /* @__PURE__ */ jsx("div", { className: "rounded-card border bg-card px-5 shadow-sm", children: /* @__PURE__ */ jsxs(
3212
3765
  "button",
3213
3766
  {
3214
3767
  type: "button",
@@ -3262,7 +3815,7 @@ function ShippingSettings({ provider }) {
3262
3815
  /* @__PURE__ */ jsx("p", { className: "mt-0.5 text-[10px] text-muted-foreground", children: "Conectado, ele cota o frete para o CEP do comprador em vez de a loja adivinhar. O pre\xE7o cotado \xE9 gravado no servidor e \xE9 ele que o pedido cobra \u2014 o navegador escolhe entre op\xE7\xF5es, nunca informa um valor. Quais transportadoras aparecem vem da conta do gateway, n\xE3o desta lista." }),
3263
3816
  !carriersConnected && /* @__PURE__ */ jsx("p", { className: "mt-1 text-[10px] text-muted-foreground", children: "Nenhum conectado \u2014 a loja entrega por conta pr\xF3pria, pelo pre\xE7o base e pelas zonas acima. Conectar \xE9 opcional e pode ficar para depois." })
3264
3817
  ] }),
3265
- /* @__PURE__ */ jsx("div", { className: "rounded-lg border bg-card shadow-sm", children: /* @__PURE__ */ jsx("ul", { className: "divide-y", children: carriers.map((connector) => /* @__PURE__ */ jsx(ConnectorRow, { connector, onOpen: openJourney }, connector.id)) }) })
3818
+ /* @__PURE__ */ jsx("div", { className: "rounded-card border bg-card shadow-sm", children: /* @__PURE__ */ jsx("ul", { className: "divide-y", children: carriers.map((connector) => /* @__PURE__ */ jsx(ConnectorRow, { connector, onOpen: openJourney }, connector.id)) }) })
3266
3819
  ] }),
3267
3820
  /* @__PURE__ */ jsx(HandlingSettings, { settings: storeSettings, provider }),
3268
3821
  /* @__PURE__ */ jsx("div", { className: "flex min-h-5 items-center gap-1.5 px-1 text-xs text-muted-foreground", children: error ? /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-1.5 text-destructive", children: [
@@ -3630,6 +4183,39 @@ var appmaxConnectorDef = {
3630
4183
  disconnect: () => store2.disconnect(),
3631
4184
  ExtraPanel: AppmaxExtraPanel
3632
4185
  };
4186
+ var ABACATEPAY_FUNCTION = "abacatepay-sync";
4187
+ function client() {
4188
+ const supabase = getSupabaseClientOptional();
4189
+ if (!supabase) throw new Error("Sem conex\xE3o com o banco para falar com a AbacatePay.");
4190
+ return supabase;
4191
+ }
4192
+ async function invoke(body) {
4193
+ const tenantId = getShopTenantId();
4194
+ if (!tenantId) throw new Error("Sem loja selecionada para sincronizar com a AbacatePay.");
4195
+ const runtimeToken = await connectorRuntimeToken({ tenantId });
4196
+ const { data, error } = await client().functions.invoke(ABACATEPAY_FUNCTION, {
4197
+ body: { ...body, tenantId },
4198
+ headers: { [CONNECTOR_RUNTIME_TOKEN_HEADER]: runtimeToken }
4199
+ });
4200
+ if (!error) return data;
4201
+ const status = Number(error?.context?.status ?? 0);
4202
+ let message2 = "";
4203
+ try {
4204
+ const payload = await error?.context?.json?.();
4205
+ message2 = String(payload?.message ?? payload?.error ?? "").trim();
4206
+ } catch {
4207
+ }
4208
+ if (!message2 && status === 404) {
4209
+ message2 = "Esta loja n\xE3o tem uma conta AbacatePay conectada.";
4210
+ }
4211
+ throw new Error(message2 || error.message);
4212
+ }
4213
+ function pushCatalogue() {
4214
+ return invoke({ action: "push_products" });
4215
+ }
4216
+ function reconcileCatalogue() {
4217
+ return invoke({ action: "reconcile" });
4218
+ }
3633
4219
  var ABACATEPAY_CONNECTOR_ID = "abacatepay";
3634
4220
  var ABACATEPAY_LATENCY_BUDGET_MS = 6e3;
3635
4221
  var ABACATEPAY_FIELDS = [
@@ -3649,8 +4235,13 @@ var ABACATEPAY_SETTINGS = [
3649
4235
  type: "multi-select",
3650
4236
  options: [
3651
4237
  { value: "PIX", label: "Pix" },
3652
- { value: "BOLETO", label: "Boleto" },
3653
- { value: "CARD", label: "Cart\xE3o de cr\xE9dito" }
4238
+ { value: "BOLETO", label: "Boleto" }
4239
+ // Credit card does NOT belong here. AbacatePay discontinued it for new
4240
+ // clients and new integrations, so offering the checkbox would let a
4241
+ // merchant switch on a method the acquirer refuses — and the first to find
4242
+ // out would be a buyer. `CARD` stays in METHOD_LABELS because an older
4243
+ // connection may have the option stored and its row still has to describe
4244
+ // itself.
3654
4245
  ],
3655
4246
  requiredToOperate: true
3656
4247
  }
@@ -3665,33 +4256,127 @@ function methodsDetail2(value) {
3665
4256
  const labels = list.map((v) => METHOD_LABELS2[String(v).trim()] ?? String(v).trim()).filter(Boolean);
3666
4257
  return labels.length > 0 ? labels.join(", ") : void 0;
3667
4258
  }
3668
- function AbacatePayExtraPanel() {
3669
- const [connected, setConnected] = useState(false);
3670
- const [loaded, setLoaded] = useState(false);
3671
- useEffect(() => {
3672
- let cancelled = false;
3673
- void store3.get().then((c) => {
3674
- if (!cancelled) setConnected(!!c?.active);
3675
- }).catch(() => {
3676
- }).finally(() => {
3677
- if (!cancelled) setLoaded(true);
3678
- });
3679
- return () => {
3680
- cancelled = true;
3681
- };
3682
- }, []);
3683
- if (!loaded || !connected) return null;
3684
- return /* @__PURE__ */ jsxs("div", { className: "space-y-2 border-t pt-4 text-xs text-muted-foreground", children: [
3685
- /* @__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. Cart\xE3o vai para a p\xE1gina segura da AbacatePay e volta com o resultado \u2014 assim nenhum n\xFAmero de cart\xE3o passa pelo seu site." }),
3686
- /* @__PURE__ */ jsx("p", { children: "Quem decide se a cobran\xE7a \xE9 de verdade \xE9 a chave. Uma chave de teste simula o pagamento e n\xE3o movimenta dinheiro; a de produ\xE7\xE3o cobra o cliente. \xC9 a mesma tela para as duas, ent\xE3o confira no painel da AbacatePay qual chave voc\xEA colou aqui." }),
3687
- /* @__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.' })
3688
- ] });
3689
- }
4259
+ function CatalogueSync() {
4260
+ const [busy, setBusy] = useState(null);
4261
+ const [result, setResult] = useState(null);
4262
+ const [error, setError] = useState(null);
4263
+ const run = async (kind) => {
4264
+ setBusy(kind);
4265
+ setError(null);
4266
+ try {
4267
+ setResult(await (kind === "push" ? pushCatalogue() : reconcileCatalogue()));
4268
+ } catch (caught) {
4269
+ setError(caught instanceof Error ? caught.message : "N\xE3o conseguimos sincronizar agora.");
4270
+ } finally {
4271
+ setBusy(null);
4272
+ }
4273
+ };
4274
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
4275
+ /* @__PURE__ */ jsxs("div", { children: [
4276
+ /* @__PURE__ */ jsx("p", { className: "text-sm font-medium text-foreground", children: "Cat\xE1logo" }),
4277
+ /* @__PURE__ */ jsx("p", { className: "mt-0.5 text-xs text-muted-foreground", children: "O link de pagamento \xE9 aberto na p\xE1gina da AbacatePay, e ela cobra por produtos que precisam existir l\xE1. Envie os seus antes de mandar um link para um cliente." })
4278
+ ] }),
4279
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-2", children: [
4280
+ /* @__PURE__ */ jsx(
4281
+ "button",
4282
+ {
4283
+ type: "button",
4284
+ onClick: () => {
4285
+ void run("push");
4286
+ },
4287
+ disabled: busy !== null,
4288
+ className: "inline-flex min-h-9 items-center rounded-md bg-primary px-3 text-xs font-medium text-primary-foreground transition hover:opacity-90 disabled:opacity-60",
4289
+ children: busy === "push" ? "Enviando\u2026" : "Enviar produtos"
4290
+ }
4291
+ ),
4292
+ /* @__PURE__ */ jsx(
4293
+ "button",
4294
+ {
4295
+ type: "button",
4296
+ onClick: () => {
4297
+ void run("reconcile");
4298
+ },
4299
+ disabled: busy !== null,
4300
+ className: "inline-flex min-h-9 items-center rounded-md border px-3 text-xs font-medium transition hover:bg-muted/50 disabled:opacity-60",
4301
+ children: busy === "reconcile" ? "Conferindo\u2026" : "Conferir pre\xE7os"
4302
+ }
4303
+ )
4304
+ ] }),
4305
+ error && /* @__PURE__ */ jsx("p", { className: "text-xs text-destructive", children: error }),
4306
+ result && /* @__PURE__ */ jsxs("div", { className: "space-y-1.5 rounded-md border bg-muted/30 p-3 text-xs", children: [
4307
+ /* @__PURE__ */ jsxs("p", { className: "text-foreground", children: [
4308
+ result.pushed > 0 ? `${result.pushed} produto${result.pushed > 1 ? "s" : ""} enviado${result.pushed > 1 ? "s" : ""}.` : "Nenhum produto novo para enviar.",
4309
+ " ",
4310
+ /* @__PURE__ */ jsxs("span", { className: "text-muted-foreground", children: [
4311
+ result.unchanged,
4312
+ " j\xE1 ",
4313
+ result.unchanged === 1 ? "estava" : "estavam",
4314
+ " l\xE1, de ",
4315
+ result.total,
4316
+ "."
4317
+ ] })
4318
+ ] }),
4319
+ result.hasMore && /* @__PURE__ */ jsx("p", { className: "text-muted-foreground", children: "Ainda h\xE1 produtos na fila \u2014 clique novamente para continuar." }),
4320
+ result.withoutImage?.length > 0 && /* @__PURE__ */ jsxs("p", { className: "text-muted-foreground", children: [
4321
+ result.withoutImage.length,
4322
+ " produto",
4323
+ result.withoutImage.length > 1 ? "s foram enviados" : " foi enviado",
4324
+ " sem foto \u2014 a AbacatePay recusou a imagem. O produto vende normalmente; s\xF3 aparece sem imagem na p\xE1gina dela."
4325
+ ] }),
4326
+ result.skipped?.length > 0 && /* @__PURE__ */ jsxs("p", { className: "text-muted-foreground", children: [
4327
+ result.skipped.length,
4328
+ " ignorado",
4329
+ result.skipped.length > 1 ? "s" : "",
4330
+ " por estar sem nome ou com pre\xE7o zerado."
4331
+ ] }),
4332
+ result.failed?.length > 0 && /* @__PURE__ */ jsxs("div", { className: "text-destructive", children: [
4333
+ /* @__PURE__ */ jsxs("p", { children: [
4334
+ result.failed.length,
4335
+ " recusado",
4336
+ result.failed.length > 1 ? "s" : "",
4337
+ " pela AbacatePay:"
4338
+ ] }),
4339
+ /* @__PURE__ */ jsx("ul", { className: "mt-0.5 list-disc pl-4", children: result.failed.slice(0, 3).map((f) => /* @__PURE__ */ jsx("li", { children: f.error }, f.productId)) })
4340
+ ] }),
4341
+ result.drifted && result.drifted.length > 0 && /* @__PURE__ */ jsxs("p", { className: "text-amber-600 dark:text-amber-500", children: [
4342
+ result.drifted.length,
4343
+ " produto",
4344
+ result.drifted.length > 1 ? "s mudaram" : " mudou",
4345
+ " de pre\xE7o aqui depois de enviado. A AbacatePay n\xE3o permite alterar um produto j\xE1 criado, ent\xE3o o link ainda cobra o pre\xE7o antigo \u2014 refa\xE7a o produto l\xE1 se o novo valor precisa valer."
4346
+ ] })
4347
+ ] })
4348
+ ] });
4349
+ }
4350
+ function AbacatePayExtraPanel() {
4351
+ const [connected, setConnected] = useState(false);
4352
+ const [loaded, setLoaded] = useState(false);
4353
+ useEffect(() => {
4354
+ let cancelled = false;
4355
+ void store3.get().then((c) => {
4356
+ if (!cancelled) setConnected(!!c?.active);
4357
+ }).catch(() => {
4358
+ }).finally(() => {
4359
+ if (!cancelled) setLoaded(true);
4360
+ });
4361
+ return () => {
4362
+ cancelled = true;
4363
+ };
4364
+ }, []);
4365
+ if (!loaded || !connected) return null;
4366
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-4 border-t pt-4", children: [
4367
+ /* @__PURE__ */ jsx(CatalogueSync, {}),
4368
+ /* @__PURE__ */ jsxs("div", { className: "space-y-2 text-xs text-muted-foreground", children: [
4369
+ /* @__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. Cart\xE3o de cr\xE9dito a AbacatePay encerrou para contas novas \u2014 para vender no cart\xE3o, conecte outro provedor em Pagamentos." }),
4370
+ /* @__PURE__ */ jsx("p", { children: "Quem decide se a cobran\xE7a \xE9 de verdade \xE9 a chave. Uma chave de teste simula o pagamento e n\xE3o movimenta dinheiro; a de produ\xE7\xE3o cobra o cliente. \xC9 a mesma tela para as duas, ent\xE3o confira no painel da AbacatePay qual chave voc\xEA colou aqui." }),
4371
+ /* @__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.' })
4372
+ ] })
4373
+ ] });
4374
+ }
3690
4375
  var abacatePayConnectorDef = {
3691
4376
  id: ABACATEPAY_CONNECTOR_ID,
3692
4377
  hostPluginId: "shop",
3693
4378
  name: "AbacatePay",
3694
- description: "Pix, boleto e cart\xE3o com uma chave s\xF3, sem contrato de adquirente e sem tirar o cliente da loja.",
4379
+ description: "Pix e boleto com uma chave s\xF3, sem contrato de adquirente e sem tirar o cliente da loja.",
3695
4380
  icon: "CreditCard",
3696
4381
  category: "payments",
3697
4382
  // One pasted key, no consent screen: AbacatePay has no OAuth and no app to
@@ -3740,7 +4425,7 @@ var abacatePayConnectorDef = {
3740
4425
  invokedAt: [
3741
4426
  {
3742
4427
  key: "shop.checkout.session_open",
3743
- label: "O cliente finaliza a compra e a cobran\xE7a \xE9 aberta na AbacatePay (Pix, boleto ou cart\xE3o)",
4428
+ label: "O cliente finaliza a compra e a cobran\xE7a \xE9 aberta na AbacatePay (Pix ou boleto)",
3744
4429
  touches: ["order", "payment"]
3745
4430
  },
3746
4431
  {
@@ -3908,6 +4593,10 @@ var stripeConnectConnectorDef = {
3908
4593
  };
3909
4594
  var VINDI_CONNECTOR_ID = "vindi";
3910
4595
  var VINDI_LATENCY_BUDGET_MS = 6e3;
4596
+ var VINDI_AUTHENTICATION_URLS = {
4597
+ sandbox: "https://tc-intermediador-sandbox.yapay.com.br/authentication?consumer_key=",
4598
+ production: "https://tc.intermediador.yapay.com.br/authentication?consumer_key="
4599
+ };
3911
4600
  var VINDI_FIELDS = [
3912
4601
  {
3913
4602
  key: "tokenAccount",
@@ -3915,6 +4604,27 @@ var VINDI_FIELDS = [
3915
4604
  type: "password",
3916
4605
  secret: true,
3917
4606
  placeholder: "Vindi Pagamentos \u2192 Minha Conta \u2192 Token"
4607
+ },
4608
+ {
4609
+ key: "consumerKey",
4610
+ label: "Chave de consumidor (para links de pagamento)",
4611
+ type: "password",
4612
+ secret: true,
4613
+ placeholder: "consumer_key"
4614
+ },
4615
+ {
4616
+ key: "consumerSecret",
4617
+ label: "Segredo de consumidor",
4618
+ type: "password",
4619
+ secret: true,
4620
+ placeholder: "consumer_secret"
4621
+ },
4622
+ {
4623
+ key: "authorizationCode",
4624
+ label: "CODE de autoriza\xE7\xE3o",
4625
+ type: "password",
4626
+ secret: true,
4627
+ placeholder: "copiado da barra de endere\xE7o, uma vez por ambiente"
3918
4628
  }
3919
4629
  ];
3920
4630
  var VINDI_ENVIRONMENT_SETTING = {
@@ -3928,26 +4638,80 @@ var VINDI_ENVIRONMENT_SETTING = {
3928
4638
  ],
3929
4639
  requiredToOperate: true
3930
4640
  };
4641
+ var VINDI_CARD_METHOD = "3";
3931
4642
  var VINDI_SETTINGS = [
3932
4643
  VINDI_ENVIRONMENT_SETTING,
3933
4644
  {
3934
4645
  key: "methods",
3935
4646
  label: "Formas de pagamento",
3936
- description: "O que o cliente pode escolher no checkout.",
4647
+ description: "O que o cliente pode escolher. O cart\xE3o \xE9 pago na p\xE1gina segura da Vindi.",
3937
4648
  type: "multi-select",
3938
4649
  options: [
3939
4650
  { value: "27", label: "Pix" },
3940
4651
  { value: "28", label: "Bolepix (boleto com QR)" },
3941
- { value: "6", label: "Boleto" }
4652
+ { value: "6", label: "Boleto" },
4653
+ { value: VINDI_CARD_METHOD, label: "Cart\xE3o de cr\xE9dito (p\xE1gina da Vindi)" }
3942
4654
  ],
3943
4655
  requiredToOperate: true
4656
+ },
4657
+ /**
4658
+ * The seller's postcode, which Vindi's cart endpoint refuses to open without.
4659
+ *
4660
+ * NOT `requiredToOperate` in the static list — a store selling Pix has never
4661
+ * needed it, and turning it into a requirement would put every working
4662
+ * connection into "falta configurar" the day this version ships. It becomes
4663
+ * required the moment card is enabled: see `requiredSettingsFor`.
4664
+ */
4665
+ {
4666
+ key: "sellerPostalCode",
4667
+ label: "CEP da loja",
4668
+ description: "De onde a loja vende. A Vindi exige esse dado para abrir a p\xE1gina de cart\xE3o.",
4669
+ type: "text",
4670
+ placeholder: "01310-100",
4671
+ pattern: "\\d{5}-?\\d{3}",
4672
+ patternHint: "Oito d\xEDgitos, com ou sem h\xEDfen."
4673
+ },
4674
+ /**
4675
+ * How many instalments Vindi's page may offer.
4676
+ *
4677
+ * A `select` rather than free text because it is a number with a ceiling the
4678
+ * merchant does not know: Vindi refuses a split above what the account was
4679
+ * accredited for, and a typed "24" would be a checkout that fails at the last
4680
+ * click. The default is 1 — a store that never thought about instalments
4681
+ * should not discover it is financing purchases.
4682
+ */
4683
+ {
4684
+ key: "maxSplit",
4685
+ label: "Parcelas no cart\xE3o",
4686
+ description: "Em quantas vezes o cliente pode dividir. Depende do que a Vindi liberou para a conta.",
4687
+ type: "select",
4688
+ options: [
4689
+ { value: "1", label: "\xC0 vista" },
4690
+ { value: "2", label: "At\xE9 2x" },
4691
+ { value: "3", label: "At\xE9 3x" },
4692
+ { value: "6", label: "At\xE9 6x" },
4693
+ { value: "10", label: "At\xE9 10x" },
4694
+ { value: "12", label: "At\xE9 12x" }
4695
+ ],
4696
+ defaultValue: "1"
3944
4697
  }
3945
4698
  ];
4699
+ function requiredSettingsFor(settings) {
4700
+ const raw = settings?.methods;
4701
+ const list = Array.isArray(raw) ? raw : String(raw ?? "").split(",");
4702
+ const hasCard = list.map((v) => String(v).trim()).includes(VINDI_CARD_METHOD);
4703
+ return VINDI_SETTINGS.map((setting) => setting.key === "sellerPostalCode" && hasCard ? { ...setting, requiredToOperate: true } : setting);
4704
+ }
3946
4705
  var store5 = createConnectionStore(VINDI_CONNECTOR_ID, {
3947
4706
  fields: VINDI_FIELDS,
3948
4707
  settings: VINDI_SETTINGS
3949
4708
  });
3950
- var METHOD_LABELS4 = { "27": "Pix", "28": "Bolepix", "6": "Boleto" };
4709
+ var METHOD_LABELS4 = {
4710
+ "27": "Pix",
4711
+ "28": "Bolepix",
4712
+ "6": "Boleto",
4713
+ [VINDI_CARD_METHOD]: "Cart\xE3o"
4714
+ };
3951
4715
  var ENVIRONMENT_LABELS = { sandbox: "Sandbox", production: "Produ\xE7\xE3o" };
3952
4716
  function connectionDetail(settings) {
3953
4717
  const environment = ENVIRONMENT_LABELS[String(settings?.environment ?? "")];
@@ -3959,11 +4723,14 @@ function connectionDetail(settings) {
3959
4723
  }
3960
4724
  function VindiExtraPanel() {
3961
4725
  const [connected, setConnected] = useState(false);
4726
+ const [environment, setEnvironment] = useState("");
3962
4727
  const [loaded, setLoaded] = useState(false);
3963
4728
  useEffect(() => {
3964
4729
  let cancelled = false;
3965
4730
  void store5.get().then((c) => {
3966
- if (!cancelled) setConnected(!!c?.active);
4731
+ if (cancelled) return;
4732
+ setConnected(!!c?.active);
4733
+ setEnvironment(String(c?.settings?.environment ?? ""));
3967
4734
  }).catch(() => {
3968
4735
  }).finally(() => {
3969
4736
  if (!cancelled) setLoaded(true);
@@ -3973,17 +4740,59 @@ function VindiExtraPanel() {
3973
4740
  };
3974
4741
  }, []);
3975
4742
  if (!loaded) return null;
3976
- return /* @__PURE__ */ jsx("div", { className: "space-y-2 border-t pt-4 text-xs text-muted-foreground", children: connected ? /* @__PURE__ */ jsxs(Fragment, { children: [
3977
- /* @__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. Cart\xE3o ainda n\xE3o \u2014 ele vai chegar pela p\xE1gina segura da Vindi, para que nenhum n\xFAmero de cart\xE3o passe pelo seu site." }),
3978
- /* @__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." }),
3979
- /* @__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.' })
3980
- ] }) : /* @__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." }) });
4743
+ const authenticationUrl = VINDI_AUTHENTICATION_URLS[environment];
4744
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-2 border-t pt-4 text-xs text-muted-foreground", children: [
4745
+ connected ? /* @__PURE__ */ jsxs(Fragment, { children: [
4746
+ /* @__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." }),
4747
+ /* @__PURE__ */ jsxs("p", { children: [
4748
+ "Voc\xEA tamb\xE9m pode gerar um ",
4749
+ /* @__PURE__ */ jsx("strong", { children: "link de pagamento" }),
4750
+ " 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."
4751
+ ] }),
4752
+ /* @__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." }),
4753
+ /* @__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.' })
4754
+ ] }) : /* @__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." }),
4755
+ /* @__PURE__ */ jsxs("div", { className: "rounded-md border border-dashed p-3", children: [
4756
+ /* @__PURE__ */ jsx("p", { className: "font-medium text-foreground", children: "Para gerar links de pagamento" }),
4757
+ /* @__PURE__ */ jsxs("p", { className: "mt-1", children: [
4758
+ "O token da conta n\xE3o emite link \u2014 a Vindi exige uma autoriza\xE7\xE3o \xE0 parte. Pe\xE7a a",
4759
+ " ",
4760
+ /* @__PURE__ */ jsx("strong", { children: "chave" }),
4761
+ " e o ",
4762
+ /* @__PURE__ */ jsx("strong", { children: "segredo de consumidor" }),
4763
+ " no painel da Vindi e preencha os campos acima. Falta ainda o ",
4764
+ /* @__PURE__ */ jsx("strong", { children: "CODE" }),
4765
+ ":"
4766
+ ] }),
4767
+ /* @__PURE__ */ jsxs("ol", { className: "mt-2 list-decimal space-y-1 pl-4", children: [
4768
+ /* @__PURE__ */ jsx("li", { children: authenticationUrl ? /* @__PURE__ */ jsxs(Fragment, { children: [
4769
+ "Abra no navegador",
4770
+ " ",
4771
+ /* @__PURE__ */ jsxs("code", { className: "break-all rounded bg-muted px-1 py-0.5", children: [
4772
+ authenticationUrl,
4773
+ "SUA_CHAVE"
4774
+ ] }),
4775
+ " ",
4776
+ "trocando ",
4777
+ /* @__PURE__ */ jsx("code", { children: "SUA_CHAVE" }),
4778
+ " pela sua chave de consumidor."
4779
+ ] }) : /* @__PURE__ */ jsx(Fragment, { children: "Escolha o ambiente acima e o endere\xE7o para buscar o CODE aparece aqui." }) }),
4780
+ /* @__PURE__ */ jsx("li", { children: "A Vindi redireciona. O CODE fica na barra de endere\xE7o \u2014 copie de l\xE1." }),
4781
+ /* @__PURE__ */ jsxs("li", { children: [
4782
+ "Cole no campo ",
4783
+ /* @__PURE__ */ jsx("strong", { children: "CODE de autoriza\xE7\xE3o" }),
4784
+ " e salve."
4785
+ ] })
4786
+ ] }),
4787
+ /* @__PURE__ */ jsx("p", { className: "mt-2", children: "\xC9 uma vez por ambiente. A partir da\xED a renova\xE7\xE3o di\xE1ria acontece sozinha." })
4788
+ ] })
4789
+ ] });
3981
4790
  }
3982
4791
  var vindiConnectorDef = {
3983
4792
  id: VINDI_CONNECTOR_ID,
3984
4793
  hostPluginId: "shop",
3985
4794
  name: "Vindi",
3986
- description: "Pix e boleto pela Vindi. A loja recebe na conta dela, com o cliente pagando sem sair da p\xE1gina.",
4795
+ description: "Pix, boleto e cart\xE3o pela Vindi. A loja recebe na conta dela; o cart\xE3o \xE9 digitado na p\xE1gina segura da Vindi.",
3987
4796
  icon: "CreditCard",
3988
4797
  // Declared rather than borrowed from the catalogue, because the catalogue row
3989
4798
  // is a change in another repo that has to be seeded before it exists anywhere:
@@ -4021,7 +4830,7 @@ var vindiConnectorDef = {
4021
4830
  kind: "data",
4022
4831
  resource: "payment",
4023
4832
  actions: ["write"],
4024
- reason: "Registrar aqui a cobran\xE7a aberta e a confirma\xE7\xE3o do pagamento. Dados do cart\xE3o n\xE3o passam por este app.",
4833
+ reason: "Registrar aqui a cobran\xE7a aberta e a confirma\xE7\xE3o do pagamento. Dados do cart\xE3o n\xE3o passam por este app \u2014 s\xE3o digitados na p\xE1gina da Vindi.",
4025
4834
  sensitive: true
4026
4835
  }
4027
4836
  ],
@@ -4035,7 +4844,7 @@ var vindiConnectorDef = {
4035
4844
  invokedAt: [
4036
4845
  {
4037
4846
  key: "shop.checkout.session_open",
4038
- label: "O cliente finaliza a compra e a cobran\xE7a \xE9 aberta na Vindi (Pix ou boleto)",
4847
+ label: "O cliente finaliza a compra e a cobran\xE7a \xE9 aberta na Vindi (Pix, boleto ou a p\xE1gina de cart\xE3o)",
4039
4848
  touches: ["order", "payment"]
4040
4849
  },
4041
4850
  {
@@ -4054,7 +4863,10 @@ var vindiConnectorDef = {
4054
4863
  connected: !!connection?.active,
4055
4864
  detail: connectionDetail(connection?.settings),
4056
4865
  plane: "request",
4057
- ...settingsStatus(VINDI_SETTINGS, connection)
4866
+ // Judged against what THIS connection needs, not against a constant: a
4867
+ // store that ticked card owes a postcode, and one that did not owes
4868
+ // nothing. See `requiredSettingsFor`.
4869
+ ...settingsStatus(requiredSettingsFor(connection?.settings), connection)
4058
4870
  }),
4059
4871
  { store: () => store5 }
4060
4872
  ),
@@ -4073,6 +4885,188 @@ var vindiConnectorDef = {
4073
4885
  disconnect: () => store5.disconnect(),
4074
4886
  ExtraPanel: VindiExtraPanel
4075
4887
  };
4888
+ var MERCADOPAGO_CONNECTOR_ID = "mercadopago";
4889
+ var MERCADOPAGO_LATENCY_BUDGET_MS = 6e3;
4890
+ var MERCADOPAGO_FIELDS = [
4891
+ {
4892
+ key: "accessToken",
4893
+ label: "Access token",
4894
+ type: "password",
4895
+ secret: true,
4896
+ placeholder: "APP_USR-\u2026 (Suas integra\xE7\xF5es \u2192 sua aplica\xE7\xE3o \u2192 Credenciais)"
4897
+ },
4898
+ {
4899
+ key: "publicKey",
4900
+ label: "Public key",
4901
+ // Not masked: it travels to the browser anyway, and a merchant who cannot
4902
+ // see it cannot tell the test key from the production one at a glance.
4903
+ type: "text",
4904
+ secret: true,
4905
+ placeholder: "APP_USR-\u2026"
4906
+ }
4907
+ ];
4908
+ var store6 = createConnectionStore(MERCADOPAGO_CONNECTOR_ID, {
4909
+ fields: MERCADOPAGO_FIELDS
4910
+ });
4911
+ function connectionDetail2(settings) {
4912
+ const key = String(settings?.publicKey ?? "").trim();
4913
+ if (!key) return "Pix e cart\xE3o";
4914
+ return /^TEST-/i.test(key) ? "Sandbox \xB7 Pix e cart\xE3o" : "Produ\xE7\xE3o \xB7 Pix e cart\xE3o";
4915
+ }
4916
+ function MercadoPagoExtraPanel() {
4917
+ const [connected, setConnected] = useState(false);
4918
+ const [loaded, setLoaded] = useState(false);
4919
+ useEffect(() => {
4920
+ let cancelled = false;
4921
+ void store6.get().then((c) => {
4922
+ if (!cancelled) setConnected(!!c?.active);
4923
+ }).catch(() => {
4924
+ }).finally(() => {
4925
+ if (!cancelled) setLoaded(true);
4926
+ });
4927
+ return () => {
4928
+ cancelled = true;
4929
+ };
4930
+ }, []);
4931
+ if (!loaded) return null;
4932
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-2 border-t pt-4 text-xs text-muted-foreground", children: [
4933
+ connected ? /* @__PURE__ */ jsxs(Fragment, { children: [
4934
+ /* @__PURE__ */ jsxs("p", { children: [
4935
+ "Pix e cart\xE3o acontecem ",
4936
+ /* @__PURE__ */ jsx("strong", { children: "dentro da sua loja" }),
4937
+ ". 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."
4938
+ ] }),
4939
+ /* @__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." }),
4940
+ /* @__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.' }),
4941
+ /* @__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." })
4942
+ ] }) : /* @__PURE__ */ jsxs("p", { children: [
4943
+ "As duas chaves ficam no painel do Mercado Pago, em ",
4944
+ /* @__PURE__ */ jsx("strong", { children: "Suas integra\xE7\xF5es" }),
4945
+ " \u2192 sua aplica\xE7\xE3o \u2192 ",
4946
+ /* @__PURE__ */ jsx("strong", { children: "Credenciais de produ\xE7\xE3o" }),
4947
+ ". Copie o ",
4948
+ /* @__PURE__ */ jsx("em", { children: "access token" }),
4949
+ " e a",
4950
+ " ",
4951
+ /* @__PURE__ */ jsx("em", { children: "public key" }),
4952
+ " da ",
4953
+ /* @__PURE__ */ jsx("strong", { children: "mesma" }),
4954
+ " aplica\xE7\xE3o: elas s\xE3o um par, e uma sem a outra n\xE3o fecha uma venda."
4955
+ ] }),
4956
+ /* @__PURE__ */ jsxs("div", { className: "rounded-md border border-dashed p-3", children: [
4957
+ /* @__PURE__ */ jsx("p", { className: "font-medium text-foreground", children: "Para testar sem cobrar de verdade" }),
4958
+ /* @__PURE__ */ jsxs("p", { className: "mt-1", children: [
4959
+ "As chaves que come\xE7am com ",
4960
+ /* @__PURE__ */ jsx("code", { className: "rounded bg-muted px-1 py-0.5", children: "TEST-" }),
4961
+ " ",
4962
+ /* @__PURE__ */ jsx("strong", { children: "n\xE3o funcionam" }),
4963
+ " aqui \u2014 o Mercado Pago as recusa nesta API. O teste \xE9 feito com ",
4964
+ /* @__PURE__ */ jsx("strong", { children: "usu\xE1rios de teste" }),
4965
+ ": crie um vendedor e um comprador em Suas integra\xE7\xF5es \u2192 Contas de teste, e use as chaves de ",
4966
+ /* @__PURE__ */ jsx("em", { children: "produ\xE7\xE3o" }),
4967
+ " do vendedor de teste. N\xE3o \xE9 dinheiro real, e \xE9 assim que o pr\xF3prio Mercado Pago recomenda."
4968
+ ] })
4969
+ ] })
4970
+ ] });
4971
+ }
4972
+ var mercadoPagoConnectorDef = {
4973
+ id: MERCADOPAGO_CONNECTOR_ID,
4974
+ hostPluginId: "shop",
4975
+ name: "Mercado Pago",
4976
+ 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.",
4977
+ icon: "CreditCard",
4978
+ // Declared rather than borrowed from the catalogue, for the same reason Vindi
4979
+ // declares its own: the catalogue row is a change in another repo that has to
4980
+ // be seeded before it exists anywhere, and until then the shelf renders a grey
4981
+ // plug. The absolute URL answers from any origin — a tenant app is served from
4982
+ // its own domain, so a path would not.
4983
+ logoUrl: "https://assets.fayz.ai/connectors/mercadopago.png",
4984
+ category: "payments",
4985
+ // Pasted keys today. Unlike Vindi — whose consent screen only LOOKS like OAuth
4986
+ // — Mercado Pago has a real merchant OAuth, and it is the right end state: one
4987
+ // platform application, each seller authorizing it, `marketplace_fee` taken on
4988
+ // the charge instead of invoiced afterwards. What it needs is the redirect URI
4989
+ // registered against the platform broker, which is a change in ymaia and not
4990
+ // here. The same slug is already declared by plugin-financial with
4991
+ // `authKind: 'oauth'`, and because a credential is keyed by slug + tenant, a
4992
+ // store that authorizes there is redeemable from here the day that lands.
4993
+ authKind: "api-key",
4994
+ // The charge is opened in the buyer's request path; the notification and the
4995
+ // refund are outbound sync work.
4996
+ planes: ["request", "sync"],
4997
+ direction: "outbound",
4998
+ docsUrl: "https://www.mercadopago.com.br/developers/pt/docs/checkout-api/landing",
4999
+ providerInstall: { url: "https://www.mercadopago.com.br/developers/panel/app" },
5000
+ // Grounded in what the request plane does, and padded with nothing: the total
5001
+ // is recomputed server-side from the order before the charge is opened, which
5002
+ // is a read of the order and never a write of it; the charge and its
5003
+ // confirmation are the write.
5004
+ permissions: [
5005
+ {
5006
+ kind: "data",
5007
+ resource: "order",
5008
+ actions: ["read"],
5009
+ 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.",
5010
+ sensitive: true
5011
+ },
5012
+ {
5013
+ kind: "data",
5014
+ resource: "payment",
5015
+ actions: ["write"],
5016
+ 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.",
5017
+ sensitive: true
5018
+ }
5019
+ ],
5020
+ latencyBudgetMs: MERCADOPAGO_LATENCY_BUDGET_MS,
5021
+ // A gateway that does not answer must stop the sale: an order that reads as
5022
+ // confirmed without a payment is worse than a refused checkout. Same verdict
5023
+ // as the other four gateways, opposite to the carrier next door.
5024
+ onFailure: "block",
5025
+ fields: MERCADOPAGO_FIELDS,
5026
+ invokedAt: [
5027
+ {
5028
+ key: "shop.checkout.session_open",
5029
+ 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)",
5030
+ touches: ["order", "payment"]
5031
+ },
5032
+ {
5033
+ key: "shop.payment.capture",
5034
+ label: "O Mercado Pago avisa que o pagamento caiu, a cobran\xE7a \xE9 reconsultada e o pedido \xE9 liberado",
5035
+ plane: "sync",
5036
+ trigger: "webhook",
5037
+ touches: ["payment"]
5038
+ }
5039
+ ],
5040
+ // Read from the row the list already batched, so a screen with N connectors
5041
+ // costs one plg_connections request rather than N.
5042
+ //
5043
+ // No `settingsStatus`: this connector has nothing to configure, so a
5044
+ // connection either has its credential pair or does not. "Conectado, falta
5045
+ // configurar" would be a state that can never be true here.
5046
+ ...connectionStatus(
5047
+ MERCADOPAGO_CONNECTOR_ID,
5048
+ (connection) => ({
5049
+ connected: !!connection?.active,
5050
+ detail: connectionDetail2(connection?.settings),
5051
+ plane: "request"
5052
+ }),
5053
+ { store: () => store6 }
5054
+ ),
5055
+ ...connectionSettings(store6),
5056
+ // Reached only AFTER the credential sink accepted the pair — that ordering is
5057
+ // what makes `active = true` mean "credentials were placed for this store"
5058
+ // rather than "somebody pressed a button". The secret never arrives here: the
5059
+ // hub split it out before calling this.
5060
+ //
5061
+ // No `testConnection`: the probe is handed the non-secret half, and a public
5062
+ // key alone cannot open an order. Testing belongs to the edge function that
5063
+ // holds the credential.
5064
+ saveConnection: async () => {
5065
+ await store6.save({ active: true });
5066
+ },
5067
+ disconnect: () => store6.disconnect(),
5068
+ ExtraPanel: MercadoPagoExtraPanel
5069
+ };
4076
5070
  var KIND_META = {
4077
5071
  online_store: { label: "Loja Online", icon: Globe, hint: "Seu storefront fayz \u2014 a vitrine p\xFAblica da loja.", placeholder: "https://minhaloja.com.br/" },
4078
5072
  whatsapp: { label: "WhatsApp", icon: MessageCircle, hint: "Link direto para o WhatsApp Business.", placeholder: "https://wa.me/5521999999999" },
@@ -4090,12 +5084,12 @@ function notifyChannelsChanged() {
4090
5084
  if (typeof window !== "undefined") window.dispatchEvent(new Event("fayz:channels-changed"));
4091
5085
  }
4092
5086
  function ChannelsPage({ resolveProvider }) {
4093
- const provider = React8.useMemo(() => resolve2(resolveProvider), [resolveProvider]);
5087
+ const provider = React9.useMemo(() => resolve2(resolveProvider), [resolveProvider]);
4094
5088
  const supported = typeof provider.listSalesChannels === "function";
4095
- const [channels, setChannels] = React8.useState([]);
4096
- const [loading, setLoading] = React8.useState(true);
4097
- const [adding, setAdding] = React8.useState(null);
4098
- const reload = React8.useCallback(async () => {
5089
+ const [channels, setChannels] = React9.useState([]);
5090
+ const [loading, setLoading] = React9.useState(true);
5091
+ const [adding, setAdding] = React9.useState(null);
5092
+ const reload = React9.useCallback(async () => {
4099
5093
  if (!provider.listSalesChannels) {
4100
5094
  setLoading(false);
4101
5095
  return;
@@ -4113,7 +5107,7 @@ function ChannelsPage({ resolveProvider }) {
4113
5107
  setLoading(false);
4114
5108
  }
4115
5109
  }, [provider]);
4116
- React8.useEffect(() => {
5110
+ React9.useEffect(() => {
4117
5111
  void reload();
4118
5112
  }, [reload]);
4119
5113
  const patch = async (id, input) => {
@@ -4169,7 +5163,7 @@ function ChannelsPage({ resolveProvider }) {
4169
5163
  ] });
4170
5164
  }
4171
5165
  function OnlineStoreCard({ channel, onSave, onToggle }) {
4172
- const [url, setUrl] = React8.useState(channel.url ?? "");
5166
+ const [url, setUrl] = React9.useState(channel.url ?? "");
4173
5167
  const dirty = (channel.url ?? "") !== url;
4174
5168
  const meta = KIND_META.online_store;
4175
5169
  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: [
@@ -4195,8 +5189,8 @@ function OnlineStoreCard({ channel, onSave, onToggle }) {
4195
5189
  ] }) });
4196
5190
  }
4197
5191
  function ChannelRow({ channel, onSave, onToggle, onRemove }) {
4198
- const [url, setUrl] = React8.useState(channel.url ?? "");
4199
- const [editing, setEditing] = React8.useState(false);
5192
+ const [url, setUrl] = React9.useState(channel.url ?? "");
5193
+ const [editing, setEditing] = React9.useState(false);
4200
5194
  const meta = KIND_META[channel.kind] ?? KIND_META.custom;
4201
5195
  const Icon = meta.icon;
4202
5196
  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: [
@@ -4224,9 +5218,9 @@ function ChannelRow({ channel, onSave, onToggle, onRemove }) {
4224
5218
  }
4225
5219
  function AddChannelForm({ kind, onKind, onCancel, onAdd }) {
4226
5220
  const meta = KIND_META[kind];
4227
- const [label, setLabel] = React8.useState(meta.label);
4228
- const [url, setUrl] = React8.useState("");
4229
- React8.useEffect(() => {
5221
+ const [label, setLabel] = React9.useState(meta.label);
5222
+ const [url, setUrl] = React9.useState("");
5223
+ React9.useEffect(() => {
4230
5224
  setLabel(KIND_META[kind].label);
4231
5225
  }, [kind]);
4232
5226
  return /* @__PURE__ */ jsxs("div", { className: "mt-3 rounded-card border border-border bg-card p-4", children: [
@@ -4278,17 +5272,17 @@ function formatSize(bytes) {
4278
5272
  return `${(bytes / 1024 ** i).toFixed(i === 0 ? 0 : 1)} ${units[i]}`;
4279
5273
  }
4280
5274
  function ContentPage({ resolveProvider }) {
4281
- const provider = React8.useMemo(() => resolve3(resolveProvider), [resolveProvider]);
5275
+ const provider = React9.useMemo(() => resolve3(resolveProvider), [resolveProvider]);
4282
5276
  const supported = typeof provider.listMediaFiles === "function";
4283
- const [files, setFiles] = React8.useState(null);
4284
- const [tab, setTab] = React8.useState("all");
4285
- const [uploading, setUploading] = React8.useState(false);
4286
- const [pendingDelete, setPendingDelete] = React8.useState(null);
4287
- const [deleting, setDeleting] = React8.useState(false);
4288
- const [replacing, setReplacing] = React8.useState(false);
4289
- const [openId, setOpenId] = React8.useState(null);
4290
- const [dimensions, setDimensions] = React8.useState({});
4291
- const reload = React8.useCallback(async () => {
5277
+ const [files, setFiles] = React9.useState(null);
5278
+ const [tab, setTab] = React9.useState("all");
5279
+ const [uploading, setUploading] = React9.useState(false);
5280
+ const [pendingDelete, setPendingDelete] = React9.useState(null);
5281
+ const [deleting, setDeleting] = React9.useState(false);
5282
+ const [replacing, setReplacing] = React9.useState(false);
5283
+ const [openId, setOpenId] = React9.useState(null);
5284
+ const [dimensions, setDimensions] = React9.useState({});
5285
+ const reload = React9.useCallback(async () => {
4292
5286
  if (!provider.listMediaFiles) {
4293
5287
  setFiles([]);
4294
5288
  return;
@@ -4300,10 +5294,10 @@ function ContentPage({ resolveProvider }) {
4300
5294
  setFiles([]);
4301
5295
  }
4302
5296
  }, [provider]);
4303
- React8.useEffect(() => {
5297
+ React9.useEffect(() => {
4304
5298
  void reload();
4305
5299
  }, [reload]);
4306
- const measure = React8.useCallback((id, d) => {
5300
+ const measure = React9.useCallback((id, d) => {
4307
5301
  setDimensions((prev) => prev[id] ? prev : { ...prev, [id]: d });
4308
5302
  }, []);
4309
5303
  const handleUpload = async (picked) => {
@@ -4493,7 +5487,7 @@ function SectionTitle({ title, count }) {
4493
5487
  ] });
4494
5488
  }
4495
5489
  function UploadButton({ uploading, onPick }) {
4496
- const inputRef = React8.useRef(null);
5490
+ const inputRef = React9.useRef(null);
4497
5491
  return /* @__PURE__ */ jsxs(Fragment, { children: [
4498
5492
  /* @__PURE__ */ jsx(
4499
5493
  "input",
@@ -4573,8 +5567,8 @@ function MediaDetail({
4573
5567
  onDelete,
4574
5568
  replacing
4575
5569
  }) {
4576
- const replaceRef = React8.useRef(null);
4577
- React8.useEffect(() => {
5570
+ const replaceRef = React9.useRef(null);
5571
+ React9.useEffect(() => {
4578
5572
  function onKey(e) {
4579
5573
  if (e.key === "ArrowLeft") onNavigate(-1);
4580
5574
  if (e.key === "ArrowRight") onNavigate(1);
@@ -4582,116 +5576,138 @@ function MediaDetail({
4582
5576
  document.addEventListener("keydown", onKey);
4583
5577
  return () => document.removeEventListener("keydown", onKey);
4584
5578
  }, [onNavigate]);
4585
- return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
4586
- /* @__PURE__ */ jsx(PageHeader, { title: file.name, parentLabel: "Conte\xFAdo", onBack }),
4587
- position.total > 1 && /* @__PURE__ */ jsx("div", { className: "flex items-center justify-end gap-3", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1 text-xs text-muted-foreground", children: [
4588
- /* @__PURE__ */ jsx(
4589
- "button",
4590
- {
4591
- type: "button",
4592
- "aria-label": "Arquivo anterior",
4593
- disabled: position.index === 0,
4594
- onClick: () => onNavigate(-1),
4595
- className: "rounded-md border p-1 transition hover:bg-muted disabled:opacity-40",
4596
- children: /* @__PURE__ */ jsx(ChevronLeft, { className: "h-4 w-4" })
4597
- }
4598
- ),
4599
- /* @__PURE__ */ jsxs("span", { className: "px-1 tabular-nums", children: [
4600
- position.index + 1,
4601
- " de ",
4602
- position.total
4603
- ] }),
4604
- /* @__PURE__ */ jsx(
4605
- "button",
4606
- {
4607
- type: "button",
4608
- "aria-label": "Pr\xF3ximo arquivo",
4609
- disabled: position.index === position.total - 1,
4610
- onClick: () => onNavigate(1),
4611
- className: "rounded-md border p-1 transition hover:bg-muted disabled:opacity-40",
4612
- children: /* @__PURE__ */ jsx(ChevronRight, { className: "h-4 w-4" })
4613
- }
4614
- )
4615
- ] }) }),
4616
- /* @__PURE__ */ jsxs("div", { className: "fz-split", children: [
4617
- /* @__PURE__ */ jsx(Card, { className: "flex min-h-[320px] items-center justify-center overflow-hidden bg-muted p-2", children: file.kind === "image" ? /* @__PURE__ */ jsx(
4618
- "img",
4619
- {
4620
- src: file.url,
4621
- alt: file.name,
4622
- className: "max-h-[70vh] w-auto max-w-full object-contain",
4623
- onLoad: (e) => {
4624
- const img = e.currentTarget;
4625
- if (img.naturalWidth) onMeasured({ width: img.naturalWidth, height: img.naturalHeight });
5579
+ return (
5580
+ // `fz-container` é o ponto de MEDIÇÃO: sem ele a container query de
5581
+ // `fz-split` nunca casa e as duas colunas ficam empilhadas para sempre,
5582
+ // por mais larga que a janela esteja.
5583
+ /* @__PURE__ */ jsxs("div", { className: "fz-container space-y-4", children: [
5584
+ /* @__PURE__ */ jsx(PageHeader, { title: file.name, parentLabel: "Conte\xFAdo", onBack }),
5585
+ position.total > 1 && /* @__PURE__ */ jsx("div", { className: "flex items-center justify-end gap-3", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1 text-xs text-muted-foreground", children: [
5586
+ /* @__PURE__ */ jsx(
5587
+ "button",
5588
+ {
5589
+ type: "button",
5590
+ "aria-label": "Arquivo anterior",
5591
+ disabled: position.index === 0,
5592
+ onClick: () => onNavigate(-1),
5593
+ className: "rounded-md border p-1 transition hover:bg-muted disabled:opacity-40",
5594
+ children: /* @__PURE__ */ jsx(ChevronLeft, { className: "h-4 w-4" })
4626
5595
  }
4627
- },
4628
- file.url
4629
- ) : /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center gap-2 p-12 text-muted-foreground", children: [
4630
- /* @__PURE__ */ jsx(KindIcon, { kind: file.kind, className: "h-10 w-10" }),
4631
- /* @__PURE__ */ jsx("span", { className: "text-sm", children: "Pr\xE9-visualiza\xE7\xE3o n\xE3o dispon\xEDvel" })
4632
- ] }) }),
4633
- /* @__PURE__ */ jsxs(Card, { className: "space-y-4 p-4", children: [
4634
- /* @__PURE__ */ jsxs("div", { children: [
4635
- /* @__PURE__ */ jsx("p", { className: "break-words font-medium", title: file.name, children: file.name }),
4636
- /* @__PURE__ */ jsx("p", { className: "mt-0.5 text-xs text-muted-foreground", children: /* @__PURE__ */ jsx(FileMeta, { file, dimensions }) }),
4637
- isLowResolution(file, dimensions) && /* @__PURE__ */ jsxs("p", { className: "mt-2 rounded-md bg-warning/10 px-2 py-1.5 text-xs font-medium text-warning", children: [
4638
- "Baixa resolu\xE7\xE3o \u2014 no zoom da loja essa foto fica borrada. Troque por uma de pelo menos ",
4639
- LOW_RESOLUTION_PX,
4640
- "px no lado maior."
4641
- ] })
4642
- ] }),
4643
- /* @__PURE__ */ jsxs("div", { children: [
4644
- /* @__PURE__ */ jsx("p", { className: "text-[11px] uppercase tracking-wide text-muted-foreground", children: "Onde \xE9 usada" }),
4645
- file.reference ? /* @__PURE__ */ jsx(RecordLink, { reference: file.reference, className: "mt-1 text-sm" }) : /* @__PURE__ */ jsx("p", { className: "mt-1 text-sm text-muted-foreground", children: "Biblioteca \u2014 este arquivo n\xE3o est\xE1 preso a nenhum registro." }),
4646
- file.reference?.isCover && /* @__PURE__ */ jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: "\xC9 a capa: \xE9 ela que aparece no cat\xE1logo, no carrinho e no pedido." })
5596
+ ),
5597
+ /* @__PURE__ */ jsxs("span", { className: "px-1 tabular-nums", children: [
5598
+ position.index + 1,
5599
+ " de ",
5600
+ position.total
4647
5601
  ] }),
4648
- /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
4649
- /* @__PURE__ */ jsxs(Button, { disabled: true, className: "w-full", title: "Em breve", children: [
4650
- /* @__PURE__ */ jsx(Sparkles, { className: "mr-2 h-4 w-4" }),
4651
- "Melhorar com IA"
5602
+ /* @__PURE__ */ jsx(
5603
+ "button",
5604
+ {
5605
+ type: "button",
5606
+ "aria-label": "Pr\xF3ximo arquivo",
5607
+ disabled: position.index === position.total - 1,
5608
+ onClick: () => onNavigate(1),
5609
+ className: "rounded-md border p-1 transition hover:bg-muted disabled:opacity-40",
5610
+ children: /* @__PURE__ */ jsx(ChevronRight, { className: "h-4 w-4" })
5611
+ }
5612
+ )
5613
+ ] }) }),
5614
+ /* @__PURE__ */ jsxs("div", { className: "fz-split", children: [
5615
+ /* @__PURE__ */ jsx(Card, { className: "flex min-h-[320px] items-center justify-center overflow-hidden bg-muted p-2", children: file.kind === "image" ? /* @__PURE__ */ jsx(
5616
+ "img",
5617
+ {
5618
+ src: file.url,
5619
+ alt: file.name,
5620
+ className: "max-h-[70vh] w-auto max-w-full object-contain",
5621
+ onLoad: (e) => {
5622
+ const img = e.currentTarget;
5623
+ if (img.naturalWidth) onMeasured({ width: img.naturalWidth, height: img.naturalHeight });
5624
+ }
5625
+ },
5626
+ file.url
5627
+ ) : /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center gap-2 p-12 text-muted-foreground", children: [
5628
+ /* @__PURE__ */ jsx(KindIcon, { kind: file.kind, className: "h-10 w-10" }),
5629
+ /* @__PURE__ */ jsx("span", { className: "text-sm", children: "Pr\xE9-visualiza\xE7\xE3o n\xE3o dispon\xEDvel" })
5630
+ ] }) }),
5631
+ /* @__PURE__ */ jsxs(Card, { className: "space-y-4 p-4", children: [
5632
+ /* @__PURE__ */ jsxs("div", { children: [
5633
+ /* @__PURE__ */ jsx("p", { className: "break-words font-medium", title: file.name, children: file.name }),
5634
+ /* @__PURE__ */ jsx("p", { className: "mt-0.5 text-xs text-muted-foreground", children: /* @__PURE__ */ jsx(FileMeta, { file, dimensions }) }),
5635
+ isLowResolution(file, dimensions) && /* @__PURE__ */ jsxs("p", { className: "mt-2 rounded-md bg-warning/10 px-2 py-1.5 text-xs font-medium text-warning", children: [
5636
+ "Baixa resolu\xE7\xE3o \u2014 no zoom da loja essa foto fica borrada. Troque por uma de pelo menos ",
5637
+ LOW_RESOLUTION_PX,
5638
+ "px no lado maior."
5639
+ ] })
4652
5640
  ] }),
4653
- /* @__PURE__ */ jsx(Button, { asChild: true, variant: "outline", className: "w-full", children: /* @__PURE__ */ jsxs("a", { href: downloadUrlOf(file), download: file.name, children: [
4654
- /* @__PURE__ */ jsx(Download, { className: "mr-2 h-4 w-4" }),
4655
- "Baixar"
4656
- ] }) }),
4657
- onReplace && /* @__PURE__ */ jsxs(Fragment, { children: [
4658
- /* @__PURE__ */ jsx(
4659
- "input",
4660
- {
4661
- ref: replaceRef,
4662
- type: "file",
4663
- accept: file.kind === "image" ? "image/*" : void 0,
4664
- className: "hidden",
4665
- onChange: (e) => {
4666
- const picked = e.target.files?.[0];
4667
- e.target.value = "";
4668
- if (picked) onReplace(picked);
4669
- }
4670
- }
4671
- ),
4672
- /* @__PURE__ */ jsxs(
4673
- Button,
5641
+ /* @__PURE__ */ jsxs("div", { children: [
5642
+ /* @__PURE__ */ jsx("p", { className: "text-[11px] uppercase tracking-wide text-muted-foreground", children: "Onde \xE9 usada" }),
5643
+ file.reference ? /* @__PURE__ */ jsx(RecordLink, { reference: file.reference, className: "mt-1 text-sm" }) : /* @__PURE__ */ jsx("p", { className: "mt-1 text-sm text-muted-foreground", children: "Biblioteca \u2014 este arquivo n\xE3o est\xE1 preso a nenhum registro." }),
5644
+ file.reference?.isCover && /* @__PURE__ */ jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: "\xC9 a capa: \xE9 ela que aparece no cat\xE1logo, no carrinho e no pedido." })
5645
+ ] }),
5646
+ /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
5647
+ file.kind === "image" && /* @__PURE__ */ jsx(
5648
+ AskAIButton,
4674
5649
  {
4675
- variant: "outline",
4676
5650
  className: "w-full",
4677
- disabled: replacing,
4678
- onClick: () => replaceRef.current?.click(),
4679
- children: [
4680
- /* @__PURE__ */ jsx(RefreshCw, { className: `mr-2 h-4 w-4 ${replacing ? "animate-spin" : ""}` }),
4681
- replacing ? "Trocando\u2026" : "Trocar arquivo"
4682
- ]
5651
+ size: "lg",
5652
+ label: "Melhorar com IA",
5653
+ media: { url: file.url, name: file.name },
5654
+ draft: "Melhora essa imagem",
5655
+ hint: "o que voc\xEA quer mudar nessa foto",
5656
+ intro: {
5657
+ persona: "Est\xFAdio de imagem",
5658
+ pitch: `Vamos melhorar ${file.name}.`,
5659
+ description: "Posso limpar o fundo, corrigir luz e cor, e gerar uma vers\xE3o em resolu\xE7\xE3o maior para o cat\xE1logo.",
5660
+ suggestions: [
5661
+ { label: "Fundo branco", prompt: "Remove o fundo e deixa branco" },
5662
+ { label: "Mais n\xEDtida", prompt: "Aumenta a resolu\xE7\xE3o e a nitidez" },
5663
+ { label: "Corrigir a luz", prompt: "Corrige a ilumina\xE7\xE3o e as cores" }
5664
+ ]
5665
+ }
4683
5666
  }
4684
5667
  ),
4685
- file.reference && /* @__PURE__ */ jsx("p", { className: "text-[11px] text-muted-foreground", children: "A troca mant\xE9m a posi\xE7\xE3o da foto na galeria do produto." })
4686
- ] }),
4687
- onDelete && /* @__PURE__ */ jsxs(Button, { variant: "ghost", className: "w-full text-destructive hover:text-destructive", onClick: onDelete, children: [
4688
- /* @__PURE__ */ jsx(Trash2, { className: "mr-2 h-4 w-4" }),
4689
- "Excluir"
5668
+ /* @__PURE__ */ jsx(Button, { asChild: true, variant: "outline", className: "w-full", children: /* @__PURE__ */ jsxs("a", { href: downloadUrlOf(file), download: file.name, children: [
5669
+ /* @__PURE__ */ jsx(Download, { className: "mr-2 h-4 w-4" }),
5670
+ "Baixar"
5671
+ ] }) }),
5672
+ onReplace && /* @__PURE__ */ jsxs(Fragment, { children: [
5673
+ /* @__PURE__ */ jsx(
5674
+ "input",
5675
+ {
5676
+ ref: replaceRef,
5677
+ type: "file",
5678
+ accept: file.kind === "image" ? "image/*" : void 0,
5679
+ className: "hidden",
5680
+ onChange: (e) => {
5681
+ const picked = e.target.files?.[0];
5682
+ e.target.value = "";
5683
+ if (picked) onReplace(picked);
5684
+ }
5685
+ }
5686
+ ),
5687
+ /* @__PURE__ */ jsxs(
5688
+ Button,
5689
+ {
5690
+ variant: "outline",
5691
+ className: "w-full",
5692
+ disabled: replacing,
5693
+ onClick: () => replaceRef.current?.click(),
5694
+ children: [
5695
+ /* @__PURE__ */ jsx(RefreshCw, { className: `mr-2 h-4 w-4 ${replacing ? "animate-spin" : ""}` }),
5696
+ replacing ? "Trocando\u2026" : "Trocar arquivo"
5697
+ ]
5698
+ }
5699
+ ),
5700
+ file.reference && /* @__PURE__ */ jsx("p", { className: "text-[11px] text-muted-foreground", children: "A troca mant\xE9m a posi\xE7\xE3o da foto na galeria do produto." })
5701
+ ] }),
5702
+ onDelete && /* @__PURE__ */ jsxs(Button, { variant: "ghost", className: "w-full text-destructive hover:text-destructive", onClick: onDelete, children: [
5703
+ /* @__PURE__ */ jsx(Trash2, { className: "mr-2 h-4 w-4" }),
5704
+ "Excluir"
5705
+ ] })
4690
5706
  ] })
4691
5707
  ] })
4692
5708
  ] })
4693
5709
  ] })
4694
- ] });
5710
+ );
4695
5711
  }
4696
5712
  function RecordLink({ reference, className = "" }) {
4697
5713
  return /* @__PURE__ */ jsxs(
@@ -4730,16 +5746,16 @@ var BLING_SAMPLE_SIZE = 100;
4730
5746
  var BLING_SIGNATURE_HEADER = "x-bling-signature-256";
4731
5747
  var BLING_WEBHOOK_DEADLINE_MS = 5e3;
4732
5748
  var BLING_FUNCTION = "bling-sync";
4733
- function client() {
5749
+ function client2() {
4734
5750
  const supabase = getSupabaseClientOptional();
4735
5751
  if (!supabase) throw new Error("Sem conex\xE3o com o banco para falar com o Bling.");
4736
5752
  return supabase;
4737
5753
  }
4738
- async function invoke(body) {
5754
+ async function invoke2(body) {
4739
5755
  const tenantId = getShopTenantId();
4740
5756
  if (!tenantId) throw new Error("Sem loja selecionada para sincronizar com o Bling.");
4741
5757
  const runtimeToken = await connectorRuntimeToken({ tenantId });
4742
- const { data, error } = await client().functions.invoke(BLING_FUNCTION, {
5758
+ const { data, error } = await client2().functions.invoke(BLING_FUNCTION, {
4743
5759
  body: { ...body, tenantId },
4744
5760
  headers: { [CONNECTOR_RUNTIME_TOKEN_HEADER]: runtimeToken }
4745
5761
  });
@@ -4747,19 +5763,19 @@ async function invoke(body) {
4747
5763
  return data;
4748
5764
  }
4749
5765
  function pullCatalogue(mode) {
4750
- return invoke({ action: "pull_products", ...{ mode } });
5766
+ return invoke2({ action: "pull_products", ...{ mode } });
4751
5767
  }
4752
- function pushCatalogue() {
4753
- return invoke({ action: "push_products" });
5768
+ function pushCatalogue2() {
5769
+ return invoke2({ action: "push_products" });
4754
5770
  }
4755
5771
  function pushCustomers() {
4756
- return invoke({ action: "push_customers" });
5772
+ return invoke2({ action: "push_customers" });
4757
5773
  }
4758
5774
  function pushOrders() {
4759
- return invoke({ action: "push_orders" });
5775
+ return invoke2({ action: "push_orders" });
4760
5776
  }
4761
5777
  function pushStock() {
4762
- return invoke({ action: "push_stock" });
5778
+ return invoke2({ action: "push_stock" });
4763
5779
  }
4764
5780
  async function countRows(table, apply) {
4765
5781
  const tenantId = getShopTenantId();
@@ -4779,7 +5795,7 @@ async function pendingCustomers() {
4779
5795
  if (!tenantId || !supabase) return null;
4780
5796
  const { data: linked } = await supabase.from("plg_shop_customer_links").select("customer_id").eq("tenant_id", tenantId).eq("source", "bling");
4781
5797
  const already = new Set((linked ?? []).map((row) => row.customer_id));
4782
- const { data, error } = await supabase.from("plg_shop_customers").select("id").eq("tenant_id", tenantId).gt("orders_count", 0);
5798
+ const { data, error } = await supabase.from("v_shop_customers").select("id").eq("tenant_id", tenantId).gt("orders_count", 0);
4783
5799
  if (error) return null;
4784
5800
  return (data ?? []).filter((row) => !already.has(row.id)).length;
4785
5801
  }
@@ -4789,7 +5805,7 @@ async function pendingOrders() {
4789
5805
  if (!tenantId || !supabase) return null;
4790
5806
  const { data: linked } = await supabase.from("plg_shop_order_links").select("order_id").eq("tenant_id", tenantId).eq("source", "bling").eq("kind", "sales_order");
4791
5807
  const already = new Set((linked ?? []).map((row) => row.order_id));
4792
- const { data, error } = await supabase.from("plg_shop_orders").select("id").eq("tenant_id", tenantId).neq("status", "cancelled");
5808
+ const { data, error } = await supabase.from("v_shop_orders").select("id").eq("tenant_id", tenantId).neq("status", "cancelled");
4793
5809
  if (error) return null;
4794
5810
  return (data ?? []).filter((row) => !already.has(row.id)).length;
4795
5811
  }
@@ -4797,7 +5813,7 @@ async function listOrderDeliveries(limit = 10) {
4797
5813
  const tenantId = getShopTenantId();
4798
5814
  const supabase = getSupabaseClientOptional();
4799
5815
  if (!tenantId || !supabase) return [];
4800
- const { data: orders } = await supabase.from("plg_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);
5816
+ 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);
4801
5817
  const rows = orders ?? [];
4802
5818
  if (rows.length === 0) return [];
4803
5819
  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));
@@ -4836,7 +5852,7 @@ async function listOrderDeliveries(limit = 10) {
4836
5852
  });
4837
5853
  }
4838
5854
  function reconcile() {
4839
- return invoke({ action: "reconcile" });
5855
+ return invoke2({ action: "reconcile" });
4840
5856
  }
4841
5857
 
4842
5858
  // src/integrations/bling/connectorDef.tsx
@@ -5062,7 +6078,7 @@ var blingConnectorDef = {
5062
6078
  },
5063
6079
  pending: pendingProducts,
5064
6080
  run: async () => {
5065
- const result = await pushCatalogue();
6081
+ const result = await pushCatalogue2();
5066
6082
  return { message: result.message, status: result.status };
5067
6083
  }
5068
6084
  },
@@ -5275,6 +6291,311 @@ async function acceptWebhook(request, options) {
5275
6291
  }
5276
6292
  return { status: 200, queued: true };
5277
6293
  }
6294
+
6295
+ // src/integrations/neochef/limits.ts
6296
+ var NEOCHEF_CONNECTOR_ID = "neochef";
6297
+ var NEOCHEF_RATE_LIMIT = { perSecond: 5, perDay: 5e4 };
6298
+ var NEOCHEF_PAGE_SIZE = 100;
6299
+ var NEOCHEF_SAMPLE_SIZE = 100;
6300
+ var NEOCHEF_CURSOR_OVERLAP_MS = 6e4;
6301
+ var NEOCHEF_TOKEN_LEAD_MS = 10 * 6e4;
6302
+
6303
+ // src/integrations/neochef/mark.ts
6304
+ var NEOCHEF_MARK = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAATp0lEQVR42u1da2xcx3WeMzP3sU/uksunKIl6y7JkyZYsSzYtuUlrGE1ctClao3kVadMkRVPYTQIXbdKkSIPWBYqkSZ00qYOmllGgRpu0aOEisBPXEuVHJFmmLFsv6kFJlCi+ltz3fcyd0x+zd3dJy7YSU+Ry9w5ACAtSd++955vvnPnmnDNAbuIYflpHEoz3PPoecuBmXRsCgzc3ICAwfHMDAQLDNzcQIDB8cwOBBsZvnPGL2IYGxm9uEEBg+OZ2CTQwfnOzAQ2M39wgoMErau5Bg9nf3CxAA+M3NwhoYPzmBkEQAwQxQDD7m5kFAgYIGCCY/c3MAgEDBAwQzP5mZoGAAQIGCEYAgGA07YDA/zf34I0JayoJIYSgpIQQQvXWaSO166yZ6i9qia1RLdrXBlpLtJwQg+hmC6JwccqZOZazJ1807clX1nr2RFv5YkiASoIeCxig/h9nlrGM1O7j0b6PzphdD6zh4d6eG72KZ10bK117dqgw/FTUGj+wTV2aeQpQCAEA6nXWl2e80bbzzZZNXyyGuh+4s/J7lBJRCEIIAaCUEEoJABBEJERKRCnV7xgjwCqz3Rp7/mjmxF8za2Jg69zvCQBQN0/BPIIeA6o7ic1/+XJ8wyP9yoiIKF0XgFIC/MbdHQqBKCUA5wQoJYSQ3NB3Dkwf/9J2FIWI/30BAOrI+Dy84kpq1760kdq9RRleCDWblQE9a2zcnnzpvJ0+bLvZ0yFpT4ZRWhyoIajRXtRi60pG6w7dSN29ioV6uqqs4Xn+dZzpwVOTr3zUdHNDfY0CgqUNgLIR9MSWM+39/xXl4d4eNeMB/BlvTQwcy5/7p0Lp2nObpDOdeNd1sdaSNTvffyK25pO62fn+O6qMgAhU0zx7YnLi4IfG7KlDtzYCCJYuAMovX4ttON/5vp/GmdGeIigEIQAEGHNzZy7MvP7F8eKV/75r1v8hgNVADkF9LgeQBKHWoKHuBw4ntnytRU9sWU/Q8whBJMC5dGcyYy88MOpMv7ZxqYNgiQIAkAAg1RKZ7l8+mOXRNStrjZ+/8OTB6cEvbJVuNqaWhIA3HsGXVxIEgaCkwEwrufVvD8XWfmZPLQi80pXR0efupp411kHAvz4JlMCFjPhTO78/xKNrVqJ0Xd/4M8e/8sLU4U/1K+NzQVBSNUNvdPlWZgGUlADz0LPM9NGH90wPfmG/CiwBULouCy3rTu3aN0oAULEHYACAmznj/R/ggqDHoqs+MRDq+cDOqs9Xxs+cfOw+AlyoWS/em9CFHvO/M3vmH/amX/v8fgKMAVCK0nXNjr3b4uv+eICgx6ruZWmBgdbtDAcuZvvs8g8KTo1UOnnb124lBNWLBs7zF548mDn52H1ANffnm/E3wgiCA9Xc3NDje3ND3y6DAICglC2b/2I7Cy0bVWCruU8/5pj1HAEA3sX3clGRcFFw35BADZvyWJ5q8RwhhMTXP/w6NVKtKF0XKOcif3Y4ffSROwgwD+fV+DUwkIITYF568NF7nJljpwlwjigE5bFoyy2PDhFCCOWxPOWxPDDTqjBIzXNUmCkIAq8f0fsf9eQdJ82OPeNG606dR1cnqZ6MADN1gp6UTqbAo6t7gRkGQSkJMDZ+4MEjpWvP7rjpEXn5+kbq7uNd73t+s/p+5Q5E/txl4LEQUM7Qs13pzhRE/vy0nT5i2xMHUvbU4U2zmKFOVg6LDAAVzROUFHi0EF35kVejq363XW/dfssN+GePAGPW+P7BsRfu37ZgL7X8PR39PzwU6vngTv8+3u2/OTPHTheGn7qWv7Dvdulm4iqQrXEXTecCapZakZW/82LP/T+baN3+rT166/ZbCEqJnmWp6P5t7F9ekmXPfNNZ+MALMHvmmzohiP59XN9tuC56lkXQ8/TE1g3JbX+3t/v+w9no6t8bqCxL/Z3LpmKA8iyiWiLTuuPxE5Hlv7VbvTDHIQQAqKb5fyrdbE7ak9NS5C2gOqd6a4wZqTYClEpnKj3yP2tC6FlmVdhZKOZi3rIPnBrn4eU9hCB69uSUdNJZ9CyX8liIGm0JqrXE37K/QHWdEEJKV585NHX406s9eyK1mC5h4QHga/eRlSMd/T8qaS2b19VKrYQQ4mZPnS+O/Ocla+z5pJs71ePZ6aS/pKNaS5aFeibMjr1XCDDMDX1772x1b+F0iOjqTw4AM6Q1vr/HK450SHemxX9GprdO89j6q2bHfenI8g8t01o2r/NZwZeqRf7s8PjAh6SbO716sUCwsAAovzgeXn61875nBY+uXlHdrWPMzZ46nzn52Gjx8g/vROnopFEGcBFe9sEjLZu+2Konbltf2WSimuaVRsfGXri/6ObOrFqMreYFBIAK+ICFS13ve+GKntiyXhlf7bTlzv7jgenXv7QdRT7ivzQ/RphN7+XAkfgxxCJG00Clug9JZwd05XikIkMr9gJmWonNX/lZfMPn9lZ2LKmmify5i6M/uTcm3fJm1QKCgC707G/b/q3BqvEpJUBp+rU/OZA++sgeFPnILBWvsqavpfcyKCq/X8RRuY+5+wy+aFXWAcpuAT3LnD72Z3unDn/qoJ98gtJ1eXTNyrY7vzekrrOwOgFdSL8f6vngocjKj9xDUAhfvk2/9rn9uaHv7AGqudXZgg2VdlUrKwPV3PyFJ/unDn3qRQKU+rJyeNmDd0VWfvjFqqzcSABASYEadnLrYx1q6aR21PLnvz+QG/r2XqCai7JBDT9HVkbpakA1Nz+8rz97+hv7qxoCYvK2r60GHik0oAtACPf++qtabF2f8nuci8LFkfTgo0q+bQrjv1VWnjn+5d1u9uQ5oJz7O4zRlR8+upDvYsGQFl39+2ElmCASAjDz5lcvoihEFnwJVydMQAggSkefOf7lSUIA1A9idNUnUgspDi0IALTYumGj7a6NKhbUdVG4OFK89O87lM9vzHz7G4oJgMri1Wd2uJkTZ4FqGkFEPbltvZ7YdrqhAGB2vv8SMNNUSh8hxZEfnUNpGyrYabbZX8sCqoahcOnpERUFuC4BxkJd9481FACM1O5yYobK0C1dey7WnNR/PRAQYo0/366WhSqR1UjtjjSYC9jYquhf09ArldzsyZ7ZIk+z2l89v5sbWiad9LS/ItBi6zoaCgDM7EhUy67Gp6Q9lSTBqDCAdGZaPGssXTGKkUo21iqgZncPpe02lM5P5iPfEUG6mZKvBwDVeAPnAwAERn8rCwBwuhjvZ0EAgKJQrJJBLAQ8UgwMX7tpxAXVk5Hq+ypaDQUArzgy7dMbM1JtzOyaKIM9aE5BCGFm+xQLdbf7n0VpdLKhAOBk3pipZPwA50brHVcV+hc3Hao+GlkA6omtl4CFwyoFDtHNvDHRUACwJg5otTpAqOdB8tZt3uYNAEM9v1oJAAkBsCYGGksKtsb3b5D2ZFqlfCGGuh/YwsyuMVV+1awsoGRwqiUykd7f3FzRSUS+YF17dk1DAUDak62l0R+f9N0A1eKx+IZHTlXk0Ka0v5LBY2s/85oqcrFtQggpjT3/hihe7mm4ZWDu3PcSFbkTpYyt/cO7tJZNZwmqrdGm8/0oOI+sHIlv/Px2glJCufA0d/Y7ekNmBNlTh24tXX3mEAHGEIUAZpqpnT8QwEyrqVyB/5zAvNTOf05TLR5DFIIA59bEgWPW2P/d3nDbwf7+d3rw0Q70SiUAxggKoSe3bUzd9S+DFV3crw1sZNpHSQlK2nbnd1822vtvU+lx6n1Mv/YFc5Y+0EgpYQSoFPmzfdODf3pYsQAiQSHCvb+xq6P/Pw5TrSVbcQeN5hL8amf0GLBwMbVr30vRvo/3+/UQBBibPv6VF52ZYxsWuj5ggesCuCAoeOv2xwdia/7g3toiCTd78lz66MM5a3z/tll/X5tlW2njUoe7iBXQzmk7U1OtbKTuPt56+zc0PbltY20xTOHS0y9NvvLxuxejOGSBK4P8Ro6Spnbtezmy4rfvJuXefX5Tp8Klp1/Knf1uzJ58eXOj6AR6644TsbWfTkf7PnYPIQC1z1y6+syhiZceuh3RY4tRLLoItYHViuDW27++P7buj/b6y0OlE6jNEGd68JQ1MTDmTB/longphm7WBBZyCEqw04dvrSchB3ikYLbvOSGdqTB6NgcetXl4WV5P3uGa7fe26607NvlCj+ppoOoD8xd+MDB15LO7q7N+4QG/SOXhVRBE+z52MLH1b25hRntbebvYVg0ar19yLQoXLl/5303LqkUUi8gSfpu65LZT3b/ys43vEANJRNcFahiq4HUmM/P6l17PnXvi3qq7WJznoIu2BVpuwpQffqr/2nO77Ny5JwbQKxaBGgYBxghKidK2Udp2pTsXQaRaPEK1RKaeKJ6FerMEpaxQOwpRvXfVRAKoYaC07fzwUwdHn92VzZ174t7qcm/xQMwXPzOWeaI40pN+9bM92dN/Pxzt+/BwqOfXevSWW9f6M6ZWJ6daMsHMjmHppJOKRRYzTlCzl0dWlghQSsr9hglwDpXWtIhu5sTZ4ugzI4Xhf13uZk/211OXEF4vJVP+MnHmja/2zbzxV6jFN57XE1vHeHSNUymvLgsmzOzKuNlTpF767fBwr+/hEYCQ0uiPDzvpIwU3f15zZgY73eypVQS9tbUScL2kw/O6yYop58mrVGnB3ezJNW72pNoUQfeFxJbN61AKAYxzFuouLrRg8s77+d2z3uP063+edDNv3vmWJTDxexaS4MCIt8+SLQd3AAhUd1AKLp2ZWbEKM9rrRDFUegQzO0OVVvPoeUS6jAAXAICV5d177VnYXCeGIBBUhZQEPebZE3o5gwhU1mx7fWQSleMPqreGy4oflW4mK53pOEHBEeu/9mFJ5OVLJ23WJkxSPcnqRQMgwAXV4uHa/EfpFU0SdAqdRwCIvFE96YMQqif4Yi+fKi+QhSzgYaMa0xatatOqAADzkjKNoqj7y0DVkTNaNwAgVHeAmWYVrAV7MZW9hmQA9CwdpRD+6R/Awhqpmz0g3QVq6NV7Lbn1tEJZ2gAoB1koLZ1guSW8yp1jtb9fHMsrAwM1HP98ojJY3aWU8r40ijPnNuKkGq03N1XT/UMG5wXc9PwKU6uje3FUnqNCKcDSKn1bouXZWE9oRBWbYHBiyAIqhrJ+AtSSriqeaHBm0MK99KJbx680cAHzHmkz0yGgqorq8CZnf+IhbdFXKI3GAEAN1VMY6xAAKJhySeUlKisDgAQAmMdk4oitfKzy/VIUnUVfa1c0CltTGUu+MBTSq5k+9a8F0KXQQIHyqOMnXNTdKmDO/VAeMYGaFiFBDDCPcnvCqlWE0LNEvcwwlK7mF3aW2SoMPBwAYF4ZwEg5syRBdLGeklgQJVYZIBqhPFZYKnIwXRotVDo9P+muzABeHTEAJ+UOqCoDmHNqtOWCGGBe06676eyX7tQRAwiO0hXVk8wIYWZHIXAB87jRwkO9Zm1KGHql+lECpauhdESti+Kh3tJSYQC+FFqpstCy+Bz5FW+s/drc8wRvxCD++r38L875PKfknRAE9OxZyiSLrFgyO4K83hsoUR7Ls1BPW22TKRRFJAQQgEoktQcz+5XDc403fx29Zpdy+cmr1iwAaJFVRgCA+ZCBEYGFuiaZ0bacEESCnkcQQIoCqONXrt9yFphpUR7LUz2Zo1pLEbS4TbWYAzzqAdUlMBOB6khQEgKUoCgBogsoigy9ApNuxpBu1pROJiLdTFS62dg7pXWjKDgEhVCKICKPrEpUi14CALynJSALr0gTAisIAfBz71DkKAHm8ciqyzyyIq1F1+V5bB3yyEqTh3pi1OxMUK0lRnkkeSPn+r69B3IcFIWidNJXPOtaRhQv50Vh2BG5s9zNn20RhYvtXulqF0ohCXAOTJWDsXBvG/BoQR2BV9+p4XUPAKNtV5YApZ41PuHmTl910odnjPY9Wmz9w5d5eHkXMLPvXdOJ0D+qpnxg1XUJx0/kKB/fAgBAdR10Xad6MsGja8hcXpduJisKw6ftiRcLucL5A0brzg4tvn4FMzs7tPiGk0761VtUP4T6ZYI6BoAKAO2pV+LjBx48YqePrJFOeishhHT+0k+OabF1fcq4UhIiJVaMXGtS36jqfMJ3z9fxhSYpVUm3f73rXBcYo1o8pie2xvMX9l3LDT2+lwCVPLTsmpG6ZxDdXPdS2BTi9b4CsMZ+esfckzopC2l+W1XVVILznycRq9KaZvaOE6906i67jXe8JkqpEkEYY6Eu9NvZiOLlHnHp33rmPkcAgPccfasIHyjzWKg7CbPPILClPTXtla6mPetaXhRHLM8ak541xqST1qSb0VHkDSkKBpEuQ+nwuXv6wAwXqC6AR23KozbVEw7V2wQLdUlmdjIW6g3zUHecmp2tTG9LEqDUj0mY0cFVgysuqgWu1d5AAQDmgQn8mAB4LO+VRqftqUMXnfSrlpN9MyLy59u84pUOKXJdNxuMVG+d4eHeM1psfVprudUxWnfGCfH3AlQLON99LYWxSC1igkECKTgYdQGAvoec4AiXJh19DzkQMEDgAoIRACAYzQ2AIA5oTv8fMEAwqgAIWKD5Zn/AAMGYDYCABZpr9gcMEIy3AiBggeaZ/W/LAAEImsP47+gCAhA0vvGDGCAY7wyAgAUae/bfEAMEIGhc4xMyt8HNu4wge6hxDP8LxQABGzSW8X+hIDAAQeMY/+d2AYFLaBzDzwsAAiAsXcPPKwACICw9w98UAASAqH+Dzx3/Dzwey48k2VW1AAAAAElFTkSuQmCC";
6305
+ var NEOCHEF_FUNCTION = "neochef-sync";
6306
+ function client3() {
6307
+ const supabase = getSupabaseClientOptional();
6308
+ if (!supabase) throw new Error("Sem conex\xE3o com o banco para falar com o NeoChef.");
6309
+ return supabase;
6310
+ }
6311
+ async function invoke3(body) {
6312
+ const tenantId = getShopTenantId();
6313
+ if (!tenantId) throw new Error("Sem loja selecionada para sincronizar com o NeoChef.");
6314
+ const runtimeToken = await connectorRuntimeToken({ tenantId });
6315
+ const { data, error } = await client3().functions.invoke(NEOCHEF_FUNCTION, {
6316
+ body: { ...body, tenantId },
6317
+ headers: { [CONNECTOR_RUNTIME_TOKEN_HEADER]: runtimeToken }
6318
+ });
6319
+ if (error) throw new Error(error.message);
6320
+ return data;
6321
+ }
6322
+ function testNeoChef() {
6323
+ return invoke3({ action: "test" });
6324
+ }
6325
+ function pullCatalogue2(mode) {
6326
+ return invoke3({ action: "pull_products", ...mode ? { mode } : {} });
6327
+ }
6328
+ function pushOrders2() {
6329
+ return invoke3({ action: "push_orders" });
6330
+ }
6331
+ function reconcile2() {
6332
+ return invoke3({ action: "reconcile" });
6333
+ }
6334
+ async function pendingOrders2() {
6335
+ const tenantId = getShopTenantId();
6336
+ const supabase = getSupabaseClientOptional();
6337
+ if (!tenantId || !supabase) return null;
6338
+ const { data: linked } = await supabase.from("plg_shop_order_links").select("order_id").eq("tenant_id", tenantId).eq("source", "neochef").eq("kind", "kitchen_order");
6339
+ const already = new Set((linked ?? []).map((row) => row.order_id));
6340
+ const { data, error } = await supabase.from("v_shop_orders").select("id").eq("tenant_id", tenantId).neq("status", "cancelled");
6341
+ if (error) return null;
6342
+ return (data ?? []).filter((row) => !already.has(row.id)).length;
6343
+ }
6344
+ async function listOrderDeliveries2(limit = 10) {
6345
+ const tenantId = getShopTenantId();
6346
+ const supabase = getSupabaseClientOptional();
6347
+ if (!tenantId || !supabase) return [];
6348
+ 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);
6349
+ const rows = orders ?? [];
6350
+ if (rows.length === 0) return [];
6351
+ 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));
6352
+ const byOrder = new Map(
6353
+ (filed ?? []).map((row) => [row.order_id, row])
6354
+ );
6355
+ return rows.map((order) => {
6356
+ const subject = order.order_number ? `Pedido ${order.order_number}` : "Pedido";
6357
+ const link = byOrder.get(order.id);
6358
+ if (link) {
6359
+ return {
6360
+ subject,
6361
+ subjectId: order.id,
6362
+ status: "delivered",
6363
+ externalId: link.external_number ?? link.external_id,
6364
+ at: link.synced_at ?? order.paid_at,
6365
+ ...order.customer_name ? { detail: String(order.customer_name) } : {}
6366
+ };
6367
+ }
6368
+ if (String(order.status ?? "") === "cancelled") {
6369
+ return {
6370
+ subject,
6371
+ subjectId: order.id,
6372
+ status: "failed",
6373
+ detail: "Cancelado \u2014 n\xE3o sobe para a cozinha.",
6374
+ at: order.created_at
6375
+ };
6376
+ }
6377
+ return {
6378
+ subject,
6379
+ subjectId: order.id,
6380
+ status: "pending",
6381
+ detail: "Ainda n\xE3o subiu \u2014 vai na pr\xF3xima sincroniza\xE7\xE3o.",
6382
+ at: order.paid_at ?? order.created_at
6383
+ };
6384
+ });
6385
+ }
6386
+
6387
+ // src/integrations/neochef/connectorDef.tsx
6388
+ var neoChefConnectionStore = () => createConnectionStore(NEOCHEF_CONNECTOR_ID, {
6389
+ settingsKeys: ["kitchenId", "backfillFrom", "stockOwner"]
6390
+ });
6391
+ var neoChefConnectorDef = {
6392
+ id: NEOCHEF_CONNECTOR_ID,
6393
+ hostPluginId: "shop",
6394
+ name: "NeoChef",
6395
+ description: "Traz o card\xE1pio, o pre\xE7o e o estoque do NeoChef para a loja e devolve cada pedido pago como pedido na cozinha.",
6396
+ // The first question anyone connecting asks, answered before they connect.
6397
+ howItSyncs: [
6398
+ "De tempos em tempos, sozinho: desce do NeoChef o que mudou l\xE1 e sobe o que estiver pendente daqui.",
6399
+ "A qualquer momento, no bot\xE3o Sincronizar agora.",
6400
+ "Nada sobe duas vezes: cada pedido guarda o n\xFAmero que recebeu na cozinha.",
6401
+ "Produto desativado no NeoChef sai de venda na loja \u2014 nenhum produto \xE9 apagado."
6402
+ ],
6403
+ icon: "ChefHat",
6404
+ // The app's own mark, so the row wears the same face the consent screen does.
6405
+ // `icon` stays as the fallback the mark resolver advances to — see mark.ts.
6406
+ logoUrl: NEOCHEF_MARK,
6407
+ authKind: "oauth",
6408
+ // `erp` and not `ecommerce`: NeoChef is the back office, not the storefront.
6409
+ // Same reading of §2 that moved Bling off the word 'commerce'.
6410
+ category: "erp",
6411
+ // Both ways, and the connector says so before anyone connects: the catalogue
6412
+ // comes in, the paid order goes out.
6413
+ direction: "inbound",
6414
+ planes: ["sync"],
6415
+ docsUrl: "https://fayz.ai/docs/integrations/neochef",
6416
+ /**
6417
+ * The consent names these, entity by entity, on NeoChef's own screen — this
6418
+ * list is what the STORE says it will do with them, which is the other half
6419
+ * of the same sentence.
6420
+ *
6421
+ * `shop_product` is read as well as written because the catalogue is a
6422
+ * MIRROR: `reconcileMirror` has to see what is here before it can decide what
6423
+ * a product missing from a page means.
6424
+ */
6425
+ permissions: [
6426
+ {
6427
+ kind: "data",
6428
+ resource: "shop_product",
6429
+ actions: ["read", "write"],
6430
+ reason: "Trazer do NeoChef o card\xE1pio, o pre\xE7o e o saldo do estoque, comparando com o que j\xE1 est\xE1 na loja para n\xE3o duplicar nem tirar de venda por engano."
6431
+ },
6432
+ {
6433
+ kind: "data",
6434
+ resource: "shop_order",
6435
+ actions: ["read", "write"],
6436
+ // `write` is the LINK, not the order: without it every sync would file the
6437
+ // same sale again.
6438
+ reason: "Ler o pedido pago para lan\xE7\xE1-lo na cozinha, e guardar aqui o n\xFAmero que ele recebeu l\xE1. O conte\xFAdo do pedido n\xE3o \xE9 alterado.",
6439
+ sensitive: true
6440
+ },
6441
+ {
6442
+ kind: "event",
6443
+ resource: "shop.order_paid",
6444
+ actions: ["listen"],
6445
+ reason: "Saber que um pedido foi pago, para mand\xE1-lo para a cozinha."
6446
+ },
6447
+ {
6448
+ kind: "event",
6449
+ resource: "shop.order_cancelled",
6450
+ actions: ["listen"],
6451
+ reason: "Saber que um pedido foi cancelado, para n\xE3o mand\xE1-lo para a cozinha."
6452
+ }
6453
+ ],
6454
+ // Declared, so the scheduler owns the clock. The budget is ours and not the
6455
+ // provider's — see limits.ts.
6456
+ rateLimit: NEOCHEF_RATE_LIMIT,
6457
+ // A connect screen that waits for a whole catalogue is a connect screen nobody
6458
+ // finishes, so the first load answers with a page and the backfill follows.
6459
+ firstLoad: { sampleSize: NEOCHEF_SAMPLE_SIZE, samplePages: 1, stream: "products" },
6460
+ // With underscores. `shop.order.paid` is not a key anything publishes — the
6461
+ // shop builds its facts as `shop.${def.event}` over SHOP_EMAIL_EVENTS.
6462
+ subscribesTo: ["shop.order_paid", "shop.order_cancelled"],
6463
+ managedAutomations: [
6464
+ { eventKey: "shop.order_paid", label: "Mandar o pedido pago para a cozinha do NeoChef" },
6465
+ { eventKey: "shop.order_cancelled", label: "Parar de mandar para a cozinha um pedido cancelado" }
6466
+ ],
6467
+ // What HAPPENS, not what is declared. Nothing here names a moment that has no
6468
+ // code behind it.
6469
+ invokedAt: [
6470
+ {
6471
+ key: "shop.order.push",
6472
+ label: "O pedido pago sobe como pedido na cozinha",
6473
+ plane: "sync",
6474
+ trigger: "scheduled",
6475
+ touches: ["shop_order"]
6476
+ },
6477
+ {
6478
+ key: "shop.catalogue.pull",
6479
+ label: "O card\xE1pio, o pre\xE7o e o estoque do NeoChef descem para a loja",
6480
+ plane: "sync",
6481
+ trigger: "scheduled",
6482
+ touches: ["shop_product"]
6483
+ }
6484
+ ],
6485
+ // Read from the row the list already batched — one request for N connectors.
6486
+ ...connectionStatus(
6487
+ NEOCHEF_CONNECTOR_ID,
6488
+ (connection) => {
6489
+ if (!connection?.active) return { connected: false };
6490
+ const kitchen = connection.settings?.kitchenId;
6491
+ return {
6492
+ connected: connection.status === "connected",
6493
+ detail: kitchen ? `Cozinha ${kitchen}` : void 0,
6494
+ plane: "sync"
6495
+ };
6496
+ },
6497
+ { store: neoChefConnectionStore }
6498
+ ),
6499
+ // The whole connect step. Declaring `oauth` and no `startOAuth` is what renders
6500
+ // the Conectar button disabled with nothing on screen saying why.
6501
+ startOAuth: fayzConnectorOAuth(NEOCHEF_CONNECTOR_ID),
6502
+ // The other half of the consent: the platform stores the token and the pool
6503
+ // stores "is it on?". With nobody writing the second, the connector answers
6504
+ // "não conectado" to a merchant who has just authorized.
6505
+ saveConnection: async () => {
6506
+ await neoChefConnectionStore().save({ active: true });
6507
+ },
6508
+ // Disconnect FREEZES the mirror. A merchant who unplugs the kitchen still has
6509
+ // a store, and a connector that empties the catalogue on disconnect is one
6510
+ // nobody dares to disconnect.
6511
+ disconnect: async () => {
6512
+ await neoChefConnectionStore().disconnect();
6513
+ },
6514
+ disconnectNote: "Desconectar congela o espelho: nenhum produto \xE9 apagado e o estoque para de ser atualizado.",
6515
+ ...connectionRuns(neoChefConnectionStore),
6516
+ // The managed automation says a paid order goes to the kitchen; this says
6517
+ // whether it arrived.
6518
+ listDeliveries: (options) => listOrderDeliveries2(options?.limit ?? 10),
6519
+ syncActions: [
6520
+ {
6521
+ key: "reconcile",
6522
+ label: "Sincronizar agora",
6523
+ direction: "inbound",
6524
+ // The one that is right on an ordinary day; the rest fold away.
6525
+ primary: true,
6526
+ description: "Traz o que mudou no NeoChef e envia o que est\xE1 pendente daqui \u2014 card\xE1pio, pre\xE7o, estoque e pedidos pagos.",
6527
+ run: async () => {
6528
+ const result = await reconcile2();
6529
+ const legs = Object.values(result.push ?? {}).map((leg) => leg?.message ? String(leg.message) : null).filter(Boolean);
6530
+ return [`${result.pull?.fetched ?? 0} produto(s) lidos do NeoChef`, ...legs].join(" \xB7 ");
6531
+ }
6532
+ },
6533
+ {
6534
+ key: "pull_products",
6535
+ label: "Trazer card\xE1pio do NeoChef",
6536
+ direction: "inbound",
6537
+ description: "Pre\xE7o e estoque do NeoChef passam a valer aqui. Nada \xE9 apagado da loja.",
6538
+ run: async () => {
6539
+ const result = await pullCatalogue2("full");
6540
+ return `${result.fetched} produto(s) lidos, ${result.inserted} criado(s), ${result.updated} atualizado(s)`;
6541
+ }
6542
+ },
6543
+ {
6544
+ key: "push_orders",
6545
+ label: "Enviar pedidos para a cozinha",
6546
+ direction: "outbound",
6547
+ description: "Cada pedido pago vira um pedido na cozinha. Pedido cancelado n\xE3o sobe, e um j\xE1 enviado n\xE3o sobe duas vezes.",
6548
+ pending: pendingOrders2,
6549
+ run: async () => {
6550
+ const result = await pushOrders2();
6551
+ return { message: result.message, status: result.status };
6552
+ }
6553
+ }
6554
+ ]
6555
+ };
6556
+
6557
+ // src/integrations/neochef/mapping.ts
6558
+ function toNumber2(value) {
6559
+ if (value === null || value === void 0 || value === "") return null;
6560
+ const n = typeof value === "number" ? value : Number(String(value).replace(",", "."));
6561
+ return Number.isFinite(n) ? n : null;
6562
+ }
6563
+ function toMirroredNeoChefProduct(raw) {
6564
+ if (!raw || raw.id === null || raw.id === void 0 || raw.id === "") return null;
6565
+ const inactive = raw.is_active === false || String(raw.status ?? "active").toLowerCase() === "archived";
6566
+ return {
6567
+ externalId: String(raw.id),
6568
+ sku: raw.sku ? String(raw.sku) : null,
6569
+ name: String(raw.name ?? "").trim() || `Produto ${raw.id}`,
6570
+ price: toNumber2(raw.price),
6571
+ // The kitchen owns this number. The store mirrors it; it does not decide it.
6572
+ inventoryCount: toNumber2(raw.stock),
6573
+ status: inactive ? "archived" : "active",
6574
+ imageUrl: raw.image_url ? String(raw.image_url) : null,
6575
+ // Unlike Bling, the far side DOES return its change stamp, so the cursor can
6576
+ // advance from the data instead of from the window we asked for.
6577
+ updatedAt: raw.updated_at ? String(raw.updated_at) : null
6578
+ };
6579
+ }
6580
+ function toNeoChefOrder(order) {
6581
+ return {
6582
+ kind: order.pickup === true ? "takeout" : "delivery",
6583
+ // The store's own number, so a human can match the two screens. Falls back
6584
+ // to the id rather than to empty: an order with no reference is unmatchable.
6585
+ reference_number: String(order.referenceNumber ?? order.id),
6586
+ // It arrives paid and confirmed — the store already took the money, and a
6587
+ // kitchen that has to re-confirm it is a kitchen doing the work twice.
6588
+ status: "confirmed",
6589
+ total: toNumber2(order.total),
6590
+ subtotal: toNumber2(order.subtotal),
6591
+ notes: order.notes ? String(order.notes) : null
6592
+ };
6593
+ }
6594
+ function advanceNeoChefCursor(products, overlapMs) {
6595
+ const stamps = products.map((product) => product.updatedAt ? Date.parse(product.updatedAt) : NaN).filter((value) => Number.isFinite(value));
6596
+ if (stamps.length === 0) return null;
6597
+ return new Date(Math.max(...stamps) - overlapMs).toISOString();
6598
+ }
5278
6599
  var ORDERS = "plg_shop_rep_orders";
5279
6600
  var ITEMS = "plg_shop_rep_order_items";
5280
6601
  var CUSTOMERS = "plg_shop_rep_customers";
@@ -6102,7 +7423,7 @@ function buildShopAnalyticsCards() {
6102
7423
  }
6103
7424
 
6104
7425
  // src/index.ts
6105
- var ShopPage = React8.lazy(() => import('./ShopPage-YB5ZWECJ.js').then((m) => ({ default: m.ShopPage })));
7426
+ var ShopPage = React9.lazy(() => import('./ShopPage-QFIM2HDU.js').then((m) => ({ default: m.ShopPage })));
6106
7427
  function createShopPlugin(options) {
6107
7428
  const curr = { code: "BRL", locale: "pt-BR", symbol: "R$", ...options?.currency };
6108
7429
  setShopTenantResolver(() => useOrganizationStore.getState().currentOrg?.id);
@@ -6111,10 +7432,10 @@ function createShopPlugin(options) {
6111
7432
  registerEntityPath("shop_customer", "/orders/customers");
6112
7433
  registerPersonLookup("shop_customer", shopCustomerLookup(() => resolveProvider()));
6113
7434
  const pageFor = (section) => {
6114
- const Wrapped = () => React8.createElement(
6115
- React8.Suspense,
7435
+ const Wrapped = () => React9.createElement(
7436
+ React9.Suspense,
6116
7437
  { fallback: null },
6117
- React8.createElement(
7438
+ React9.createElement(
6118
7439
  ShopPage,
6119
7440
  { provider: options?.provider, section }
6120
7441
  )
@@ -6133,6 +7454,24 @@ function createShopPlugin(options) {
6133
7454
  const ProductsCrudPage = createCrudPage(buildShopProductEntity(curr.code, resolveProvider), {
6134
7455
  dataProvider: createShopProductDataProvider(resolveProvider),
6135
7456
  feature: "shop",
7457
+ // O produto da loja não tem tabela própria — vive atrás do provider de
7458
+ // commerce —, então as primitivas de dado do agente não o alcançam. A porta
7459
+ // funciona porque a lista empresta o próprio store: ver CrudAgentSurface.
7460
+ aiCreate: {
7461
+ hint: "Descreva o produto\u2026",
7462
+ description: "Descreva o produto e eu cadastro.",
7463
+ intro: {
7464
+ role: "operations",
7465
+ persona: "Cadastro de produtos",
7466
+ pitch: "Diga o produto que voc\xEA quer cadastrar.",
7467
+ description: "Nome, pre\xE7o, descri\xE7\xE3o, estoque \u2014 diga o que souber, do jeito que falar, e eu preencho o resto com o que faz sentido. Nasce como rascunho e voc\xEA confere antes de publicar.",
7468
+ suggestions: [
7469
+ { label: "Camiseta preta, R$ 79, estoque 20" },
7470
+ { label: "Cadastre 5 produtos de exemplo para eu testar a loja" },
7471
+ { label: "Um combo com os dois produtos mais vendidos" }
7472
+ ]
7473
+ }
7474
+ },
6136
7475
  stats: (items, total) => [
6137
7476
  { id: "total", label: "Produtos", value: total, icon: Package },
6138
7477
  { id: "active", label: "Ativos", value: items?.filter((p) => p.status === "active").length ?? 0, icon: Eye },
@@ -6222,6 +7561,29 @@ function createShopPlugin(options) {
6222
7561
  const DiscountsCrudPage = createCrudPage(buildShopDiscountEntity(), {
6223
7562
  dataProvider: createShopDiscountDataProvider(resolveProvider),
6224
7563
  feature: "shop",
7564
+ // O cupom vive atrás do provider de commerce, sem tabela que as primitivas
7565
+ // de dado do agente alcancem — então a lista empresta o próprio store, como
7566
+ // a de produtos faz (ver CrudAgentSurface).
7567
+ aiCreate: {
7568
+ hint: "Descreva o cupom\u2026",
7569
+ description: "Descreva a promo\xE7\xE3o e eu crio o cupom.",
7570
+ // O código não se pergunta: ver DISCOUNT_CREATE_INSTRUCTIONS. `code` é
7571
+ // único no banco (0001:208), então a lista publica os que já existem e a
7572
+ // recusa do índice volta legível se dois turnos escolherem o mesmo.
7573
+ instructions: DISCOUNT_CREATE_INSTRUCTIONS,
7574
+ uniqueFields: ["code"],
7575
+ intro: {
7576
+ role: "operations",
7577
+ persona: "Cupons e promo\xE7\xF5es",
7578
+ pitch: "Diga a promo\xE7\xE3o que voc\xEA quer fazer.",
7579
+ 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.",
7580
+ suggestions: [
7581
+ { label: "Frete gr\xE1tis at\xE9 o fim da semana" },
7582
+ { label: "10% na primeira compra, uma vez por cliente" },
7583
+ { label: "R$ 50 off acima de R$ 300, limite de 100 usos" }
7584
+ ]
7585
+ }
7586
+ },
6225
7587
  stats: (items, total) => [
6226
7588
  { id: "total", label: "Total de cupons", value: total, icon: Tag },
6227
7589
  { id: "active", label: "Ativos agora", value: items?.filter((d) => d.status === "active").length ?? 0, icon: Eye },
@@ -6252,7 +7614,7 @@ function createShopPlugin(options) {
6252
7614
  dataProvider: createShopGiftCardDataProvider(resolveProvider),
6253
7615
  feature: "shop",
6254
7616
  stats: (items, total) => {
6255
- const money3 = (v) => new Intl.NumberFormat(curr.locale, { style: "currency", currency: curr.code }).format(v);
7617
+ const money2 = (v) => new Intl.NumberFormat(curr.locale, { style: "currency", currency: curr.code }).format(v);
6256
7618
  const active = items?.filter((g) => g.status === "active") ?? [];
6257
7619
  return [
6258
7620
  { id: "total", label: "Cart\xF5es emitidos", value: total, icon: CreditCard },
@@ -6263,20 +7625,20 @@ function createShopPlugin(options) {
6263
7625
  id: "outstanding",
6264
7626
  label: "Saldo em aberto",
6265
7627
  icon: CircleDollarSign,
6266
- value: money3(active.reduce((sum, g) => sum + (g.balance ?? 0), 0))
7628
+ value: money2(active.reduce((sum, g) => sum + (g.balance ?? 0), 0))
6267
7629
  },
6268
7630
  {
6269
7631
  id: "issued",
6270
7632
  label: "Valor emitido",
6271
7633
  icon: TicketCheck,
6272
- value: money3(items?.reduce((sum, g) => sum + (g.initialBalance ?? 0), 0) ?? 0)
7634
+ value: money2(items?.reduce((sum, g) => sum + (g.initialBalance ?? 0), 0) ?? 0)
6273
7635
  }
6274
7636
  ];
6275
7637
  }
6276
7638
  });
6277
- const ContentPageWrapped = () => React8.createElement(ContentPage, { resolveProvider: options?.provider });
7639
+ const ContentPageWrapped = () => React9.createElement(ContentPage, { resolveProvider: options?.provider });
6278
7640
  ContentPageWrapped.displayName = "ContentPage";
6279
- const ChannelsPageWrapped = () => React8.createElement(ChannelsPage, { resolveProvider: options?.provider });
7641
+ const ChannelsPageWrapped = () => React9.createElement(ChannelsPage, { resolveProvider: options?.provider });
6280
7642
  ChannelsPageWrapped.displayName = "ChannelsPage";
6281
7643
  const navSection = options?.navSection ?? "main";
6282
7644
  const navPosition = options?.navPosition ?? 6;
@@ -6366,7 +7728,7 @@ function createShopPlugin(options) {
6366
7728
  ];
6367
7729
  const redirectTo = (target) => {
6368
7730
  const Redirect = () => {
6369
- React8.useEffect(() => {
7731
+ React9.useEffect(() => {
6370
7732
  adminNavigateTo(target);
6371
7733
  }, []);
6372
7734
  return null;
@@ -6397,6 +7759,7 @@ function createShopPlugin(options) {
6397
7759
  const READ = { feature: "shop", action: "read" };
6398
7760
  return {
6399
7761
  id: "shop",
7762
+ defaultAgentRole: "operations",
6400
7763
  name: options?.navLabel ?? "Shop",
6401
7764
  icon: "ShoppingBag",
6402
7765
  version: "1.0.0",
@@ -6489,7 +7852,7 @@ function createShopPlugin(options) {
6489
7852
  label: label("store", "Loja"),
6490
7853
  icon: "ShoppingBag",
6491
7854
  group: COMMERCE_GROUP,
6492
- component: settingsTab("StoreGeneralSettings", () => React8.createElement(StoreGeneralSettings, { currency: curr, provider: options?.provider })),
7855
+ component: settingsTab("StoreGeneralSettings", () => React9.createElement(StoreGeneralSettings, { currency: curr, provider: options?.provider })),
6493
7856
  order: 30,
6494
7857
  permission: READ
6495
7858
  },
@@ -6498,7 +7861,7 @@ function createShopPlugin(options) {
6498
7861
  label: "Pagamentos",
6499
7862
  icon: "CreditCard",
6500
7863
  group: COMMERCE_GROUP,
6501
- component: settingsTab("PaymentsSettings", () => React8.createElement(PaymentsSettings, { provider: options?.provider })),
7864
+ component: settingsTab("PaymentsSettings", () => React9.createElement(PaymentsSettings, { provider: options?.provider })),
6502
7865
  order: 31,
6503
7866
  permission: READ
6504
7867
  },
@@ -6507,7 +7870,7 @@ function createShopPlugin(options) {
6507
7870
  label: "Checkout",
6508
7871
  icon: "ShoppingCart",
6509
7872
  group: COMMERCE_GROUP,
6510
- component: settingsTab("CheckoutSettings", () => React8.createElement(CheckoutSettings, { provider: options?.provider })),
7873
+ component: settingsTab("CheckoutSettings", () => React9.createElement(CheckoutSettings, { provider: options?.provider })),
6511
7874
  order: 32,
6512
7875
  permission: READ
6513
7876
  },
@@ -6516,7 +7879,7 @@ function createShopPlugin(options) {
6516
7879
  label: label("shipping", "Entrega"),
6517
7880
  icon: "Truck",
6518
7881
  group: COMMERCE_GROUP,
6519
- component: settingsTab("ShippingSettings", () => React8.createElement(ShippingSettings, { provider: options?.provider })),
7882
+ component: settingsTab("ShippingSettings", () => React9.createElement(ShippingSettings, { provider: options?.provider })),
6520
7883
  order: 33,
6521
7884
  permission: READ
6522
7885
  },
@@ -6525,7 +7888,7 @@ function createShopPlugin(options) {
6525
7888
  label: "Fulfillment",
6526
7889
  icon: "PackageCheck",
6527
7890
  group: COMMERCE_GROUP,
6528
- component: settingsTab("FulfillmentSettings", () => React8.createElement(FulfillmentSettings, { provider: options?.provider })),
7891
+ component: settingsTab("FulfillmentSettings", () => React9.createElement(FulfillmentSettings, { provider: options?.provider })),
6529
7892
  order: 34,
6530
7893
  permission: READ
6531
7894
  }
@@ -6534,28 +7897,37 @@ function createShopPlugin(options) {
6534
7897
  // screens (commerce-stack.md §0). Melhor Envio, AppMax and AbacatePay are
6535
7898
  // REQUEST-plane connectors: the buyer is waiting, so each declares a latency
6536
7899
  // budget and, because the carrier and the gateways want opposite answers to
6537
- // the same failure, its own `onFailure`. Bling is the SYNC-plane one —
6538
- // nobody is waiting on it, and what it declares instead is a call budget and
6539
- // a windowed cursor.
7900
+ // the same failure, its own `onFailure`. Bling and NeoChef are the SYNC-plane
7901
+ // ones — nobody is waiting on them, and what they declare instead is a call
7902
+ // budget and a cursor.
6540
7903
  //
6541
- // Two gateways on the shelf is the point: the store picks one, and the
6542
- // second one arriving changed no contract — only how it authorizes.
7904
+ // Four gateways on the shelf is the point: the store picks one, and each
7905
+ // one arriving changed no contract — only how it authorizes. Mercado Pago
7906
+ // pushes that furthest, because it is the first whose CARD is paid inside
7907
+ // the store: it tokenises in the browser, so there is no PAN for an edge
7908
+ // function to hold and no redirect to send the buyer through. The shelf
7909
+ // absorbed that too, as a capability the checkout reads rather than a branch
7910
+ // it hard-codes. NeoChef makes the same point about the back office, and one
7911
+ // better: its provider is another Fayz app, and it still fits without a
7912
+ // special case.
6543
7913
  connectors: [
6544
7914
  melhorEnvioConnectorDef,
6545
7915
  appmaxConnectorDef,
6546
7916
  abacatePayConnectorDef,
6547
7917
  stripeConnectConnectorDef,
6548
7918
  vindiConnectorDef,
6549
- blingConnectorDef
7919
+ mercadoPagoConnectorDef,
7920
+ blingConnectorDef,
7921
+ neoChefConnectorDef
6550
7922
  ],
6551
- // What having a store makes plugable — the four above plus the ones still
7923
+ // What having a store makes plugable — the ones above plus the ones still
6552
7924
  // to be built. Carried over from the Fayz plugin page's "Integra com" row,
6553
7925
  // minus Nuvemshop: the platform catalogue has no row for it, and a slug the
6554
7926
  // directory cannot back is refused rather than shown.
6555
- integrations: ["shopify", "stripe", "mercadopago"]
7927
+ integrations: ["shopify", "stripe"]
6556
7928
  };
6557
7929
  }
6558
7930
 
6559
- 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, STRIPE_CONNECT_CONNECTOR_ID, STRIPE_CONNECT_LATENCY_BUDGET_MS, VINDI_CONNECTOR_ID, VINDI_LATENCY_BUDGET_MS, abacatePayConnectorDef, acceptWebhook, appmaxConnectorDef, blingConnectionStore, blingConnectorDef, buildShopAnalyticsCards, createShopPlugin, firstWindow, melhorEnvioConnectorDef, orphanConnection, pullProducts, routeCallback, sampleProducts, stripeConnectConnectorDef, toMirroredProduct, vindiConnectorDef, windowFor };
7931
+ 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 };
6560
7932
  //# sourceMappingURL=index.js.map
6561
7933
  //# sourceMappingURL=index.js.map