@fayz-ai/plugin-shop 0.12.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/dist/{ShopPage-VVLOH3RF.js → ShopPage-QFIM2HDU.js} +1494 -423
  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 +1 -0
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +1312 -298
  8. package/dist/index.js.map +1 -1
  9. package/dist/integrations/bling/data.d.ts.map +1 -1
  10. package/dist/integrations/mercadopago/connectorDef.d.ts +41 -0
  11. package/dist/integrations/mercadopago/connectorDef.d.ts.map +1 -0
  12. package/dist/integrations/neochef/data.d.ts.map +1 -1
  13. package/dist/integrations/vindi/connectorDef.d.ts +42 -29
  14. package/dist/integrations/vindi/connectorDef.d.ts.map +1 -1
  15. package/dist/lib/onboarding.d.ts +17 -0
  16. package/dist/lib/onboarding.d.ts.map +1 -0
  17. package/dist/views/CustomerModal.d.ts +33 -0
  18. package/dist/views/CustomerModal.d.ts.map +1 -0
  19. package/dist/views/NewOrderCharge.d.ts +26 -2
  20. package/dist/views/NewOrderCharge.d.ts.map +1 -1
  21. package/dist/views/NewOrderPage.d.ts +10 -1
  22. package/dist/views/NewOrderPage.d.ts.map +1 -1
  23. package/dist/views/OrderDetailView.d.ts.map +1 -1
  24. package/dist/views/OrdersAgentSurface.d.ts +10 -0
  25. package/dist/views/OrdersAgentSurface.d.ts.map +1 -0
  26. package/dist/views/ProductPositionsTab.d.ts +7 -0
  27. package/dist/views/ProductPositionsTab.d.ts.map +1 -0
  28. package/dist/views/ProductStockTab.d.ts +84 -0
  29. package/dist/views/ProductStockTab.d.ts.map +1 -0
  30. package/dist/views/ShopPage.d.ts.map +1 -1
  31. package/dist/views/StockEntryModal.d.ts +32 -0
  32. package/dist/views/StockEntryModal.d.ts.map +1 -0
  33. package/dist/views/customerEntity.d.ts.map +1 -1
  34. package/dist/views/discount-agent.d.ts +2 -0
  35. package/dist/views/discount-agent.d.ts.map +1 -0
  36. package/dist/views/new-order-rules.d.ts +13 -0
  37. package/dist/views/new-order-rules.d.ts.map +1 -1
  38. package/dist/views/orders-agent.d.ts +11 -0
  39. package/dist/views/orders-agent.d.ts.map +1 -0
  40. package/dist/views/product-matching.d.ts +38 -0
  41. package/dist/views/product-matching.d.ts.map +1 -0
  42. package/dist/views/productEntity.d.ts.map +1 -1
  43. package/dist/views/stock-agent.d.ts +82 -0
  44. package/dist/views/stock-agent.d.ts.map +1 -0
  45. package/package.json +8 -6
  46. package/dist/ShopPage-VVLOH3RF.js.map +0 -1
  47. package/dist/chunk-HAKXHHAG.js.map +0 -1
@@ -1,11 +1,13 @@
1
- import { errorMessage, shopCustomerLookup, listProductsDetailed, invalidateProductLists, buildStockLocationsRegistry } from './chunk-HAKXHHAG.js';
2
- import React2, { useState, useEffect, useMemo, useCallback, useRef } from 'react';
3
- import { Thumbnail, Button, DataTable, toast, ConfirmDialog, formatPhone, SearchSelect, Modal, ModalPortal, ModalOverlay, ModalContent, ModalHeader, ModalTitle, ModalBody, ModalFooter, Skeleton, PhoneField } from '@fayz-ai/ui';
4
- import { Minus, Plus, Trash2, CreditCard, XCircle, ShoppingBag, Package, Wallet, X, Pencil, MapPin, Search, Check, Copy, MessageCircle, CheckCircle2, Printer, RefreshCw, ExternalLink, Truck, AlertTriangle, Loader2 } from 'lucide-react';
5
- import { CHARGEABLE_PROVIDERS, supportsPaymentLink, getShopProvider, formatDocument, resolveShippingLabelProvider, documentDigits, isValidDocument } from '@fayz-ai/shop';
6
- import { useAdminPath, adminNavigateTo, OrderFormBlock, OrderFormView, OrderDetailSkeleton, OrderDetailBlock, OrderDetailRow, PersonLink, OrderDetailView as OrderDetailView$1, OrderListView, RecordPickerModal, useConnectors, CrudRecordModal } from '@fayz-ai/admin';
1
+ import { errorMessage, shopCustomerLookup, listProductsDetailed, invalidateProductLists, buildShopCustomerEntity, buildStockLocationsRegistry, listProductsCached, resolveTerm, candidateMeta } from './chunk-2VMTPRGC.js';
2
+ import * as React2 from 'react';
3
+ import React2__default, { useState, useEffect, useMemo, useCallback, useRef } from 'react';
4
+ import { useSaveBar, Thumbnail, Button, Modal, ModalContent, ModalHeader, ModalTitle, ModalDescription, ModalFooter, formatPhone, toast, DataTable, ConfirmDialog, SearchSelect, ModalPortal, ModalOverlay, ModalBody, Skeleton } from '@fayz-ai/ui';
5
+ import { Minus, Plus, Trash2, RotateCcw, QrCode, Pencil, Archive, Ban, CreditCard, XCircle, ShoppingBag, Package, Wallet, WandSparkles, X, MapPin, Search, Link2, HandCoins, Check, Copy, Share2, MessageCircle, CheckCircle2, Printer, RefreshCw, ExternalLink, Truck, AlertTriangle, Loader2 } from 'lucide-react';
6
+ import { CHARGEABLE_PROVIDERS, supportsPaymentLink, getShopProvider, formatDocument, resolveShippingLabelProvider, isValidDocument, documentDigits } from '@fayz-ai/shop';
7
+ import { useAdminPath, adminNavigateTo, OrderFormBlock, OrderFormView, OrderDetailSkeleton, PageActions, OrderDetailBlock, OrderDetailRow, PersonLink, OrderDetailView as OrderDetailView$1, OrderListView, askAI, RecordPickerModal, useConnectors, CrudRecordModal, useAgentSurface, duplicateWrite } from '@fayz-ai/admin';
7
8
  import { usePluginRuntimeOptional, getSupabaseClientOptional } from '@fayz-ai/core';
8
9
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
10
+ import { listStockReasonCodes } from '@fayz-ai/shop/inventory';
9
11
 
10
12
  function ShipOrderForm({ orderId, provider, onShipped }) {
11
13
  const [open, setOpen] = useState(false);
@@ -136,8 +138,8 @@ function SenderLocationModal({
136
138
  locationId
137
139
  }) {
138
140
  const resolve = provider ?? (() => getShopProvider());
139
- const registry = React2.useMemo(() => buildStockLocationsRegistry(resolve), [provider]);
140
- const chosen = React2.useRef(null);
141
+ const registry = React2__default.useMemo(() => buildStockLocationsRegistry(resolve), [provider]);
142
+ const chosen = React2__default.useRef(null);
141
143
  return /* @__PURE__ */ jsx(
142
144
  CrudRecordModal,
143
145
  {
@@ -198,15 +200,13 @@ function validateNewOrder(draft, ending) {
198
200
  }
199
201
  return null;
200
202
  }
201
- function isDuplicateEmail(error) {
202
- const code = String(error?.code ?? "");
203
- if (code === "23505") return true;
204
- const message = String(error?.message ?? error ?? "");
205
- return /duplicate key value/iu.test(message) && /email/iu.test(message);
203
+ function insufficientStockProduct(message) {
204
+ const found = /insufficient stock for\s+(.+?)\s*$/iu.exec(String(message ?? ""));
205
+ return found ? found[1].trim() : null;
206
206
  }
207
207
  function isSenderRefusal(message) {
208
- const text = String(message ?? "");
209
- return /\bfrom\./iu.test(text) || /remetente/iu.test(text);
208
+ const text2 = String(message ?? "");
209
+ return /\bfrom\./iu.test(text2) || /remetente/iu.test(text2);
210
210
  }
211
211
  var money = (value) => new Intl.NumberFormat("pt-BR", { style: "currency", currency: "BRL" }).format(Number(value) || 0);
212
212
  var eta = (option) => {
@@ -831,6 +831,408 @@ async function cancelShopOrder(provider, orderId, reason) {
831
831
  const { error } = await db.rpc("shop_cancel_order", { p_order_id: orderId, p_reason: reason ?? null });
832
832
  if (error) throw new Error(error.message);
833
833
  }
834
+ var POLL_MS = 4e3;
835
+ function ChargeChoice({ icon: Icon, title, hint, onClick, testId }) {
836
+ return /* @__PURE__ */ jsxs(
837
+ "button",
838
+ {
839
+ type: "button",
840
+ onClick,
841
+ "data-testid": testId,
842
+ className: "flex w-full items-start gap-3 rounded-md border px-3 py-2.5 text-left transition hover:border-primary hover:bg-muted/40",
843
+ children: [
844
+ /* @__PURE__ */ jsx(Icon, { className: "mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" }),
845
+ /* @__PURE__ */ jsxs("span", { className: "min-w-0", children: [
846
+ /* @__PURE__ */ jsx("span", { className: "block text-sm font-medium", children: title }),
847
+ /* @__PURE__ */ jsx("span", { className: "block text-xs text-muted-foreground", children: hint })
848
+ ] })
849
+ ]
850
+ }
851
+ );
852
+ }
853
+ function NewOrderCharge({
854
+ open,
855
+ provider,
856
+ sessionId,
857
+ emv,
858
+ qrcodeBase64,
859
+ qrcodeUrl,
860
+ paymentUrl,
861
+ acquirer,
862
+ total,
863
+ currency: currency2,
864
+ customerName,
865
+ customerPhone,
866
+ onPaid,
867
+ onClose,
868
+ onChoose,
869
+ opening,
870
+ onRegisterReceipt,
871
+ error
872
+ }) {
873
+ const [state, setState] = useState("waiting");
874
+ const [copied, setCopied] = useState(false);
875
+ const [checking, setChecking] = useState(false);
876
+ const settled = useRef(false);
877
+ const inFlight = useRef(false);
878
+ const money4 = new Intl.NumberFormat("pt-BR", { style: "currency", currency: currency2 }).format(total);
879
+ const check = useCallback(async () => {
880
+ if (settled.current || !sessionId || inFlight.current) return;
881
+ inFlight.current = true;
882
+ setChecking(true);
883
+ try {
884
+ const result = await provider().settleCheckoutSession?.(sessionId, acquirer);
885
+ if (!result) return;
886
+ if (result.state === "paid") {
887
+ settled.current = true;
888
+ setState("paid");
889
+ setTimeout(() => onPaid(result.orderId ?? void 0), 1200);
890
+ return;
891
+ }
892
+ const outcome = chargeOutcome(result.state);
893
+ if (outcome !== "waiting") {
894
+ settled.current = true;
895
+ setState(outcome);
896
+ }
897
+ } catch {
898
+ } finally {
899
+ inFlight.current = false;
900
+ setChecking(false);
901
+ }
902
+ }, [provider, sessionId, acquirer, onPaid]);
903
+ useEffect(() => {
904
+ if (!open || !sessionId) return;
905
+ const timer = setInterval(() => {
906
+ void check();
907
+ }, POLL_MS);
908
+ return () => clearInterval(timer);
909
+ }, [open, sessionId, check]);
910
+ const isLink = Boolean(paymentUrl && !emv);
911
+ const payable = paymentUrl ?? emv ?? "";
912
+ const qrSrc = qrcodeBase64 ? `data:image/png;base64,${qrcodeBase64}` : qrcodeUrl || null;
913
+ const ready = Boolean(sessionId && payable);
914
+ const choosing = Boolean(onChoose) && !ready && !opening;
915
+ async function copy() {
916
+ if (!payable) return;
917
+ try {
918
+ await navigator.clipboard.writeText(payable);
919
+ setCopied(true);
920
+ setTimeout(() => setCopied(false), 2400);
921
+ } catch {
922
+ }
923
+ }
924
+ async function shareQr() {
925
+ if (!qrSrc) return;
926
+ let blob;
927
+ try {
928
+ blob = qrcodeBase64 ? new Blob([Uint8Array.from(atob(qrcodeBase64), (c) => c.charCodeAt(0))], { type: "image/png" }) : await (await fetch(qrSrc)).blob();
929
+ } catch {
930
+ toast.error("N\xE3o consegui pegar a imagem do QR", {
931
+ description: 'Use "Copiar c\xF3digo" \u2014 o cliente cola no app do banco.'
932
+ });
933
+ return;
934
+ }
935
+ const file = new File([blob], `pix-${money4.replace(/\D/gu, "")}.${blob.type.includes("svg") ? "svg" : "png"}`, { type: blob.type || "image/png" });
936
+ const share = navigator;
937
+ try {
938
+ if (share.canShare?.({ files: [file] })) {
939
+ await navigator.share({ files: [file], text: message });
940
+ return;
941
+ }
942
+ const Item = window.ClipboardItem;
943
+ if (!Item || !blob.type.includes("png")) throw new Error("sem \xE1rea de transfer\xEAncia de imagem");
944
+ await navigator.clipboard.write([new Item({ "image/png": blob })]);
945
+ toast.success("QR copiado \u2014 \xE9 s\xF3 colar na conversa do WhatsApp.");
946
+ } catch {
947
+ toast.error("Este navegador n\xE3o compartilha imagem", {
948
+ description: 'Use "Copiar c\xF3digo" \u2014 o cliente cola no app do banco.'
949
+ });
950
+ }
951
+ }
952
+ const greeting = customerName ? ` ${customerName.split(" ")[0]}` : "";
953
+ const message = isLink ? `Ol\xE1${greeting}! Aqui est\xE1 o link para pagar seu pedido de ${money4}:
954
+
955
+ ${payable}
956
+
957
+ Voc\xEA escolhe a forma de pagamento na p\xE1gina.` : `Ol\xE1${greeting}! Segue o Pix de ${money4} para o seu pedido:
958
+
959
+ ${payable}`;
960
+ const whatsapp = customerPhone && payable ? `https://wa.me/${customerPhone.length > 11 ? customerPhone : `55${customerPhone}`}?text=${encodeURIComponent(message)}` : null;
961
+ return /* @__PURE__ */ jsx(Modal, { open, onOpenChange: (next) => {
962
+ if (!next) onClose();
963
+ }, children: /* @__PURE__ */ jsx(ModalPortal, { children: /* @__PURE__ */ jsx(ModalOverlay, { children: /* @__PURE__ */ jsxs(ModalContent, { size: "lg", "data-testid": "new-order-charge-modal", children: [
964
+ /* @__PURE__ */ jsxs(ModalHeader, { className: "mb-4", children: [
965
+ /* @__PURE__ */ jsx(ModalTitle, { children: choosing || opening ? `Cobrar ${money4}` : isLink ? `Link de ${money4}` : `Pix de ${money4}` }),
966
+ choosing && /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: "Como o cliente vai pagar?" }),
967
+ opening && /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: "Abrindo a cobran\xE7a no adquirente\u2026" }),
968
+ ready && state === "waiting" && /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: isLink ? "Mande para o cliente. O pedido \xE9 criado quando o pagamento cair \u2014 pode fechar esta janela." : "Vale por 15 minutos. O pedido \xE9 criado quando o pagamento cair." })
969
+ ] }),
970
+ /* @__PURE__ */ jsxs(ModalBody, { children: [
971
+ error && /* @__PURE__ */ jsx(
972
+ "p",
973
+ {
974
+ role: "alert",
975
+ "data-testid": "charge-error",
976
+ className: "mb-3 rounded-md border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-800",
977
+ children: error
978
+ }
979
+ ),
980
+ choosing && /* @__PURE__ */ jsxs("div", { className: "space-y-2", "data-testid": "charge-choices", children: [
981
+ /* @__PURE__ */ jsx(
982
+ ChargeChoice,
983
+ {
984
+ icon: QrCode,
985
+ title: "Cobrar por Pix",
986
+ hint: "QR e c\xF3digo na hora, para ler ou mandar agora",
987
+ onClick: () => onChoose?.(false),
988
+ testId: "charge-choose-pix"
989
+ }
990
+ ),
991
+ /* @__PURE__ */ jsx(
992
+ ChargeChoice,
993
+ {
994
+ icon: Link2,
995
+ title: "Gerar link de pagamento",
996
+ hint: "Uma p\xE1gina do adquirente \u2014 o cliente escolhe como pagar",
997
+ onClick: () => onChoose?.(true),
998
+ testId: "charge-choose-link"
999
+ }
1000
+ ),
1001
+ onRegisterReceipt && /* @__PURE__ */ jsx(
1002
+ ChargeChoice,
1003
+ {
1004
+ icon: HandCoins,
1005
+ title: "J\xE1 recebi",
1006
+ hint: "Dinheiro, maquininha, Pix na sua conta \u2014 s\xF3 registrar",
1007
+ onClick: onRegisterReceipt,
1008
+ testId: "charge-choose-received"
1009
+ }
1010
+ )
1011
+ ] }),
1012
+ opening && /* @__PURE__ */ jsxs(
1013
+ "div",
1014
+ {
1015
+ className: "flex flex-col items-center gap-4 sm:flex-row sm:items-start",
1016
+ "aria-busy": true,
1017
+ "data-testid": "charge-skeleton",
1018
+ children: [
1019
+ /* @__PURE__ */ jsx("div", { className: "h-40 w-40 shrink-0 animate-pulse rounded-md border bg-muted" }),
1020
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1 space-y-3", children: [
1021
+ /* @__PURE__ */ jsx("div", { className: "h-3 w-28 animate-pulse rounded bg-muted" }),
1022
+ /* @__PURE__ */ jsx("div", { className: "h-16 w-full animate-pulse rounded-md bg-muted" }),
1023
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
1024
+ /* @__PURE__ */ jsx("div", { className: "h-8 w-32 animate-pulse rounded-md bg-muted" }),
1025
+ /* @__PURE__ */ jsx("div", { className: "h-8 w-40 animate-pulse rounded-md bg-muted" })
1026
+ ] })
1027
+ ] })
1028
+ ]
1029
+ }
1030
+ ),
1031
+ ready && /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center gap-4 sm:flex-row sm:items-start", children: [
1032
+ qrSrc && /* @__PURE__ */ jsx(
1033
+ "img",
1034
+ {
1035
+ src: qrSrc,
1036
+ alt: "QR code do Pix",
1037
+ "data-testid": "new-order-qr",
1038
+ className: "h-40 w-40 shrink-0 rounded-md border bg-white p-2"
1039
+ }
1040
+ ),
1041
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1 space-y-3", children: [
1042
+ /* @__PURE__ */ jsxs("div", { children: [
1043
+ /* @__PURE__ */ jsx("p", { className: "text-xs font-medium text-muted-foreground", children: isLink ? "Link de pagamento" : "Pix copia e cola" }),
1044
+ /* @__PURE__ */ jsx(
1045
+ "p",
1046
+ {
1047
+ "data-testid": isLink ? "new-order-payment-link" : "new-order-emv",
1048
+ className: "mt-1 max-h-20 overflow-auto break-all rounded-md border bg-muted/30 p-2 font-mono text-[11px]",
1049
+ children: payable
1050
+ }
1051
+ ),
1052
+ isLink && /* @__PURE__ */ jsx("p", { className: "mt-1 text-[11px] text-muted-foreground", children: "O cliente escolhe Pix, boleto ou cart\xE3o na p\xE1gina do adquirente." })
1053
+ ] }),
1054
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-2", children: [
1055
+ /* @__PURE__ */ jsxs(Button, { size: "sm", onClick: copy, "data-testid": "new-order-copy", children: [
1056
+ copied ? /* @__PURE__ */ jsx(Check, { className: "mr-1.5 h-3.5 w-3.5" }) : /* @__PURE__ */ jsx(Copy, { className: "mr-1.5 h-3.5 w-3.5" }),
1057
+ copied ? "Copiado" : isLink ? "Copiar link" : "Copiar c\xF3digo"
1058
+ ] }),
1059
+ qrSrc && /* @__PURE__ */ jsxs(
1060
+ Button,
1061
+ {
1062
+ size: "sm",
1063
+ variant: "outline",
1064
+ onClick: () => void shareQr(),
1065
+ "data-testid": "new-order-share-qr",
1066
+ children: [
1067
+ /* @__PURE__ */ jsx(Share2, { className: "mr-1.5 h-3.5 w-3.5" }),
1068
+ "Compartilhar QR"
1069
+ ]
1070
+ }
1071
+ ),
1072
+ whatsapp && /* @__PURE__ */ jsxs(
1073
+ "a",
1074
+ {
1075
+ href: whatsapp,
1076
+ target: "_blank",
1077
+ rel: "noreferrer",
1078
+ "data-testid": "new-order-whatsapp",
1079
+ className: "inline-flex items-center rounded-md border px-3 py-1.5 text-sm font-medium transition hover:bg-muted",
1080
+ children: [
1081
+ /* @__PURE__ */ jsx(MessageCircle, { className: "mr-1.5 h-3.5 w-3.5" }),
1082
+ "Mandar no WhatsApp"
1083
+ ]
1084
+ }
1085
+ )
1086
+ ] })
1087
+ ] })
1088
+ ] }),
1089
+ ready && /* @__PURE__ */ jsxs("div", { className: "mt-4 rounded-md border bg-muted/30 px-3 py-2 text-sm", children: [
1090
+ state === "waiting" && /* @__PURE__ */ jsxs("p", { className: "flex items-center gap-2 text-muted-foreground", "data-testid": "new-order-waiting", children: [
1091
+ /* @__PURE__ */ jsx("span", { className: "h-2 w-2 animate-pulse rounded-full bg-amber-500" }),
1092
+ "Aguardando o pagamento\u2026 a tela confirma sozinha."
1093
+ ] }),
1094
+ state === "paid" && /* @__PURE__ */ jsxs("p", { className: "flex items-center gap-2 font-medium text-emerald-700", "data-testid": "new-order-paid", children: [
1095
+ /* @__PURE__ */ jsx(Check, { className: "h-4 w-4" }),
1096
+ " Pagamento confirmado \u2014 abrindo o pedido\u2026"
1097
+ ] }),
1098
+ (state === "expired" || state === "failed") && /* @__PURE__ */ jsx("p", { className: "text-red-700", "data-testid": "new-order-dead", children: state === "expired" ? `${isLink ? "O link expirou" : "O c\xF3digo expirou"} sem pagamento. O pedido continua aqui \u2014 d\xE1 para cobrar de outro jeito.` : "O pagamento n\xE3o foi conclu\xEDdo. O pedido continua aqui \u2014 d\xE1 para cobrar de outro jeito." })
1099
+ ] })
1100
+ ] }),
1101
+ /* @__PURE__ */ jsxs(ModalFooter, { className: "mt-4 gap-2", children: [
1102
+ ready && state === "waiting" && /* @__PURE__ */ jsx(Button, { variant: "outline", onClick: () => void check(), disabled: checking, "data-testid": "new-order-check", children: checking ? "Conferindo\u2026" : "J\xE1 pagou? Conferir" }),
1103
+ /* @__PURE__ */ jsx(
1104
+ Button,
1105
+ {
1106
+ variant: state === "waiting" ? "ghost" : "default",
1107
+ onClick: onClose,
1108
+ "data-testid": "new-order-charge-close",
1109
+ children: state === "waiting" ? "Fechar" : "Voltar ao pedido"
1110
+ }
1111
+ )
1112
+ ] })
1113
+ ] }) }) }) });
1114
+ }
1115
+ var onlyDigits = (value) => value.replace(/\D/gu, "");
1116
+ function CustomerModal({ open, initialName, customerId, initial, provider, onClose, onSaved }) {
1117
+ const entityDef = useMemo(() => buildShopCustomerEntity(), []);
1118
+ const [conflict, setConflict] = useState(null);
1119
+ const [refused, setRefused] = useState(null);
1120
+ const [forcing, setForcing] = useState(false);
1121
+ const initialData = useMemo(() => {
1122
+ const [head, ...rest] = String(initial?.name ?? initialName ?? "").trim().split(/\s+/u);
1123
+ return {
1124
+ firstName: head ?? "",
1125
+ lastName: rest.join(" "),
1126
+ email: initial?.email ?? "",
1127
+ phone: initial?.phone ?? "",
1128
+ document: initial?.document ?? ""
1129
+ };
1130
+ }, [initial, initialName]);
1131
+ useEffect(() => {
1132
+ if (open) {
1133
+ setConflict(null);
1134
+ setRefused(null);
1135
+ }
1136
+ }, [open]);
1137
+ async function saveAnyway() {
1138
+ if (!refused || !conflict) return;
1139
+ setForcing(true);
1140
+ try {
1141
+ const fields = { ...refused, [conflict.field]: void 0 };
1142
+ const saved = customerId ? await provider().updateCustomer(customerId, fields) : await provider().createCustomer(fields);
1143
+ toast.success(`Cadastro salvo sem o ${conflict.label} \u2014 ele j\xE1 pertence a outro cadastro.`);
1144
+ onSaved(saved);
1145
+ } catch (err) {
1146
+ toast.error("N\xE3o consegui salvar", { description: errorMessage(err) });
1147
+ } finally {
1148
+ setForcing(false);
1149
+ }
1150
+ }
1151
+ async function save(data) {
1152
+ const document = documentDigits(String(data.document ?? ""));
1153
+ if (document && !isValidDocument(document)) throw new Error("Esse CPF/CNPJ n\xE3o confere.");
1154
+ const email = String(data.email ?? "").trim().toLowerCase();
1155
+ const fields = {
1156
+ firstName: String(data.firstName ?? "").trim(),
1157
+ lastName: String(data.lastName ?? "").trim(),
1158
+ email: email || void 0,
1159
+ phone: onlyDigits(String(data.phone ?? "")) || void 0,
1160
+ document: document || void 0,
1161
+ notes: data.notes ?? void 0
1162
+ };
1163
+ try {
1164
+ const saved = customerId ? await provider().updateCustomer(customerId, fields) : await provider().createCustomer(fields);
1165
+ onSaved(saved);
1166
+ return saved;
1167
+ } catch (err) {
1168
+ const clash = duplicateWrite(err);
1169
+ if (!clash?.field) throw err;
1170
+ setRefused(fields);
1171
+ const hit = clash.field === "email" && email ? (await provider().listCustomers({ search: email, limit: 5 }).catch(() => [])).find((c) => (c.email ?? "").toLowerCase() === email) ?? null : null;
1172
+ setConflict({ field: clash.field, label: clash.label, hit });
1173
+ return;
1174
+ }
1175
+ }
1176
+ return /* @__PURE__ */ jsx(
1177
+ CrudRecordModal,
1178
+ {
1179
+ open,
1180
+ mode: customerId ? "edit" : "create",
1181
+ entityDef,
1182
+ initialData,
1183
+ title: customerId ? "Cadastro do cliente" : "Novo cliente",
1184
+ onClose,
1185
+ save,
1186
+ notice: conflict && /* @__PURE__ */ jsxs(
1187
+ "div",
1188
+ {
1189
+ role: "alert",
1190
+ "data-testid": "new-customer-duplicate",
1191
+ className: "rounded-md border border-amber-200 bg-amber-50 px-3 py-2 text-sm text-amber-900",
1192
+ children: [
1193
+ /* @__PURE__ */ jsx("p", { children: conflict.hit ? /* @__PURE__ */ jsxs(Fragment, { children: [
1194
+ /* @__PURE__ */ jsxs("strong", { children: [
1195
+ conflict.hit.firstName,
1196
+ " ",
1197
+ conflict.hit.lastName
1198
+ ] }),
1199
+ " j\xE1 \xE9 cliente com esse ",
1200
+ conflict.label,
1201
+ "."
1202
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
1203
+ "Esse ",
1204
+ conflict.label,
1205
+ " j\xE1 est\xE1 em outro cadastro desta loja."
1206
+ ] }) }),
1207
+ /* @__PURE__ */ jsxs("div", { className: "mt-1 flex flex-wrap items-center gap-3", children: [
1208
+ conflict.hit && /* @__PURE__ */ jsx(
1209
+ "button",
1210
+ {
1211
+ type: "button",
1212
+ onClick: () => onSaved(conflict.hit),
1213
+ "data-testid": "new-customer-use-existing",
1214
+ className: "font-medium underline underline-offset-2",
1215
+ children: "Usar este cliente"
1216
+ }
1217
+ ),
1218
+ /* @__PURE__ */ jsx(
1219
+ "button",
1220
+ {
1221
+ type: "button",
1222
+ onClick: () => void saveAnyway(),
1223
+ disabled: forcing,
1224
+ "data-testid": "new-customer-save-anyway",
1225
+ className: "font-medium underline underline-offset-2 disabled:opacity-60",
1226
+ children: forcing ? "Salvando\u2026" : `Salvar sem o ${conflict.label}`
1227
+ }
1228
+ )
1229
+ ] })
1230
+ ]
1231
+ }
1232
+ )
1233
+ }
1234
+ );
1235
+ }
834
1236
  var money2 = (v, currency2 = "BRL") => new Intl.NumberFormat("pt-BR", { style: "currency", currency: currency2 }).format(Number(v) || 0);
835
1237
  var dateTime2 = (iso) => new Intl.DateTimeFormat("pt-BR", { day: "2-digit", month: "2-digit", year: "numeric", hour: "2-digit", minute: "2-digit" }).format(new Date(iso));
836
1238
  var dateOnly = (iso) => new Date(iso).toLocaleDateString("pt-BR");
@@ -939,6 +1341,17 @@ function deliveryFromNotes(notes) {
939
1341
  return (match?.[1] ?? "").trim() || null;
940
1342
  }
941
1343
  var shopOrderCache = /* @__PURE__ */ new Map();
1344
+ function fromSession(session) {
1345
+ return {
1346
+ sessionId: session.sessionId,
1347
+ emv: session.charge?.emv ?? null,
1348
+ qrcode: session.charge?.qrcodeBase64 ?? null,
1349
+ qrcodeUrl: session.charge?.qrcodeUrl ?? null,
1350
+ paymentUrl: session.paymentUrl ?? null,
1351
+ total: session.total,
1352
+ provider: session.provider ?? null
1353
+ };
1354
+ }
942
1355
  var shopShipmentsCache = /* @__PURE__ */ new Map();
943
1356
  var shopReceivableCache = /* @__PURE__ */ new Map();
944
1357
  function OrderDetailView({ orderId, provider, onBack }) {
@@ -955,6 +1368,12 @@ function OrderDetailView({ orderId, provider, onBack }) {
955
1368
  const [statusError, setStatusError] = useState(null);
956
1369
  const [cancelReason, setCancelReason] = useState("");
957
1370
  const [receivable, setReceivable] = useState(() => shopReceivableCache.get(orderId) ?? null);
1371
+ const [charge, setCharge] = useState(null);
1372
+ const [chargeOpen, setChargeOpen] = useState(false);
1373
+ const [charging, setCharging] = useState(null);
1374
+ const [customer, setCustomer] = useState(null);
1375
+ const [editingCustomer, setEditingCustomer] = useState(false);
1376
+ const [chargeError, setChargeError] = useState(null);
958
1377
  useEffect(() => {
959
1378
  let cancelled = false;
960
1379
  const cached = shopOrderCache.get(orderId);
@@ -986,6 +1405,19 @@ function OrderDetailView({ orderId, provider, onBack }) {
986
1405
  cancelled = true;
987
1406
  };
988
1407
  }, [orderId, provider]);
1408
+ const providerRef = useRef(provider);
1409
+ providerRef.current = provider;
1410
+ const loadCustomer = useCallback(async () => {
1411
+ const id = shopOrderCache.get(orderId)?.customerId;
1412
+ if (!id) {
1413
+ setCustomer(null);
1414
+ return;
1415
+ }
1416
+ setCustomer(await providerRef.current().getCustomer(id).catch(() => null));
1417
+ }, [orderId]);
1418
+ useEffect(() => {
1419
+ void loadCustomer();
1420
+ }, [loadCustomer, order?.customerId]);
989
1421
  useEffect(() => {
990
1422
  let cancelled = false;
991
1423
  const db = getSupabaseClientOptional();
@@ -1023,6 +1455,19 @@ function OrderDetailView({ orderId, provider, onBack }) {
1023
1455
  useEffect(() => {
1024
1456
  void loadReceivable();
1025
1457
  }, [loadReceivable]);
1458
+ useEffect(() => {
1459
+ let cancelled = false;
1460
+ const shop = providerRef.current();
1461
+ const find = shop.findOpenOrderCharge;
1462
+ if (!find || !order || order.financialStatus !== "pending") return;
1463
+ void find.call(shop, orderId).then((standing) => {
1464
+ if (!cancelled && standing) setCharge(fromSession(standing));
1465
+ }).catch(() => {
1466
+ });
1467
+ return () => {
1468
+ cancelled = true;
1469
+ };
1470
+ }, [orderId, order?.financialStatus]);
1026
1471
  if (loading) return /* @__PURE__ */ jsx(OrderDetailSkeleton, { parentLabel: "Pedidos", onBack });
1027
1472
  if (error) {
1028
1473
  return /* @__PURE__ */ jsxs("div", { className: "rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-800", children: [
@@ -1097,80 +1542,115 @@ function OrderDetailView({ orderId, provider, onBack }) {
1097
1542
  setStatusSaving(false);
1098
1543
  }
1099
1544
  }
1545
+ async function openCharge(asLink) {
1546
+ if (charge) return;
1547
+ setCharging(asLink ? "link" : "pix");
1548
+ setChargeError(null);
1549
+ try {
1550
+ const session = await provider().chargeExistingOrder(orderId, { asLink });
1551
+ setCharge(fromSession(session));
1552
+ } catch (err) {
1553
+ const message = errorMessage(err);
1554
+ setChargeError(message);
1555
+ toast.error("N\xE3o consegui abrir a cobran\xE7a", { description: message });
1556
+ } finally {
1557
+ setCharging(null);
1558
+ }
1559
+ }
1560
+ const missingOnCustomer = customer ? [
1561
+ !customer.document && { key: "cpf", label: "+ Adicionar CPF" },
1562
+ !customer.email && { key: "email", label: "+ Adicionar e-mail" },
1563
+ !customer.phone && { key: "phone", label: "+ Adicionar celular" }
1564
+ ].filter(Boolean) : [];
1100
1565
  const destructive = pendingStatus === "cancelled";
1101
- const statusActions = pendingStatus ? /* @__PURE__ */ jsxs("div", { className: `w-full space-y-2 rounded-md border px-3 py-2 ${destructive ? "border-red-300 bg-red-50" : "border-amber-300 bg-amber-50"}`, children: [
1102
- /* @__PURE__ */ jsx("p", { className: `text-xs ${destructive ? "text-red-900" : "text-amber-900"}`, children: LIFECYCLE_CONFIRM[pendingStatus].warning }),
1103
- destructive && order.financialStatus === "paid" && /* @__PURE__ */ jsx("p", { className: "text-xs font-medium text-red-900", children: "Este pedido est\xE1 pago. Cancelar N\xC3O devolve o dinheiro \u2014 o estorno \xE9 feito na m\xE3o, no Financeiro ou no seu meio de pagamento." }),
1104
- destructive && /* @__PURE__ */ jsx(
1105
- "input",
1106
- {
1107
- value: cancelReason,
1108
- onChange: (event) => setCancelReason(event.target.value),
1109
- placeholder: "Motivo (opcional) \u2014 fica no hist\xF3rico do pedido",
1110
- "data-testid": "order-cancel-reason",
1111
- className: "w-full rounded-md border bg-background px-2.5 py-1.5 text-sm outline-none focus:border-primary"
1112
- }
1113
- ),
1114
- statusError && /* @__PURE__ */ jsx("p", { className: "rounded-md border border-red-200 bg-white px-2.5 py-1.5 text-xs text-red-800", children: statusError }),
1115
- /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
1566
+ function closeStatus() {
1567
+ setPendingStatus(null);
1568
+ setStatusError(null);
1569
+ setCancelReason("");
1570
+ }
1571
+ const statusModal = /* @__PURE__ */ jsx(Modal, { open: pendingStatus !== null, onOpenChange: (isOpen) => {
1572
+ if (!isOpen && !statusSaving) closeStatus();
1573
+ }, children: /* @__PURE__ */ jsx(ModalContent, { size: "sm", children: pendingStatus && /* @__PURE__ */ jsxs(Fragment, { children: [
1574
+ /* @__PURE__ */ jsxs(ModalHeader, { children: [
1575
+ /* @__PURE__ */ jsx(ModalTitle, { children: LIFECYCLE_CONFIRM[pendingStatus].cta }),
1576
+ /* @__PURE__ */ jsx(ModalDescription, { children: LIFECYCLE_CONFIRM[pendingStatus].warning })
1577
+ ] }),
1578
+ /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
1579
+ destructive && order.financialStatus === "paid" && /* @__PURE__ */ jsx("p", { className: "rounded-md border border-red-200 bg-red-50 px-2.5 py-1.5 text-xs font-medium text-red-900", children: "Este pedido est\xE1 pago. Cancelar N\xC3O devolve o dinheiro \u2014 o estorno \xE9 feito na m\xE3o, no Financeiro ou no seu meio de pagamento." }),
1580
+ destructive && /* @__PURE__ */ jsx(
1581
+ "input",
1582
+ {
1583
+ value: cancelReason,
1584
+ onChange: (event) => setCancelReason(event.target.value),
1585
+ placeholder: "Motivo (opcional) \u2014 fica no hist\xF3rico do pedido",
1586
+ "data-testid": "order-cancel-reason",
1587
+ className: "w-full rounded-md border bg-background px-2.5 py-1.5 text-sm outline-none focus:border-primary"
1588
+ }
1589
+ ),
1590
+ statusError && /* @__PURE__ */ jsx("p", { role: "alert", className: "rounded-md border border-red-200 bg-red-50 px-2.5 py-1.5 text-xs text-red-800", children: statusError })
1591
+ ] }),
1592
+ /* @__PURE__ */ jsxs(ModalFooter, { children: [
1593
+ /* @__PURE__ */ jsx(Button, { variant: "outline", onClick: closeStatus, disabled: statusSaving, children: "Voltar" }),
1116
1594
  /* @__PURE__ */ jsx(
1117
- "button",
1595
+ Button,
1118
1596
  {
1119
- type: "button",
1597
+ variant: destructive ? "destructive" : "default",
1120
1598
  onClick: () => applyStatus(pendingStatus),
1121
1599
  disabled: statusSaving,
1122
1600
  "data-testid": "order-status-confirm",
1123
- className: `rounded-md px-3 py-1.5 text-sm font-medium text-white transition disabled:opacity-60 ${destructive ? "bg-red-600 hover:bg-red-700" : "bg-primary hover:opacity-90"}`,
1124
1601
  children: statusSaving ? "Salvando\u2026" : LIFECYCLE_CONFIRM[pendingStatus].cta
1125
1602
  }
1126
- ),
1127
- /* @__PURE__ */ jsx(
1128
- "button",
1129
- {
1130
- type: "button",
1131
- onClick: () => {
1132
- setPendingStatus(null);
1133
- setStatusError(null);
1134
- setCancelReason("");
1135
- },
1136
- className: "rounded-md border bg-background px-3 py-1.5 text-sm font-medium transition hover:bg-muted",
1137
- children: "Voltar"
1138
- }
1139
1603
  )
1140
1604
  ] })
1141
- ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
1142
- order.status !== "open" && /* @__PURE__ */ jsx(
1143
- "button",
1144
- {
1145
- type: "button",
1146
- onClick: () => setPendingStatus("open"),
1147
- "data-testid": "order-reopen",
1148
- className: "rounded-md border px-3 py-1.5 text-sm font-medium transition hover:bg-muted",
1149
- children: "Reabrir pedido"
1150
- }
1151
- ),
1152
- order.status === "open" && /* @__PURE__ */ jsx(
1153
- "button",
1154
- {
1155
- type: "button",
1156
- onClick: () => setPendingStatus("archived"),
1157
- "data-testid": "order-archive",
1158
- className: "rounded-md border px-3 py-1.5 text-sm font-medium transition hover:bg-muted",
1159
- children: "Arquivar"
1160
- }
1161
- ),
1162
- order.status !== "cancelled" && /* @__PURE__ */ jsx(
1163
- "button",
1605
+ ] }) }) });
1606
+ const statusActions = (
1607
+ // Three hand-written <button>s of equal weight, with the destructive one
1608
+ // last and loudest, was the shape this page had. Declared instead: editing
1609
+ // is what you came for, archiving and cancelling live behind the caret, and
1610
+ // the destructive tone comes from the token rather than `border-red-200`.
1611
+ /* @__PURE__ */ jsx(
1612
+ PageActions,
1164
1613
  {
1165
- type: "button",
1166
- onClick: () => setPendingStatus("cancelled"),
1167
- "data-testid": "order-cancel",
1168
- className: "rounded-md border border-red-200 px-3 py-1.5 text-sm font-medium text-red-700 transition hover:bg-red-50",
1169
- children: "Cancelar pedido"
1614
+ actions: [
1615
+ ...order.status !== "open" ? [{ id: "reopen", testId: "order-reopen", label: "Reabrir pedido", icon: RotateCcw, intent: "primary", onSelect: () => setPendingStatus("open") }] : [],
1616
+ // UMA porta. Pix, link e "já recebi" eram três entradas de menu dizendo
1617
+ // quase a mesma coisa — pôr o pedido em dia —, e a escolha entre elas é
1618
+ // do momento, não do menu. Ela mora dentro do modal, que abre no clique
1619
+ // e mostra o esqueleto enquanto o adquirente responde.
1620
+ ...order.status === "open" && order.financialStatus === "pending" && typeof provider().chargeExistingOrder === "function" ? [{
1621
+ id: "charge",
1622
+ testId: "order-charge",
1623
+ label: charge ? "Ver cobran\xE7a" : "Cobrar",
1624
+ icon: QrCode,
1625
+ intent: "primary",
1626
+ onSelect: () => {
1627
+ setChargeError(null);
1628
+ setChargeOpen(true);
1629
+ }
1630
+ }] : [],
1631
+ // Nothing paid, nothing shipped, and the STORE typed it — still a draft,
1632
+ // so items, customer and address can change without cancelling the sale.
1633
+ // An order from the storefront is out: that cart belongs to the buyer.
1634
+ ...order.status === "open" && order.financialStatus === "pending" && order.fulfillmentStatus === "unfulfilled" && order.origin !== "storefront" && typeof provider().reviseOrder === "function" ? [{
1635
+ id: "edit",
1636
+ testId: "order-edit",
1637
+ label: "Editar pedido",
1638
+ icon: Pencil,
1639
+ // Secundária enquanto há o que cobrar: a casca pinta UMA primária,
1640
+ // e num pedido em aberto ela é a que põe dinheiro na loja.
1641
+ intent: typeof provider().chargeExistingOrder === "function" ? "secondary" : "primary",
1642
+ onSelect: () => adminNavigateTo(`/orders/${order.id}/edit`)
1643
+ }] : [],
1644
+ // Cobrar um pedido que já existe (0073). Salvar virou a ação principal
1645
+ // da venda, e até aqui a única saída depois era "registrar recebimento"
1646
+ // — que é dizer que o dinheiro entrou, não pedi-lo. Pix e link são a
1647
+ // MESMA cobrança: muda por onde ela chega ao cliente.
1648
+ ...order.status === "open" ? [{ id: "archive", testId: "order-archive", label: "Arquivar", icon: Archive, intent: "secondary", onSelect: () => setPendingStatus("archived") }] : [],
1649
+ ...order.status !== "cancelled" ? [{ id: "cancel", testId: "order-cancel", label: "Cancelar pedido", icon: Ban, intent: "danger", onSelect: () => setPendingStatus("cancelled") }] : []
1650
+ ]
1170
1651
  }
1171
- ),
1172
- statusError && /* @__PURE__ */ jsx("p", { className: "w-full rounded-md border border-red-200 bg-red-50 px-3 py-2 text-xs text-red-800", children: statusError })
1173
- ] });
1652
+ )
1653
+ );
1174
1654
  const installments = receivable && receivable.bills.length > 0 ? receivable.bills.map((b) => ({
1175
1655
  number: b.installment_number ?? 0,
1176
1656
  amount: `${money2(Number(b.paid_amount ?? 0), cur)} / ${money2(Number(b.amount), cur)}`,
@@ -1179,8 +1659,20 @@ function OrderDetailView({ orderId, provider, onBack }) {
1179
1659
  dueDate: `vence ${dateOnly(b.due_date)}`
1180
1660
  })) : [];
1181
1661
  const data = {
1662
+ // No topo, onde o layout compartilhado já reserva lugar para uma recusa.
1663
+ errorBanner: chargeError && !chargeOpen ? /* @__PURE__ */ jsx(
1664
+ "div",
1665
+ {
1666
+ role: "alert",
1667
+ "data-testid": "order-charge-error",
1668
+ className: "rounded-md border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-800",
1669
+ children: chargeError
1670
+ }
1671
+ ) : void 0,
1182
1672
  title: `Pedido #${order.orderNumber}`,
1183
- subtitle: dateTime2(order.createdAt),
1673
+ // A data saiu do topo para o rodapé de procedência, que é onde toda ficha
1674
+ // do app guarda "criado em / atualizado em".
1675
+ meta: order,
1184
1676
  badges: [
1185
1677
  { label: ORDER_STATUS_LABEL[order.financialStatus] ?? order.financialStatus, tone: toneOf(order.financialStatus) },
1186
1678
  { label: ORDER_STATUS_LABEL[order.fulfillmentStatus] ?? order.fulfillmentStatus, tone: toneOf(order.fulfillmentStatus) }
@@ -1198,7 +1690,20 @@ function OrderDetailView({ orderId, provider, onBack }) {
1198
1690
  className: "font-medium"
1199
1691
  }
1200
1692
  ) : /* @__PURE__ */ jsx("p", { className: "font-medium", children: order.customerName ?? "\u2014" }),
1201
- order.customerEmail && /* @__PURE__ */ jsx("p", { className: "text-muted-foreground", children: order.customerEmail }),
1693
+ (customer?.email ?? order.customerEmail) && /* @__PURE__ */ jsx("p", { className: "text-muted-foreground", children: customer?.email ?? order.customerEmail }),
1694
+ customer?.phone && /* @__PURE__ */ jsx("p", { className: "text-muted-foreground", children: formatPhone(customer.phone) }),
1695
+ customer?.document && /* @__PURE__ */ jsx("p", { className: "text-muted-foreground", children: formatDocument(customer.document) }),
1696
+ order.customerId && missingOnCustomer.length > 0 && /* @__PURE__ */ jsx("p", { className: "flex flex-wrap items-center gap-x-3 gap-y-1 pt-1", children: missingOnCustomer.map((field) => /* @__PURE__ */ jsx(
1697
+ "button",
1698
+ {
1699
+ type: "button",
1700
+ onClick: () => setEditingCustomer(true),
1701
+ "data-testid": `order-customer-add-${field.key}`,
1702
+ className: "text-xs font-medium text-muted-foreground underline decoration-dashed decoration-from-font underline-offset-4 transition hover:text-foreground",
1703
+ children: field.label
1704
+ },
1705
+ field.key
1706
+ )) }),
1202
1707
  !order.customerId && /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: "Compra sem cadastro \u2014 n\xE3o h\xE1 ficha de cliente para abrir." })
1203
1708
  ] }),
1204
1709
  items: order.items.map((it) => ({
@@ -1230,17 +1735,7 @@ function OrderDetailView({ orderId, provider, onBack }) {
1230
1735
  ],
1231
1736
  installments,
1232
1737
  actions: /* @__PURE__ */ jsxs(Fragment, { children: [
1233
- receivable.balance.balance > 0 && receivable.balance.status !== "cancelled" && /* @__PURE__ */ jsx(
1234
- "button",
1235
- {
1236
- type: "button",
1237
- onClick: registerReceipt,
1238
- disabled: confirming,
1239
- "data-testid": "order-register-receipt",
1240
- className: "rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground transition hover:opacity-90 disabled:opacity-60",
1241
- children: confirming ? "Registrando\u2026" : "Registrar recebimento"
1242
- }
1243
- ),
1738
+ receivable.balance.balance > 0 && receivable.balance.status !== "cancelled" && /* @__PURE__ */ jsx(Button, { onClick: registerReceipt, disabled: confirming, "data-testid": "order-register-receipt", children: confirming ? "Registrando\u2026" : "Registrar recebimento" }),
1244
1739
  hasFinancial && /* @__PURE__ */ jsx(
1245
1740
  "button",
1246
1741
  {
@@ -1308,163 +1803,220 @@ function OrderDetailView({ orderId, provider, onBack }) {
1308
1803
  ] })
1309
1804
  ] }) }),
1310
1805
  /* @__PURE__ */ jsx(OrderProviderLinks, { orderId: order.id }),
1311
- /* @__PURE__ */ jsx(OrderDetailBlock, { title: "Hist\xF3rico", children: /* @__PURE__ */ jsxs("div", { className: "space-y-1 text-sm", children: [
1312
- /* @__PURE__ */ jsx(OrderDetailRow, { label: "Criado", value: dateTime2(order.createdAt) }),
1313
- /* @__PURE__ */ jsx(OrderDetailRow, { label: "Atualizado", value: dateTime2(order.updatedAt) }),
1314
- /* @__PURE__ */ jsx(OrderDetailRow, { label: "Situa\xE7\xE3o", value: /* @__PURE__ */ jsx(InlineBadge, { value: order.status }) })
1315
- ] }) }),
1806
+ /* @__PURE__ */ jsx(OrderDetailBlock, { title: "Situa\xE7\xE3o", children: /* @__PURE__ */ jsx(InlineBadge, { value: order.status }) }),
1316
1807
  order.notes && /* @__PURE__ */ jsx(OrderDetailBlock, { title: "Observa\xE7\xF5es", children: /* @__PURE__ */ jsx("p", { className: "whitespace-pre-wrap text-sm text-muted-foreground", children: order.notes }) })
1317
1808
  ] }),
1318
1809
  onBack,
1319
1810
  backLabel: "Pedidos"
1320
1811
  };
1321
- return /* @__PURE__ */ jsx(OrderDetailView$1, { ...data });
1812
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1813
+ /* @__PURE__ */ jsx(OrderDetailView$1, { ...data }),
1814
+ statusModal,
1815
+ order.customerId && /* @__PURE__ */ jsx(
1816
+ CustomerModal,
1817
+ {
1818
+ open: editingCustomer,
1819
+ customerId: order.customerId,
1820
+ initial: {
1821
+ name: [customer?.firstName, customer?.lastName].filter(Boolean).join(" ") || (order.customerName ?? ""),
1822
+ email: customer?.email ?? order.customerEmail ?? "",
1823
+ phone: customer?.phone ?? "",
1824
+ document: customer?.document ?? ""
1825
+ },
1826
+ provider,
1827
+ onClose: () => setEditingCustomer(false),
1828
+ onSaved: (saved) => {
1829
+ setCustomer(saved);
1830
+ setEditingCustomer(false);
1831
+ toast.success("Cadastro atualizado.");
1832
+ }
1833
+ }
1834
+ ),
1835
+ chargeOpen && /* @__PURE__ */ jsx(
1836
+ NewOrderCharge,
1837
+ {
1838
+ open: chargeOpen,
1839
+ provider,
1840
+ sessionId: charge?.sessionId ?? null,
1841
+ emv: charge?.emv ?? null,
1842
+ qrcodeBase64: charge?.qrcode ?? null,
1843
+ qrcodeUrl: charge?.qrcodeUrl ?? null,
1844
+ paymentUrl: charge?.paymentUrl ?? null,
1845
+ acquirer: charge?.provider ?? null,
1846
+ opening: charging !== null,
1847
+ error: chargeError,
1848
+ onChoose: (asLink) => {
1849
+ void openCharge(asLink);
1850
+ },
1851
+ onRegisterReceipt: () => {
1852
+ setChargeOpen(false);
1853
+ void registerReceipt();
1854
+ },
1855
+ total: charge?.total ?? order.total,
1856
+ currency: cur,
1857
+ customerName: order.customerName ?? "",
1858
+ customerPhone: (order.shippingAddress?.phone ?? "").replace(/\D/gu, ""),
1859
+ onPaid: () => {
1860
+ setChargeOpen(false);
1861
+ shopOrderCache.delete(orderId);
1862
+ void provider().getOrder(orderId).then((o) => {
1863
+ if (o) {
1864
+ shopOrderCache.set(orderId, o);
1865
+ setOrder(o);
1866
+ }
1867
+ });
1868
+ void loadReceivable(true);
1869
+ },
1870
+ onClose: () => setChargeOpen(false)
1871
+ }
1872
+ )
1873
+ ] });
1322
1874
  }
1323
- var POLL_MS = 4e3;
1324
- function NewOrderCharge({
1875
+ function today() {
1876
+ const d = /* @__PURE__ */ new Date();
1877
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
1878
+ }
1879
+ function occurredAtFor(date) {
1880
+ if (!date || date === today()) return void 0;
1881
+ const [y, m, d] = date.split("-").map(Number);
1882
+ if (!y || !m || !d) return void 0;
1883
+ return new Date(y, m - 1, d, 12, 0, 0).toISOString();
1884
+ }
1885
+ function StockEntryModal({
1325
1886
  open,
1887
+ onClose,
1326
1888
  provider,
1327
- sessionId,
1328
- emv,
1329
- qrcodeBase64,
1330
- paymentUrl,
1331
- acquirer,
1332
- total,
1333
- currency: currency2,
1334
- customerName,
1335
- customerPhone,
1336
- onPaid,
1337
- onClose
1889
+ variantId,
1890
+ productId,
1891
+ productName,
1892
+ sku,
1893
+ available,
1894
+ needed,
1895
+ onReceived
1338
1896
  }) {
1339
- const [state, setState] = useState("waiting");
1340
- const [copied, setCopied] = useState(false);
1341
- const [checking, setChecking] = useState(false);
1342
- const settled = useRef(false);
1343
- const money4 = new Intl.NumberFormat("pt-BR", { style: "currency", currency: currency2 }).format(total);
1344
- const check = useCallback(async () => {
1345
- if (settled.current) return;
1346
- setChecking(true);
1347
- try {
1348
- const result = await provider().settleCheckoutSession?.(sessionId, acquirer);
1349
- if (!result) return;
1350
- if (result.state === "paid") {
1351
- settled.current = true;
1352
- setState("paid");
1353
- setTimeout(() => onPaid(result.orderId ?? void 0), 1200);
1354
- return;
1355
- }
1356
- const outcome = chargeOutcome(result.state);
1357
- if (outcome !== "waiting") {
1358
- settled.current = true;
1359
- setState(outcome);
1360
- }
1361
- } finally {
1362
- setChecking(false);
1363
- }
1364
- }, [provider, sessionId, acquirer, onPaid]);
1897
+ const reasons = useMemo(() => listStockReasonCodes("receipt"), []);
1898
+ const shortfall = Math.max(1, (needed ?? 1) - Math.max(0, available ?? 0));
1899
+ const [quantity, setQuantity] = useState(shortfall);
1900
+ const [reasonCode, setReasonCode] = useState("purchase");
1901
+ const [note, setNote] = useState("");
1902
+ const [arrivedOn, setArrivedOn] = useState(today());
1903
+ const [saving, setSaving] = useState(false);
1904
+ const [error, setError] = useState(null);
1365
1905
  useEffect(() => {
1366
1906
  if (!open) return;
1367
- const timer = setInterval(() => {
1368
- void check();
1369
- }, POLL_MS);
1370
- return () => clearInterval(timer);
1371
- }, [open, check]);
1372
- const isLink = Boolean(paymentUrl && !emv);
1373
- const payable = paymentUrl ?? emv ?? "";
1374
- async function copy() {
1375
- if (!payable) return;
1376
- try {
1377
- await navigator.clipboard.writeText(payable);
1378
- setCopied(true);
1379
- setTimeout(() => setCopied(false), 2400);
1380
- } catch {
1907
+ setQuantity(shortfall);
1908
+ setReasonCode("purchase");
1909
+ setNote("");
1910
+ setArrivedOn(today());
1911
+ setError(null);
1912
+ }, [open, shortfall]);
1913
+ const receive = provider().receiveProductStock;
1914
+ const newBalance = available == null ? null : Math.max(0, available) + quantity;
1915
+ async function submit() {
1916
+ if (quantity <= 0) {
1917
+ setError("Informe quantas unidades entraram.");
1918
+ return;
1381
1919
  }
1382
- }
1383
- const greeting = customerName ? ` ${customerName.split(" ")[0]}` : "";
1384
- const message = isLink ? `Ol\xE1${greeting}! Aqui est\xE1 o link para pagar seu pedido de ${money4}:
1385
-
1386
- ${payable}
1387
-
1388
- Voc\xEA escolhe a forma de pagamento na p\xE1gina.` : `Ol\xE1${greeting}! Segue o Pix de ${money4} para o seu pedido:
1389
-
1390
- ${payable}`;
1391
- const whatsapp = customerPhone && payable ? `https://wa.me/${customerPhone.length > 11 ? customerPhone : `55${customerPhone}`}?text=${encodeURIComponent(message)}` : null;
1392
- return /* @__PURE__ */ jsx(Modal, { open, onOpenChange: (next) => {
1393
- if (!next) onClose();
1394
- }, children: /* @__PURE__ */ jsx(ModalPortal, { children: /* @__PURE__ */ jsx(ModalOverlay, { children: /* @__PURE__ */ jsxs(ModalContent, { size: "lg", "data-testid": "new-order-charge-modal", children: [
1395
- /* @__PURE__ */ jsxs(ModalHeader, { className: "mb-4", children: [
1396
- /* @__PURE__ */ jsx(ModalTitle, { children: isLink ? `Link de ${money4}` : `Pix de ${money4}` }),
1397
- state === "waiting" && /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: isLink ? "Mande para o cliente. O pedido \xE9 criado quando o pagamento cair \u2014 pode fechar esta janela." : "Vale por 15 minutos. O pedido \xE9 criado quando o pagamento cair." })
1398
- ] }),
1399
- /* @__PURE__ */ jsxs(ModalBody, { children: [
1400
- /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center gap-4 sm:flex-row sm:items-start", children: [
1401
- qrcodeBase64 && /* @__PURE__ */ jsx(
1402
- "img",
1403
- {
1404
- src: `data:image/png;base64,${qrcodeBase64}`,
1405
- alt: "QR code do Pix",
1406
- "data-testid": "new-order-qr",
1407
- className: "h-40 w-40 shrink-0 rounded-md border bg-white p-2"
1408
- }
1409
- ),
1410
- /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1 space-y-3", children: [
1411
- /* @__PURE__ */ jsxs("div", { children: [
1412
- /* @__PURE__ */ jsx("p", { className: "text-xs font-medium text-muted-foreground", children: isLink ? "Link de pagamento" : "Pix copia e cola" }),
1413
- /* @__PURE__ */ jsx(
1414
- "p",
1415
- {
1416
- "data-testid": isLink ? "new-order-payment-link" : "new-order-emv",
1417
- className: "mt-1 max-h-20 overflow-auto break-all rounded-md border bg-muted/30 p-2 font-mono text-[11px]",
1418
- children: payable
1419
- }
1420
- ),
1421
- isLink && /* @__PURE__ */ jsx("p", { className: "mt-1 text-[11px] text-muted-foreground", children: "O cliente escolhe Pix, boleto ou cart\xE3o na p\xE1gina do adquirente." })
1422
- ] }),
1423
- /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-2", children: [
1424
- /* @__PURE__ */ jsxs(Button, { size: "sm", onClick: copy, "data-testid": "new-order-copy", children: [
1425
- copied ? /* @__PURE__ */ jsx(Check, { className: "mr-1.5 h-3.5 w-3.5" }) : /* @__PURE__ */ jsx(Copy, { className: "mr-1.5 h-3.5 w-3.5" }),
1426
- copied ? "Copiado" : isLink ? "Copiar link" : "Copiar c\xF3digo"
1427
- ] }),
1428
- whatsapp && /* @__PURE__ */ jsxs(
1429
- "a",
1430
- {
1431
- href: whatsapp,
1432
- target: "_blank",
1433
- rel: "noreferrer",
1434
- "data-testid": "new-order-whatsapp",
1435
- className: "inline-flex items-center rounded-md border px-3 py-1.5 text-sm font-medium transition hover:bg-muted",
1436
- children: [
1437
- /* @__PURE__ */ jsx(MessageCircle, { className: "mr-1.5 h-3.5 w-3.5" }),
1438
- "Mandar no WhatsApp"
1439
- ]
1440
- }
1441
- )
1442
- ] })
1920
+ setSaving(true);
1921
+ setError(null);
1922
+ try {
1923
+ const now = await provider().receiveProductStock(productId, quantity, {
1924
+ variantId,
1925
+ reasonCode,
1926
+ note: note.trim() || void 0,
1927
+ occurredAt: occurredAtFor(arrivedOn)
1928
+ });
1929
+ onReceived(now);
1930
+ } catch (err) {
1931
+ setError(errorMessage(err));
1932
+ } finally {
1933
+ setSaving(false);
1934
+ }
1935
+ }
1936
+ const inputClass = "mt-1 w-full rounded-md border bg-background px-2.5 py-1.5 text-sm outline-none focus:border-primary";
1937
+ return /* @__PURE__ */ jsx(Modal, { open, onOpenChange: (next) => {
1938
+ if (!next) onClose();
1939
+ }, children: /* @__PURE__ */ jsx(ModalPortal, { children: /* @__PURE__ */ jsx(ModalOverlay, { children: /* @__PURE__ */ jsxs(ModalContent, { size: "lg", "data-testid": "stock-entry-modal", children: [
1940
+ /* @__PURE__ */ jsxs(ModalHeader, { className: "mb-4", children: [
1941
+ /* @__PURE__ */ jsx(ModalTitle, { children: "Dar entrada no estoque" }),
1942
+ /* @__PURE__ */ jsx(ModalDescription, { children: "Entra no hist\xF3rico como recebimento \u2014 o pedido segue logo depois." })
1943
+ ] }),
1944
+ /* @__PURE__ */ jsxs(ModalBody, { className: "space-y-3", children: [
1945
+ /* @__PURE__ */ jsxs("div", { className: "rounded-md border bg-muted/30 px-3 py-2 text-sm", children: [
1946
+ /* @__PURE__ */ jsx("p", { className: "font-medium", children: productName }),
1947
+ /* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground", children: [
1948
+ sku ? `${sku} \xB7 ` : "",
1949
+ available == null ? "Estoque desconhecido" : `${available} em estoque`,
1950
+ needed != null ? ` \xB7 o pedido precisa de ${needed}` : ""
1443
1951
  ] })
1444
1952
  ] }),
1445
- /* @__PURE__ */ jsxs("div", { className: "mt-4 rounded-md border bg-muted/30 px-3 py-2 text-sm", children: [
1446
- state === "waiting" && /* @__PURE__ */ jsxs("p", { className: "flex items-center gap-2 text-muted-foreground", "data-testid": "new-order-waiting", children: [
1447
- /* @__PURE__ */ jsx("span", { className: "h-2 w-2 animate-pulse rounded-full bg-amber-500" }),
1448
- "Aguardando o pagamento\u2026 a tela confirma sozinha."
1449
- ] }),
1450
- state === "paid" && /* @__PURE__ */ jsxs("p", { className: "flex items-center gap-2 font-medium text-emerald-700", "data-testid": "new-order-paid", children: [
1451
- /* @__PURE__ */ jsx(Check, { className: "h-4 w-4" }),
1452
- " Pagamento confirmado \u2014 abrindo o pedido\u2026"
1453
- ] }),
1454
- (state === "expired" || state === "failed") && /* @__PURE__ */ jsx("p", { className: "text-red-700", "data-testid": "new-order-dead", children: state === "expired" ? `${isLink ? "O link expirou" : "O c\xF3digo expirou"} sem pagamento. O pedido continua aqui \u2014 d\xE1 para cobrar de outro jeito.` : "O pagamento n\xE3o foi conclu\xEDdo. O pedido continua aqui \u2014 d\xE1 para cobrar de outro jeito." })
1455
- ] })
1953
+ !receive && /* @__PURE__ */ jsx("p", { role: "alert", className: "rounded-md border border-amber-200 bg-amber-50 px-3 py-2 text-sm text-amber-900", children: "Esta loja n\xE3o registra entradas por aqui. Lance a entrada pela tela de estoque." }),
1954
+ /* @__PURE__ */ jsxs("label", { className: "block", children: [
1955
+ /* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-muted-foreground", children: "Quantidade recebida" }),
1956
+ /* @__PURE__ */ jsx(
1957
+ "input",
1958
+ {
1959
+ type: "number",
1960
+ min: 1,
1961
+ step: 1,
1962
+ autoFocus: true,
1963
+ value: quantity,
1964
+ onChange: (event) => setQuantity(Math.max(0, Math.trunc(Number(event.target.value) || 0))),
1965
+ "data-testid": "stock-entry-quantity",
1966
+ className: inputClass
1967
+ }
1968
+ ),
1969
+ newBalance != null && /* @__PURE__ */ jsxs("span", { className: "mt-0.5 block text-[11px] text-muted-foreground", children: [
1970
+ "Saldo depois da entrada: ",
1971
+ newBalance
1972
+ ] })
1973
+ ] }),
1974
+ /* @__PURE__ */ jsxs("label", { className: "block", children: [
1975
+ /* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-muted-foreground", children: "Data de entrada" }),
1976
+ /* @__PURE__ */ jsx(
1977
+ "input",
1978
+ {
1979
+ type: "date",
1980
+ value: arrivedOn,
1981
+ max: today(),
1982
+ onChange: (event) => setArrivedOn(event.target.value || today()),
1983
+ "data-testid": "stock-entry-date",
1984
+ className: inputClass
1985
+ }
1986
+ ),
1987
+ /* @__PURE__ */ jsx("span", { className: "mt-0.5 block text-[11px] text-muted-foreground", children: "Quando a mercadoria chegou. O lan\xE7amento fica com a data de hoje." })
1988
+ ] }),
1989
+ /* @__PURE__ */ jsxs("label", { className: "block", children: [
1990
+ /* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-muted-foreground", children: "Motivo" }),
1991
+ /* @__PURE__ */ jsx(
1992
+ "select",
1993
+ {
1994
+ value: reasonCode,
1995
+ onChange: (event) => setReasonCode(event.target.value),
1996
+ "data-testid": "stock-entry-reason",
1997
+ className: inputClass,
1998
+ children: reasons.map((reason) => /* @__PURE__ */ jsx("option", { value: reason.key, children: reason.label }, reason.key))
1999
+ }
2000
+ )
2001
+ ] }),
2002
+ /* @__PURE__ */ jsxs("label", { className: "block", children: [
2003
+ /* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-muted-foreground", children: "Observa\xE7\xE3o" }),
2004
+ /* @__PURE__ */ jsx(
2005
+ "input",
2006
+ {
2007
+ value: note,
2008
+ onChange: (event) => setNote(event.target.value),
2009
+ placeholder: "Nota fiscal, fornecedor \u2014 fica no hist\xF3rico",
2010
+ "data-testid": "stock-entry-note",
2011
+ className: inputClass
2012
+ }
2013
+ )
2014
+ ] }),
2015
+ error && /* @__PURE__ */ jsx("p", { role: "alert", className: "rounded-md border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-800", children: error })
1456
2016
  ] }),
1457
2017
  /* @__PURE__ */ jsxs(ModalFooter, { className: "mt-4 gap-2", children: [
1458
- state === "waiting" && /* @__PURE__ */ jsx(Button, { variant: "outline", onClick: () => void check(), disabled: checking, "data-testid": "new-order-check", children: checking ? "Conferindo\u2026" : "J\xE1 pagou? Conferir" }),
1459
- /* @__PURE__ */ jsx(
1460
- Button,
1461
- {
1462
- variant: state === "waiting" ? "ghost" : "default",
1463
- onClick: onClose,
1464
- "data-testid": "new-order-charge-close",
1465
- children: state === "waiting" ? "Fechar" : "Voltar ao pedido"
1466
- }
1467
- )
2018
+ /* @__PURE__ */ jsx(Button, { variant: "outline", onClick: onClose, disabled: saving, children: "Cancelar" }),
2019
+ /* @__PURE__ */ jsx(Button, { onClick: () => void submit(), disabled: saving || !receive, "data-testid": "stock-entry-save", children: saving ? "Registrando\u2026" : "Registrar entrada" })
1468
2020
  ] })
1469
2021
  ] }) }) }) });
1470
2022
  }
@@ -1487,10 +2039,10 @@ var PAYMENT_METHODS = [
1487
2039
  { id: "other", label: "Outro" }
1488
2040
  ];
1489
2041
  var money3 = (value, currency2 = "BRL") => new Intl.NumberFormat("pt-BR", { style: "currency", currency: currency2 }).format(Number(value) || 0);
1490
- var onlyDigits = (value) => value.replace(/\D/gu, "");
2042
+ var onlyDigits2 = (value) => value.replace(/\D/gu, "");
1491
2043
  var maskPostal = (value) => {
1492
- const digits = onlyDigits(value).slice(0, 8);
1493
- return digits.length > 5 ? `${digits.slice(0, 5)}-${digits.slice(5)}` : digits;
2044
+ const digits2 = onlyDigits2(value).slice(0, 8);
2045
+ return digits2.length > 5 ? `${digits2.slice(0, 5)}-${digits2.slice(5)}` : digits2;
1494
2046
  };
1495
2047
  var fieldClass = "mt-1 w-full rounded-md border bg-background px-2.5 py-1.5 text-sm outline-none focus:border-primary";
1496
2048
  function Field({ label, hint, children }) {
@@ -1500,7 +2052,10 @@ function Field({ label, hint, children }) {
1500
2052
  hint && /* @__PURE__ */ jsx("span", { className: "mt-0.5 block text-[11px] text-muted-foreground", children: hint })
1501
2053
  ] });
1502
2054
  }
1503
- function NewOrderPage({ provider, onDone, onCancel }) {
2055
+ function NewOrderPage({ provider, onDone, onCancel, orderId }) {
2056
+ const editing = Boolean(orderId);
2057
+ const [loading, setLoading] = useState(Boolean(orderId));
2058
+ const [orderNumber, setOrderNumber] = useState(null);
1504
2059
  const [lines, setLines] = useState([]);
1505
2060
  const [customer, setCustomer] = useState(EMPTY_CUSTOMER);
1506
2061
  const [address, setAddress] = useState(EMPTY_ADDRESS);
@@ -1516,7 +2071,68 @@ function NewOrderPage({ provider, onDone, onCancel }) {
1516
2071
  const [error, setError] = useState(null);
1517
2072
  const [session, setSession] = useState(null);
1518
2073
  const [chargeOpen, setChargeOpen] = useState(false);
2074
+ const [charging, setCharging] = useState(false);
2075
+ const [lastEnding, setLastEnding] = useState(null);
2076
+ const [entryOpen, setEntryOpen] = useState(false);
1519
2077
  const [editingCustomer, setEditingCustomer] = useState(false);
2078
+ useEffect(() => {
2079
+ if (!orderId) return;
2080
+ let cancelled = false;
2081
+ setLoading(true);
2082
+ void (async () => {
2083
+ try {
2084
+ const order = await provider().getOrder(orderId);
2085
+ if (cancelled || !order) return;
2086
+ setOrderNumber(order.orderNumber);
2087
+ setLines(order.items.map((item, index) => ({
2088
+ key: `${item.productId ?? item.id}:${index}`,
2089
+ productId: item.productId ?? "",
2090
+ name: item.name,
2091
+ sku: item.sku,
2092
+ imageUrl: item.imageUrl,
2093
+ unitPrice: item.unitPrice,
2094
+ quantity: item.quantity,
2095
+ // A lista de variações não é recarregada: a linha já sabe qual foi
2096
+ // vendida, e quem quiser trocar de variação tira a linha e escolhe o
2097
+ // produto de novo — que é o mesmo caminho de sempre.
2098
+ variants: [],
2099
+ variantId: item.variantId ?? null,
2100
+ optionsLabel: null,
2101
+ stock: null
2102
+ })));
2103
+ setNotes(order.notes ?? "");
2104
+ setDiscountCode(order.discountCode ?? "");
2105
+ const customerRow = order.customerId ? await provider().getCustomer(order.customerId).catch(() => null) : null;
2106
+ if (cancelled) return;
2107
+ setCustomer({
2108
+ id: order.customerId,
2109
+ name: customerRow ? `${customerRow.firstName} ${customerRow.lastName}`.trim() : order.customerName ?? "",
2110
+ email: customerRow?.email ?? order.customerEmail ?? "",
2111
+ phone: customerRow?.phone ?? "",
2112
+ document: customerRow?.document ?? ""
2113
+ });
2114
+ const addr = order.shippingAddress;
2115
+ if (addr) {
2116
+ setAddress({
2117
+ postalCode: addr.postal_code ?? "",
2118
+ street: addr.street ?? "",
2119
+ number: addr.number ?? "",
2120
+ complement: addr.complement ?? "",
2121
+ district: addr.district ?? "",
2122
+ city: addr.city ?? "",
2123
+ state: addr.state ?? ""
2124
+ });
2125
+ }
2126
+ } catch (err) {
2127
+ if (!cancelled) setError(errorMessage(err));
2128
+ } finally {
2129
+ if (!cancelled) setLoading(false);
2130
+ }
2131
+ })();
2132
+ return () => {
2133
+ cancelled = true;
2134
+ };
2135
+ }, [orderId, provider]);
1520
2136
  useEffect(() => {
1521
2137
  let cancelled = false;
1522
2138
  provider().getStoreSettings?.().then((value) => {
@@ -1535,7 +2151,7 @@ function NewOrderPage({ provider, onDone, onCancel }) {
1535
2151
  () => lines.reduce((sum, line) => sum + line.unitPrice * line.quantity, 0),
1536
2152
  [lines]
1537
2153
  );
1538
- const postalDigits = onlyDigits(address.postalCode);
2154
+ const postalDigits = onlyDigits2(address.postalCode);
1539
2155
  const delivers = postalDigits.length === 8;
1540
2156
  const shipping = delivers ? freight?.price ?? 0 : 0;
1541
2157
  const quoteFreight = useCallback(async (postal) => {
@@ -1572,7 +2188,8 @@ function NewOrderPage({ provider, onDone, onCancel }) {
1572
2188
  quantity: 1,
1573
2189
  variants: sellable.length > 1 ? sellable : [],
1574
2190
  variantId: only?.id ?? null,
1575
- optionsLabel: null
2191
+ optionsLabel: null,
2192
+ stock: only?.inventoryCount ?? product.inventoryCount ?? null
1576
2193
  }];
1577
2194
  });
1578
2195
  }
@@ -1598,8 +2215,8 @@ function NewOrderPage({ provider, onDone, onCancel }) {
1598
2215
  state: address.state.trim().toUpperCase(),
1599
2216
  country: "BR",
1600
2217
  recipient: customer.name.trim() || void 0,
1601
- phone: onlyDigits(customer.phone) || void 0,
1602
- document: onlyDigits(customer.document) || void 0
2218
+ phone: onlyDigits2(customer.phone) || void 0,
2219
+ document: onlyDigits2(customer.document) || void 0
1603
2220
  } : void 0;
1604
2221
  function validate(ending) {
1605
2222
  return validateNewOrder({
@@ -1607,14 +2224,14 @@ function NewOrderPage({ provider, onDone, onCancel }) {
1607
2224
  unresolvedVariants: lines.filter((line) => line.variants.length > 0 && !line.variantId).length,
1608
2225
  customerName: customer.name,
1609
2226
  customerEmail: customer.email,
1610
- customerPhone: onlyDigits(customer.phone),
2227
+ customerPhone: onlyDigits2(customer.phone),
1611
2228
  customerDocument: customer.document.trim(),
1612
2229
  documentValid: isValidDocument(customer.document),
1613
2230
  delivers,
1614
2231
  addressComplete: Boolean(address.street.trim() && address.city.trim() && address.state.trim())
1615
2232
  }, ending);
1616
2233
  }
1617
- async function resolveCustomerId() {
2234
+ async function resolveCustomerId2() {
1618
2235
  if (customer.id) return customer.id;
1619
2236
  const email = customer.email.trim().toLowerCase();
1620
2237
  const [first, ...rest] = customer.name.trim().split(/\s+/u);
@@ -1627,7 +2244,7 @@ function NewOrderPage({ provider, onDone, onCancel }) {
1627
2244
  firstName: first ?? customer.name.trim(),
1628
2245
  lastName: rest.join(" "),
1629
2246
  email: email || void 0,
1630
- phone: onlyDigits(customer.phone) || void 0
2247
+ phone: onlyDigits2(customer.phone) || void 0
1631
2248
  });
1632
2249
  return created.id;
1633
2250
  }
@@ -1637,6 +2254,7 @@ function NewOrderPage({ provider, onDone, onCancel }) {
1637
2254
  setError(invalid);
1638
2255
  return;
1639
2256
  }
2257
+ setLastEnding(ending);
1640
2258
  setSaving(true);
1641
2259
  setError(null);
1642
2260
  try {
@@ -1667,14 +2285,15 @@ function NewOrderPage({ provider, onDone, onCancel }) {
1667
2285
  sessionId: opened.sessionId,
1668
2286
  emv: opened.charge?.emv ?? null,
1669
2287
  qrcode: opened.charge?.qrcodeBase64 ?? null,
2288
+ qrcodeUrl: opened.charge?.qrcodeUrl ?? null,
1670
2289
  paymentUrl: opened.paymentUrl ?? null,
1671
2290
  total: opened.total
1672
2291
  });
1673
2292
  setChargeOpen(true);
1674
2293
  return;
1675
2294
  }
1676
- const customerId = await resolveCustomerId();
1677
- const order = await provider().placeOrder({
2295
+ const customerId = await resolveCustomerId2();
2296
+ const payload = {
1678
2297
  customerId,
1679
2298
  customer: { name: customer.name.trim(), email: customer.email.trim().toLowerCase() || void 0 },
1680
2299
  items: lines.map((line) => ({
@@ -1687,10 +2306,14 @@ function NewOrderPage({ provider, onDone, onCancel }) {
1687
2306
  discountCode: discountCode.trim() || void 0,
1688
2307
  paymentMethod: ending === "received" ? method : void 0,
1689
2308
  notes: notes.trim() || void 0
1690
- });
2309
+ };
2310
+ const order = orderId ? await provider().reviseOrder(orderId, payload) : await provider().placeOrder(payload);
1691
2311
  if (ending === "received") {
1692
2312
  await provider().confirmPayment?.(order.id, reference.trim() || `manual:${method}`);
1693
2313
  }
2314
+ toast.success(
2315
+ orderId ? `Pedido #${order.orderNumber} atualizado.` : ending === "received" ? `Pedido #${order.orderNumber} salvo e marcado como pago.` : `Pedido #${order.orderNumber} salvo.`
2316
+ );
1694
2317
  onDone(order.id);
1695
2318
  } catch (err) {
1696
2319
  setError(errorMessage(err));
@@ -1698,6 +2321,35 @@ function NewOrderPage({ provider, onDone, onCancel }) {
1698
2321
  setSaving(false);
1699
2322
  }
1700
2323
  }
2324
+ useSaveBar({
2325
+ dirty: lines.length > 0 && !loading && !session && !settling && !charging,
2326
+ saving,
2327
+ onSave: () => {
2328
+ void finish("later");
2329
+ },
2330
+ onDiscard: () => {
2331
+ if (orderId) {
2332
+ onCancel();
2333
+ return;
2334
+ }
2335
+ setLines([]);
2336
+ setCustomer(EMPTY_CUSTOMER);
2337
+ setAddress(EMPTY_ADDRESS);
2338
+ setNotes("");
2339
+ setDiscountCode("");
2340
+ setCouponOpen(false);
2341
+ setError(null);
2342
+ },
2343
+ message: orderId ? "Altera\xE7\xF5es n\xE3o salvas" : "Pedido n\xE3o salvo",
2344
+ saveLabel: orderId ? "Salvar altera\xE7\xF5es" : "Salvar pedido",
2345
+ discardLabel: orderId ? "Sair sem salvar" : "Descartar"
2346
+ });
2347
+ const blockedLine = useMemo(() => {
2348
+ const name = insufficientStockProduct(error);
2349
+ if (!name) return null;
2350
+ const wanted = name.trim().toLowerCase();
2351
+ return lines.find((line) => line.name.trim().toLowerCase() === wanted) ?? null;
2352
+ }, [error, lines]);
1701
2353
  const sideSections = /* @__PURE__ */ jsxs(Fragment, { children: [
1702
2354
  /* @__PURE__ */ jsx(OrderFormBlock, { title: "Cliente", children: /* @__PURE__ */ jsx(
1703
2355
  CustomerPicker,
@@ -1734,10 +2386,10 @@ function NewOrderPage({ provider, onDone, onCancel }) {
1734
2386
  /* @__PURE__ */ jsxs(
1735
2387
  OrderFormView,
1736
2388
  {
1737
- title: "Novo pedido",
2389
+ title: orderId ? `Editar pedido${orderNumber ? ` #${orderNumber}` : ""}` : "Novo pedido",
1738
2390
  parentLabel: "Pedidos",
1739
2391
  onBack: onCancel,
1740
- subtitle: "Venda por telefone, WhatsApp ou balc\xE3o",
2392
+ subtitle: orderId ? "Enquanto ningu\xE9m pagou, o pedido ainda \xE9 rascunho" : "Venda por telefone, WhatsApp ou balc\xE3o",
1741
2393
  sideSections,
1742
2394
  children: [
1743
2395
  /* @__PURE__ */ jsxs(OrderFormBlock, { title: "Produtos", children: [
@@ -1779,7 +2431,7 @@ function NewOrderPage({ provider, onDone, onCancel }) {
1779
2431
  "input",
1780
2432
  {
1781
2433
  value: line.quantity,
1782
- onChange: (event) => setQuantity(line.key, Number(onlyDigits(event.target.value) || 0)),
2434
+ onChange: (event) => setQuantity(line.key, Number(onlyDigits2(event.target.value) || 0)),
1783
2435
  "aria-label": "Quantidade",
1784
2436
  "data-testid": "new-order-quantity",
1785
2437
  className: "fz-tap w-14 rounded-md border bg-background px-1 py-1.5 text-center text-sm tabular-nums outline-none focus:border-primary"
@@ -1892,57 +2544,85 @@ function NewOrderPage({ provider, onDone, onCancel }) {
1892
2544
  customer.name
1893
2545
  ]
1894
2546
  }
2547
+ ),
2548
+ blockedLine && /* @__PURE__ */ jsxs(
2549
+ "button",
2550
+ {
2551
+ type: "button",
2552
+ onClick: () => setEntryOpen(true),
2553
+ "data-testid": "new-order-stock-entry",
2554
+ className: "mt-1 font-medium underline underline-offset-2",
2555
+ children: [
2556
+ "Dar entrada no estoque de ",
2557
+ blockedLine.name
2558
+ ]
2559
+ }
1895
2560
  )
1896
2561
  ]
1897
2562
  }
1898
2563
  ),
1899
- lines.length > 0 && /* @__PURE__ */ jsx("div", { className: "mt-4 flex flex-wrap items-center gap-2", children: settling ? /* @__PURE__ */ jsxs(Fragment, { children: [
2564
+ lines.length > 0 && !editing && /* @__PURE__ */ jsx("div", { className: "mt-4 flex flex-wrap items-center gap-2", children: settling ? /* @__PURE__ */ jsxs(Fragment, { children: [
1900
2565
  /* @__PURE__ */ jsx(Button, { onClick: () => void finish("received"), disabled: saving, "data-testid": "new-order-confirm-received", children: saving ? "Criando\u2026" : "Confirmar pagamento" }),
1901
2566
  /* @__PURE__ */ jsx(Button, { variant: "outline", onClick: () => setSettling(false), children: "Voltar" })
1902
- ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
2567
+ ] }) : charging ? (
2568
+ /* Pix e link são a MESMA cobrança — muda só por onde ela chega ao
2569
+ cliente. A escolha só aparece para quem tem as duas. */
2570
+ /* @__PURE__ */ jsxs(Fragment, { children: [
2571
+ /* @__PURE__ */ jsx(Button, { onClick: () => void finish("charge"), disabled: saving, "data-testid": "new-order-charge-pix", children: saving ? "Abrindo\u2026" : "Cobran\xE7a Pix" }),
2572
+ /* @__PURE__ */ jsx(Button, { variant: "outline", onClick: () => void finish("link"), disabled: saving, "data-testid": "new-order-link", children: saving ? "Gerando\u2026" : "Link de pagamento" }),
2573
+ /* @__PURE__ */ jsx(Button, { variant: "ghost", onClick: () => setCharging(false), children: "Voltar" })
2574
+ ] })
2575
+ ) : session ? (
2576
+ /* Com cobrança aberta o pedido nasce do pagamento, não daqui:
2577
+ salvar agora criaria um segundo pedido quando o cliente pagar. */
2578
+ /* @__PURE__ */ jsxs(Fragment, { children: [
2579
+ /* @__PURE__ */ jsx(Button, { onClick: () => setChargeOpen(true), "data-testid": "new-order-charge", children: "Ver cobran\xE7a" }),
2580
+ /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: "A cobran\xE7a est\xE1 aberta \u2014 o pedido entra sozinho quando ela for paga." })
2581
+ ] })
2582
+ ) : /* @__PURE__ */ jsxs(Fragment, { children: [
1903
2583
  canCharge && /* @__PURE__ */ jsx(
1904
2584
  Button,
1905
2585
  {
1906
- onClick: () => session ? setChargeOpen(true) : void finish("charge"),
2586
+ onClick: () => canLink ? setCharging(true) : void finish("charge"),
1907
2587
  disabled: saving,
1908
2588
  "data-testid": "new-order-charge",
1909
- children: saving ? "Abrindo\u2026" : session ? "Ver cobran\xE7a" : "Gerar cobran\xE7a Pix"
1910
- }
1911
- ),
1912
- canLink && !session && /* @__PURE__ */ jsx(
1913
- Button,
1914
- {
1915
- variant: "outline",
1916
- onClick: () => void finish("link"),
1917
- disabled: saving,
1918
- "data-testid": "new-order-link",
1919
- children: saving ? "Gerando\u2026" : "Gerar link de pagamento"
2589
+ children: saving ? "Abrindo\u2026" : canLink ? "Cobrar" : "Cobrar por Pix"
1920
2590
  }
1921
2591
  ),
1922
2592
  /* @__PURE__ */ jsx(
1923
2593
  Button,
1924
2594
  {
1925
- variant: canCharge ? "outline" : "default",
2595
+ variant: canCharge ? "ghost" : "default",
1926
2596
  onClick: () => setSettling(true),
1927
2597
  "data-testid": "new-order-received",
1928
2598
  children: "J\xE1 recebi"
1929
2599
  }
1930
- ),
1931
- /* @__PURE__ */ jsx(
1932
- Button,
1933
- {
1934
- variant: "ghost",
1935
- onClick: () => void finish("later"),
1936
- disabled: saving,
1937
- "data-testid": "new-order-later",
1938
- children: "Cobrar depois"
1939
- }
1940
2600
  )
1941
2601
  ] }) })
1942
2602
  ] })
1943
2603
  ]
1944
2604
  }
1945
2605
  ),
2606
+ blockedLine && /* @__PURE__ */ jsx(
2607
+ StockEntryModal,
2608
+ {
2609
+ open: entryOpen,
2610
+ provider,
2611
+ variantId: blockedLine.variantId,
2612
+ productId: blockedLine.productId,
2613
+ productName: blockedLine.name,
2614
+ sku: blockedLine.sku,
2615
+ available: blockedLine.stock,
2616
+ needed: blockedLine.quantity,
2617
+ onClose: () => setEntryOpen(false),
2618
+ onReceived: (now) => {
2619
+ setLines((current) => current.map((line) => line.key === blockedLine.key ? { ...line, stock: now } : line));
2620
+ setEntryOpen(false);
2621
+ setError(null);
2622
+ if (lastEnding) void finish(lastEnding);
2623
+ }
2624
+ }
2625
+ ),
1946
2626
  session && /* @__PURE__ */ jsx(
1947
2627
  NewOrderCharge,
1948
2628
  {
@@ -1951,11 +2631,12 @@ function NewOrderPage({ provider, onDone, onCancel }) {
1951
2631
  sessionId: session.sessionId,
1952
2632
  emv: session.emv,
1953
2633
  qrcodeBase64: session.qrcode,
2634
+ qrcodeUrl: session.qrcodeUrl,
1954
2635
  paymentUrl: session.paymentUrl,
1955
2636
  acquirer,
1956
2637
  total: session.total,
1957
2638
  currency: currency2,
1958
- customerPhone: onlyDigits(customer.phone),
2639
+ customerPhone: onlyDigits2(customer.phone),
1959
2640
  customerName: customer.name,
1960
2641
  onPaid: onDone,
1961
2642
  onClose: () => setChargeOpen(false)
@@ -2152,147 +2833,6 @@ function CustomerPicker({ provider, value, onChange, editing, onEditing }) {
2152
2833
  )
2153
2834
  ] });
2154
2835
  }
2155
- function CustomerModal({ open, initialName, customerId, initial, provider, onClose, onSaved }) {
2156
- const [first, setFirst] = useState("");
2157
- const [last, setLast] = useState("");
2158
- const [email, setEmail] = useState("");
2159
- const [phone, setPhone] = useState("");
2160
- const [document, setDocument] = useState("");
2161
- const [saving, setSaving] = useState(false);
2162
- const [error, setError] = useState(null);
2163
- const [duplicate, setDuplicate] = useState(null);
2164
- useEffect(() => {
2165
- if (!open) return;
2166
- const [head, ...rest] = String(initial?.name ?? initialName ?? "").trim().split(/\s+/u);
2167
- setFirst(head ?? "");
2168
- setLast(rest.join(" "));
2169
- setEmail(initial?.email ?? "");
2170
- setPhone(initial?.phone ?? "");
2171
- setDocument(initial?.document ?? "");
2172
- setError(null);
2173
- setDuplicate(null);
2174
- }, [open, initialName, initial]);
2175
- async function save() {
2176
- if (!first.trim()) {
2177
- setError("O nome \xE9 obrigat\xF3rio.");
2178
- return;
2179
- }
2180
- if (document && !isValidDocument(document)) {
2181
- setError("Esse CPF/CNPJ n\xE3o confere.");
2182
- return;
2183
- }
2184
- setSaving(true);
2185
- setError(null);
2186
- setDuplicate(null);
2187
- try {
2188
- const fields = {
2189
- firstName: first.trim(),
2190
- lastName: last.trim(),
2191
- email: email.trim().toLowerCase() || void 0,
2192
- phone: onlyDigits(phone) || void 0,
2193
- document: documentDigits(document) || void 0
2194
- };
2195
- const saved = customerId ? await provider().updateCustomer(customerId, fields) : await provider().createCustomer(fields);
2196
- onSaved(saved);
2197
- } catch (err) {
2198
- if (isDuplicateEmail(err)) {
2199
- const existing = await provider().listCustomers({ search: email.trim().toLowerCase(), limit: 5 }).catch(() => []);
2200
- const hit = existing.find((c) => (c.email ?? "").toLowerCase() === email.trim().toLowerCase());
2201
- if (hit) {
2202
- setDuplicate(hit);
2203
- return;
2204
- }
2205
- setError("J\xE1 existe um cliente com esse e-mail nesta loja. Procure por ele na busca.");
2206
- return;
2207
- }
2208
- setError(errorMessage(err));
2209
- } finally {
2210
- setSaving(false);
2211
- }
2212
- }
2213
- return /* @__PURE__ */ jsx(Modal, { open, onOpenChange: (next) => {
2214
- if (!next) onClose();
2215
- }, children: /* @__PURE__ */ jsx(ModalPortal, { children: /* @__PURE__ */ jsx(ModalOverlay, { children: /* @__PURE__ */ jsxs(ModalContent, { size: "lg", children: [
2216
- /* @__PURE__ */ jsx(ModalHeader, { className: "mb-4", children: /* @__PURE__ */ jsx(ModalTitle, { children: customerId ? "Cadastro do cliente" : "Novo cliente" }) }),
2217
- /* @__PURE__ */ jsxs(ModalBody, { className: "space-y-3", children: [
2218
- /* @__PURE__ */ jsxs("div", { className: "fz-fields fz-fields-2 gap-3", children: [
2219
- /* @__PURE__ */ jsx(Field, { label: "Nome", children: /* @__PURE__ */ jsx(
2220
- "input",
2221
- {
2222
- value: first,
2223
- onChange: (e) => setFirst(e.target.value),
2224
- autoFocus: true,
2225
- "data-testid": "new-customer-first",
2226
- className: fieldClass
2227
- }
2228
- ) }),
2229
- /* @__PURE__ */ jsx(Field, { label: "Sobrenome", children: /* @__PURE__ */ jsx(
2230
- "input",
2231
- {
2232
- value: last,
2233
- onChange: (e) => setLast(e.target.value),
2234
- "data-testid": "new-customer-last",
2235
- className: fieldClass
2236
- }
2237
- ) })
2238
- ] }),
2239
- /* @__PURE__ */ jsx(Field, { label: "Telefone", children: /* @__PURE__ */ jsx("div", { className: "mt-1", children: /* @__PURE__ */ jsx(PhoneField, { value: phone, onChange: setPhone, "data-testid": "new-customer-phone" }) }) }),
2240
- /* @__PURE__ */ jsx(Field, { label: "E-mail", children: /* @__PURE__ */ jsx(
2241
- "input",
2242
- {
2243
- type: "email",
2244
- value: email,
2245
- onChange: (e) => setEmail(e.target.value),
2246
- "data-testid": "new-customer-email",
2247
- className: fieldClass
2248
- }
2249
- ) }),
2250
- /* @__PURE__ */ jsx(Field, { label: "CPF / CNPJ", hint: "Necess\xE1rio para cobran\xE7a online e para a nota fiscal", children: /* @__PURE__ */ jsx(
2251
- "input",
2252
- {
2253
- value: formatDocument(document),
2254
- onChange: (e) => setDocument(documentDigits(e.target.value)),
2255
- placeholder: "000.000.000-00",
2256
- "data-testid": "new-customer-document",
2257
- className: fieldClass
2258
- }
2259
- ) }),
2260
- duplicate && /* @__PURE__ */ jsxs(
2261
- "div",
2262
- {
2263
- role: "alert",
2264
- "data-testid": "new-customer-duplicate",
2265
- className: "rounded-md border border-amber-200 bg-amber-50 px-3 py-2 text-sm text-amber-900",
2266
- children: [
2267
- /* @__PURE__ */ jsxs("p", { children: [
2268
- /* @__PURE__ */ jsxs("strong", { children: [
2269
- duplicate.firstName,
2270
- " ",
2271
- duplicate.lastName
2272
- ] }),
2273
- " j\xE1 \xE9 cliente com esse e-mail."
2274
- ] }),
2275
- /* @__PURE__ */ jsx(
2276
- "button",
2277
- {
2278
- type: "button",
2279
- onClick: () => onSaved(duplicate),
2280
- "data-testid": "new-customer-use-existing",
2281
- className: "mt-1 font-medium underline underline-offset-2",
2282
- children: "Usar este cliente"
2283
- }
2284
- )
2285
- ]
2286
- }
2287
- ),
2288
- error && /* @__PURE__ */ jsx("p", { role: "alert", className: "rounded-md border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-800", children: error })
2289
- ] }),
2290
- /* @__PURE__ */ jsxs(ModalFooter, { className: "mt-4 gap-2", children: [
2291
- /* @__PURE__ */ jsx(Button, { variant: "outline", onClick: onClose, children: "Cancelar" }),
2292
- /* @__PURE__ */ jsx(Button, { onClick: save, disabled: saving, "data-testid": "new-customer-save", children: saving ? "Salvando\u2026" : customerId ? "Salvar" : "Criar cliente" })
2293
- ] })
2294
- ] }) }) }) });
2295
- }
2296
2836
  function DeliveryAddress({ provider, customerId, address, onChange }) {
2297
2837
  const [saved, setSaved] = useState([]);
2298
2838
  const [chosen, setChosen] = useState(null);
@@ -2463,14 +3003,14 @@ function PostalField({ value, onChange, onResolved }) {
2463
3003
  const [looking, setLooking] = useState(false);
2464
3004
  const [missing, setMissing] = useState(false);
2465
3005
  useEffect(() => {
2466
- const digits = onlyDigits(value);
2467
- if (digits.length !== 8) {
3006
+ const digits2 = onlyDigits2(value);
3007
+ if (digits2.length !== 8) {
2468
3008
  setMissing(false);
2469
3009
  return;
2470
3010
  }
2471
3011
  let cancelled = false;
2472
3012
  setLooking(true);
2473
- fetch(`https://viacep.com.br/ws/${digits}/json/`).then((response) => response.json()).then((found) => {
3013
+ fetch(`https://viacep.com.br/ws/${digits2}/json/`).then((response) => response.json()).then((found) => {
2474
3014
  if (cancelled) return;
2475
3015
  if (found?.erro) {
2476
3016
  setMissing(true);
@@ -2503,6 +3043,496 @@ function PostalField({ value, onChange, onResolved }) {
2503
3043
  }
2504
3044
  ) });
2505
3045
  }
3046
+
3047
+ // src/views/orders-agent.ts
3048
+ var ORDER_TAKING_INSTRUCTIONS = `
3049
+ Esta tela \xE9 a lista de Pedidos da loja. O lojista recebe pedido por WhatsApp e cola a mensagem aqui.
3050
+
3051
+ Como tirar um pedido:
3052
+ 1. Leia a mensagem e separe tr\xEAs coisas: os itens com quantidade, quem \xE9 o cliente, e os recados de entrega. Item sem n\xFAmero \xE9 quantidade 1.
3053
+ 2. Chame \`findShopProducts\` UMA vez com todos os termos de item juntos. Nunca invente produto, pre\xE7o ou SKU.
3054
+ \xB7 Ela DECIDE por voc\xEA. Termo que volta com \`product\` est\xE1 resolvido: use e siga, sem confirmar, sem listar alternativa. Um vencedor folgado n\xE3o \xE9 uma d\xFAvida.
3055
+ \xB7 Termo que volta com \`needsChoice\` j\xE1 virou uma pergunta com bot\xF5es na tela do lojista. N\xC3O repita as op\xE7\xF5es no texto, n\xE3o escreva SKU, pre\xE7o nem estoque em lista: no m\xE1ximo uma linha curta, e espere ele tocar. A resposta dele volta como "Nome (id)" \u2014 o id entre par\xEAnteses \xE9 o produto escolhido.
3056
+ \xB7 Termo que volta com \`notFound\` \xE9 o \xFAnico que se pergunta por texto: diga que a loja n\xE3o tem esse item e pergunte o que usar no lugar.
3057
+ \xB7 \`outOfStock\` \xE9 aviso, n\xE3o bloqueio: diga numa frase antes de montar o pedido.
3058
+ \xB7 Produto com varia\xE7\xF5es: use a que a mensagem indicar; se ela n\xE3o indicar, pergunte.
3059
+ 3. Cliente: \`findShopCustomers\` com o nome (ou o telefone). Um candidato claro, use o \`customerId\`. V\xE1rios, pergunte. Nenhum, deixe o pedido criar o cadastro com o nome que veio \u2014 n\xE3o pe\xE7a e-mail nem CPF.
3060
+ 4. Endere\xE7o \xE9 OPCIONAL e nunca \xE9 adivinhado. S\xF3 preencha \`shippingAddress\` se a mensagem trouxer CEP ou rua/n\xFAmero/cidade. Ponto de refer\xEAncia ("Bruna recantinho"), dia ou hora ("entregar sexta") v\xE3o em \`notes\`, do jeito que o cliente escreveu.
3061
+ 5. Chame \`createShopOrder\`. O lojista confirma e corrige no cart\xE3o antes de gravar \u2014 n\xE3o pe\xE7a confirma\xE7\xE3o por texto antes, monte e chame.
3062
+ 6. Criado o pedido, mostre o link dele e ofere\xE7a em uma linha o que vem depois: gerar cobran\xE7a (Pix ou link), adicionar endere\xE7o, ou tirar o pr\xF3ximo.
3063
+
3064
+ Regras:
3065
+ \xB7 Voc\xEA n\xE3o \xE9 um cat\xE1logo. Nunca imprima lista numerada de produto com SKU, pre\xE7o e estoque \u2014 isso \xE9 despejo de banco de dados, e quem l\xEA est\xE1 no balc\xE3o com o cliente esperando.
3066
+ \xB7 Pre\xE7o, frete e total s\xE3o do servidor. N\xE3o cite total antes de o pedido existir.
3067
+ \xB7 V\xE1rias mensagens coladas de uma vez s\xE3o v\xE1rios pedidos: fa\xE7a um de cada vez.
3068
+ \xB7 Portugu\xEAs do Brasil, curto, como quem est\xE1 no balc\xE3o.
3069
+ `.trim();
3070
+ var FIND_PRODUCTS = {
3071
+ id: "shop.orders.find-products",
3072
+ name: "findShopProducts",
3073
+ description: 'Resolves the words a customer wrote ("3 costela su\xEDna", "arroz de costela") into catalog products. Send EVERY item term of the message in one call \u2014 one call resolves the whole order. Answers candidates per term with id, price and stock; it never picks for you and never invents a product. When a term has no candidate, or two candidates are equally close, ask the merchant which one it is.',
3074
+ icon: "Search",
3075
+ mode: "read",
3076
+ execution: { plane: "client" },
3077
+ category: "Shop",
3078
+ role: "operations",
3079
+ parameters: {
3080
+ type: "object",
3081
+ properties: {
3082
+ terms: {
3083
+ type: "array",
3084
+ description: "The item words as the customer wrote them, without the quantity.",
3085
+ items: { type: "string" }
3086
+ }
3087
+ },
3088
+ required: ["terms"]
3089
+ }
3090
+ };
3091
+ var FIND_CUSTOMERS = {
3092
+ id: "shop.orders.find-customers",
3093
+ name: "findShopCustomers",
3094
+ description: "Looks a customer up by name, phone or e-mail. Use the id it answers when one candidate is clearly the person; ask the merchant when there are several; place the order without an id when there is none \u2014 it creates the record.",
3095
+ icon: "Users",
3096
+ mode: "read",
3097
+ execution: { plane: "client" },
3098
+ category: "Shop",
3099
+ role: "operations",
3100
+ parameters: {
3101
+ type: "object",
3102
+ properties: { query: { type: "string", description: "Name, phone or e-mail." } },
3103
+ required: ["query"]
3104
+ }
3105
+ };
3106
+ var CREATE_ORDER = {
3107
+ id: "shop.orders.create",
3108
+ name: "createShopOrder",
3109
+ description: 'Places the order the message describes. The merchant reviews and corrects it on the confirmation card before anything is written \u2014 build it and call, do not ask for confirmation in text first. Items carry ids and quantities only: the server re-reads every price, so never send money. Leave `shippingAddress` out unless the message gave a real address; delivery notes ("entregar sexta") belong in `notes`.',
3110
+ icon: "ShoppingCart",
3111
+ mode: "persist",
3112
+ execution: { plane: "client" },
3113
+ category: "Shop",
3114
+ role: "operations",
3115
+ permission: { feature: "shop", action: "create" },
3116
+ parameters: {
3117
+ type: "object",
3118
+ properties: {
3119
+ customerName: { type: "string", description: "Who is buying, as the message names them." },
3120
+ customerId: { type: "string", description: "Id from findShopCustomers, when the person already exists." },
3121
+ customerPhone: { type: "string", description: "Only if the message carries it." },
3122
+ customerEmail: { type: "string", description: "Only if the message carries it." },
3123
+ items: {
3124
+ type: "array",
3125
+ description: "One entry per line of the order.",
3126
+ items: {
3127
+ type: "object",
3128
+ properties: {
3129
+ productId: { type: "string", description: "From findShopProducts." },
3130
+ variantId: { type: "string", description: "Required when the product has variants." },
3131
+ name: { type: "string", description: "Product name, so the merchant reads the card without ids." },
3132
+ quantity: { type: "number", description: "How many. Absent in the message means 1." }
3133
+ },
3134
+ required: ["productId", "name", "quantity"]
3135
+ }
3136
+ },
3137
+ notes: {
3138
+ type: "string",
3139
+ description: "Delivery day, reference point, anything the customer said that is not an item or an address."
3140
+ },
3141
+ shippingAddress: {
3142
+ type: "object",
3143
+ description: "Only when the message gave one. Keys: postalCode, street, number, complement, district, city, state.",
3144
+ properties: {
3145
+ postalCode: { type: "string" },
3146
+ street: { type: "string" },
3147
+ number: { type: "string" },
3148
+ complement: { type: "string" },
3149
+ district: { type: "string" },
3150
+ city: { type: "string" },
3151
+ state: { type: "string", description: "Two letters (SP, RJ)." }
3152
+ }
3153
+ }
3154
+ },
3155
+ required: ["customerName", "items"]
3156
+ }
3157
+ };
3158
+ var CHARGE_ORDER = {
3159
+ id: "shop.orders.charge",
3160
+ name: "chargeShopOrder",
3161
+ description: "Opens the charge for an order that already exists \u2014 a Pix code to read out, or a payment link to send on WhatsApp. Safe to repeat: an order that already has an open charge answers with the same one instead of a second.",
3162
+ icon: "CreditCard",
3163
+ mode: "persist",
3164
+ execution: { plane: "client" },
3165
+ category: "Shop",
3166
+ role: "operations",
3167
+ parameters: {
3168
+ type: "object",
3169
+ properties: {
3170
+ orderId: { type: "string", description: "The order to charge." },
3171
+ asLink: { type: "boolean", description: "True for a link to send, false for a Pix code to read out." }
3172
+ },
3173
+ required: ["orderId"]
3174
+ }
3175
+ };
3176
+ var SET_ADDRESS = {
3177
+ id: "shop.orders.set-address",
3178
+ name: "setShopOrderAddress",
3179
+ description: "Writes the delivery address onto an order that has none. Rewrites the order through the same trusted path that created it, so it only works while nobody has paid it.",
3180
+ icon: "MapPin",
3181
+ mode: "persist",
3182
+ execution: { plane: "client" },
3183
+ category: "Shop",
3184
+ role: "operations",
3185
+ parameters: {
3186
+ type: "object",
3187
+ properties: {
3188
+ orderId: { type: "string" },
3189
+ postalCode: { type: "string", description: "CEP." },
3190
+ street: { type: "string" },
3191
+ number: { type: "string" },
3192
+ complement: { type: "string" },
3193
+ district: { type: "string" },
3194
+ city: { type: "string" },
3195
+ state: { type: "string", description: "Two letters (SP, RJ)." },
3196
+ recipient: { type: "string", description: "Who receives, when it is not the buyer." }
3197
+ },
3198
+ required: ["orderId", "street", "city", "state"]
3199
+ }
3200
+ };
3201
+ var powersOf = (shop) => shop;
3202
+ var digits = (value) => String(value ?? "").replace(/\D/gu, "");
3203
+ var text = (value) => typeof value === "string" ? value.trim() : "";
3204
+ var orderRef = (order) => ({
3205
+ id: order.id,
3206
+ label: `Pedido #${order.orderNumber}`,
3207
+ entityKey: "shop_order",
3208
+ path: `/orders/${order.id}`
3209
+ });
3210
+ async function resolveCustomerId(shop, args) {
3211
+ if (args.customerId) return args.customerId;
3212
+ const email = (args.customerEmail ?? "").trim().toLowerCase();
3213
+ const phone = digits(args.customerPhone);
3214
+ if (email) {
3215
+ const found = await shop.listCustomers({ search: email, limit: 5 });
3216
+ const hit = found.find((c) => (c.email ?? "").toLowerCase() === email);
3217
+ if (hit) return hit.id;
3218
+ }
3219
+ if (phone.length >= 10) {
3220
+ const found = await shop.listCustomers({ search: phone, limit: 5 });
3221
+ const hit = found.find((c) => digits(c.phone).endsWith(phone.slice(-10)));
3222
+ if (hit) return hit.id;
3223
+ }
3224
+ const [first, ...rest] = args.customerName.trim().split(/\s+/u);
3225
+ const created = await shop.createCustomer({
3226
+ firstName: first ?? args.customerName.trim(),
3227
+ lastName: rest.join(" "),
3228
+ email: email || void 0,
3229
+ phone: phone || void 0
3230
+ });
3231
+ return created.id;
3232
+ }
3233
+ function readAddress(raw) {
3234
+ if (!raw || typeof raw !== "object") return void 0;
3235
+ const a = raw;
3236
+ const street = text(a.street);
3237
+ const city = text(a.city);
3238
+ const state = text(a.state);
3239
+ if (!street || !city || !state) return void 0;
3240
+ return {
3241
+ postalCode: digits(a.postalCode),
3242
+ street,
3243
+ city,
3244
+ state: state.toUpperCase().slice(0, 2),
3245
+ ...text(a.number) ? { number: text(a.number) } : {},
3246
+ ...text(a.complement) ? { complement: text(a.complement) } : {},
3247
+ ...text(a.district) ? { district: text(a.district) } : {},
3248
+ ...text(a.recipient) ? { recipient: text(a.recipient) } : {}
3249
+ };
3250
+ }
3251
+ function createOrderTakingTools({ getProvider, onChanged }) {
3252
+ const failed = (error) => ({ ok: false, error: errorMessage(error) });
3253
+ return [
3254
+ {
3255
+ tool: FIND_PRODUCTS,
3256
+ execute: async (args) => {
3257
+ const terms = (Array.isArray(args.terms) ? args.terms : []).map(text).filter(Boolean).slice(0, 20);
3258
+ if (terms.length === 0) return { ok: false, error: "no_terms" };
3259
+ try {
3260
+ const catalog = await listProductsCached(getProvider, { status: "active", limit: 500 });
3261
+ const variantsOf = /* @__PURE__ */ new Map();
3262
+ const matches = [];
3263
+ const choices = [];
3264
+ for (const [index, term] of terms.entries()) {
3265
+ const answer = resolveTerm(term, catalog);
3266
+ if (!answer.resolved) {
3267
+ if (answer.options?.length) {
3268
+ choices.push({
3269
+ id: `product-${index}`,
3270
+ question: `Qual "${term}"?`,
3271
+ options: answer.options.map((candidate) => ({
3272
+ id: candidate.productId,
3273
+ label: candidate.name,
3274
+ meta: candidateMeta(candidate)
3275
+ }))
3276
+ });
3277
+ matches.push({ term, needsChoice: true });
3278
+ } else {
3279
+ matches.push({ term, notFound: true });
3280
+ }
3281
+ continue;
3282
+ }
3283
+ const product = answer.resolved;
3284
+ if (!variantsOf.has(product.productId)) {
3285
+ let list = [];
3286
+ try {
3287
+ list = (await getProvider().getProductVariants?.(product.productId))?.variants ?? [];
3288
+ } catch {
3289
+ }
3290
+ variantsOf.set(product.productId, list);
3291
+ }
3292
+ const variants = variantsOf.get(product.productId) ?? [];
3293
+ if (variants.length > 0) {
3294
+ product.variants = variants.map((v) => ({
3295
+ variantId: v.id,
3296
+ options: v.options,
3297
+ price: v.price ?? product.price,
3298
+ stock: v.inventoryCount ?? 0
3299
+ }));
3300
+ }
3301
+ matches.push({
3302
+ term,
3303
+ product,
3304
+ // O servidor recusa o pedido por falta de estoque; dizer antes é a
3305
+ // diferença entre avisar e ser recusado na frente do cliente.
3306
+ ...product.stock <= 0 ? { outOfStock: true } : {}
3307
+ });
3308
+ }
3309
+ return {
3310
+ ok: true,
3311
+ // O marcador que o painel usa para desenhar as perguntas — só
3312
+ // aparece quando existe dúvida de verdade.
3313
+ ...choices.length > 0 ? { kind: "record_choice", choices } : {},
3314
+ matches,
3315
+ catalogSize: catalog.length
3316
+ };
3317
+ } catch (error) {
3318
+ return failed(error);
3319
+ }
3320
+ }
3321
+ },
3322
+ {
3323
+ tool: FIND_CUSTOMERS,
3324
+ execute: async (args) => {
3325
+ const query = text(args.query);
3326
+ if (!query) return { ok: false, error: "no_query" };
3327
+ try {
3328
+ const rows = await getProvider().listCustomers({ search: query, limit: 5 });
3329
+ return {
3330
+ ok: true,
3331
+ customers: rows.map((c) => {
3332
+ const name = [c.firstName, c.lastName].filter(Boolean).join(" ").trim();
3333
+ return {
3334
+ customerId: c.id,
3335
+ name,
3336
+ phone: c.phone,
3337
+ email: c.email,
3338
+ ordersCount: c.ordersCount,
3339
+ ref: { id: c.id, label: name || c.email || "Cliente", entityKey: "shop_customer" }
3340
+ };
3341
+ })
3342
+ };
3343
+ } catch (error) {
3344
+ return failed(error);
3345
+ }
3346
+ }
3347
+ },
3348
+ {
3349
+ tool: CREATE_ORDER,
3350
+ execute: async (args) => {
3351
+ const customerName = text(args.customerName);
3352
+ const rawItems = Array.isArray(args.items) ? args.items : [];
3353
+ const items = rawItems.map((raw) => {
3354
+ const item = raw ?? {};
3355
+ const quantity = Math.max(1, Math.round(Number(item.quantity) || 1));
3356
+ return {
3357
+ productId: text(item.productId),
3358
+ variantId: text(item.variantId) || void 0,
3359
+ quantity
3360
+ };
3361
+ }).filter((item) => item.productId);
3362
+ if (items.length === 0) return { ok: false, error: "no_items", message: "Nenhum item com produto do cat\xE1logo." };
3363
+ if (!customerName) return { ok: false, error: "no_customer", message: "Falta o nome de quem est\xE1 comprando." };
3364
+ const shop = getProvider();
3365
+ try {
3366
+ const customerId = await resolveCustomerId(shop, {
3367
+ customerId: text(args.customerId) || void 0,
3368
+ customerName,
3369
+ customerPhone: text(args.customerPhone),
3370
+ customerEmail: text(args.customerEmail)
3371
+ });
3372
+ const payload = {
3373
+ customerId,
3374
+ customer: { name: customerName, email: text(args.customerEmail).toLowerCase() || void 0 },
3375
+ items,
3376
+ ...readAddress(args.shippingAddress) ? { shippingAddress: readAddress(args.shippingAddress) } : {},
3377
+ ...text(args.notes) ? { notes: text(args.notes) } : {}
3378
+ };
3379
+ const order = await shop.placeOrder(payload);
3380
+ onChanged?.();
3381
+ return {
3382
+ ok: true,
3383
+ orderId: order.id,
3384
+ orderNumber: order.orderNumber,
3385
+ total: order.total,
3386
+ currency: order.currency,
3387
+ hasAddress: Boolean(order.shippingAddress),
3388
+ ref: orderRef(order),
3389
+ nextSteps: [
3390
+ "chargeShopOrder \u2014 Pix ou link para mandar no WhatsApp",
3391
+ ...order.shippingAddress ? [] : ["setShopOrderAddress \u2014 endere\xE7o de entrega"]
3392
+ ]
3393
+ };
3394
+ } catch (error) {
3395
+ return failed(error);
3396
+ }
3397
+ }
3398
+ },
3399
+ {
3400
+ tool: CHARGE_ORDER,
3401
+ execute: async (args) => {
3402
+ const orderId = text(args.orderId);
3403
+ if (!orderId) return { ok: false, error: "no_order" };
3404
+ const shop = powersOf(getProvider());
3405
+ if (typeof shop.chargeExistingOrder !== "function") {
3406
+ return { ok: false, error: "charge_unavailable", message: "Esta loja ainda n\xE3o tem um meio de cobran\xE7a conectado." };
3407
+ }
3408
+ try {
3409
+ const open = await shop.findOpenOrderCharge?.(orderId);
3410
+ const session = open ?? await shop.chargeExistingOrder(orderId, { asLink: args.asLink === true });
3411
+ onChanged?.();
3412
+ return {
3413
+ ok: true,
3414
+ reused: Boolean(open),
3415
+ total: session.total,
3416
+ paymentUrl: session.paymentUrl ?? null,
3417
+ pixCode: session.charge?.emv ?? null,
3418
+ expiresAt: session.charge?.expiresAt ?? null
3419
+ };
3420
+ } catch (error) {
3421
+ return failed(error);
3422
+ }
3423
+ }
3424
+ },
3425
+ {
3426
+ tool: SET_ADDRESS,
3427
+ execute: async (args) => {
3428
+ const orderId = text(args.orderId);
3429
+ const address = readAddress(args);
3430
+ if (!orderId) return { ok: false, error: "no_order" };
3431
+ if (!address) return { ok: false, error: "incomplete_address", message: "Endere\xE7o precisa de rua, cidade e estado." };
3432
+ const shop = getProvider();
3433
+ const revise = powersOf(shop).reviseOrder;
3434
+ if (typeof revise !== "function") {
3435
+ return { ok: false, error: "revise_unavailable", message: "Esta loja n\xE3o edita pedido por aqui." };
3436
+ }
3437
+ try {
3438
+ const order = await shop.getOrder(orderId);
3439
+ if (!order) return { ok: false, error: "not_found", message: "Pedido n\xE3o encontrado." };
3440
+ if (order.financialStatus === "paid" || order.status === "cancelled") {
3441
+ return {
3442
+ ok: false,
3443
+ error: "not_editable",
3444
+ message: "Pedido pago ou cancelado n\xE3o \xE9 reescrito \u2014 o endere\xE7o muda no envio."
3445
+ };
3446
+ }
3447
+ const revised = await revise(orderId, {
3448
+ ...order.customerId ? { customerId: order.customerId } : {},
3449
+ ...order.customerName ? { customer: { name: order.customerName, email: order.customerEmail ?? void 0 } } : {},
3450
+ items: order.items.filter((line) => line.productId).map((line) => ({
3451
+ productId: line.productId,
3452
+ // A variação só existe no pedido depois de 0029; um pedido mais
3453
+ // velho não tem o campo, e reescrever sem ele é o servidor
3454
+ // adivinhando qual tamanho foi vendido.
3455
+ variantId: line.variantId ?? void 0,
3456
+ quantity: line.quantity
3457
+ })),
3458
+ shippingAddress: address,
3459
+ ...order.notes ? { notes: order.notes } : {},
3460
+ ...order.discountCode ? { discountCode: order.discountCode } : {}
3461
+ });
3462
+ onChanged?.();
3463
+ return { ok: true, orderId: revised.id, orderNumber: revised.orderNumber, ref: orderRef(revised) };
3464
+ } catch (error) {
3465
+ return failed(error);
3466
+ }
3467
+ }
3468
+ }
3469
+ ];
3470
+ }
3471
+
3472
+ // src/views/OrdersAgentSurface.tsx
3473
+ var ORDER_TAKING_INTRO = {
3474
+ role: "operations",
3475
+ persona: "Balconista",
3476
+ pitch: "Cole a mensagem do cliente que eu monto o pedido.",
3477
+ description: "Leio os itens e as quantidades da mensagem, acho cada um no cat\xE1logo, resolvo o cliente e monto o pedido para voc\xEA conferir e confirmar. Depois gero a cobran\xE7a ou anoto o endere\xE7o.",
3478
+ suggestions: [
3479
+ { label: "O que voc\xEA precisa para montar um pedido?" },
3480
+ { label: "Quais pedidos est\xE3o esperando pagamento?" }
3481
+ ]
3482
+ };
3483
+ function OrdersAgentSurface({
3484
+ provider,
3485
+ orders,
3486
+ onChanged
3487
+ }) {
3488
+ const live = React2.useRef({ provider, orders, onChanged });
3489
+ live.current = { provider, orders, onChanged };
3490
+ const contribution = React2.useMemo(() => ({
3491
+ id: "shop.orders.list",
3492
+ pluginId: "shop",
3493
+ role: "operations",
3494
+ instructions: ORDER_TAKING_INSTRUCTIONS,
3495
+ describe: () => {
3496
+ const { orders: rows, provider: get } = live.current;
3497
+ let canCharge = false;
3498
+ let canRevise = false;
3499
+ try {
3500
+ const shop = get();
3501
+ canCharge = typeof shop.chargeExistingOrder === "function";
3502
+ canRevise = typeof shop.reviseOrder === "function";
3503
+ } catch {
3504
+ }
3505
+ return {
3506
+ title: "Lista \u2014 Pedidos",
3507
+ state: {
3508
+ screen: "/orders",
3509
+ canCharge,
3510
+ canRevise,
3511
+ ordersOnScreen: rows.length,
3512
+ // Os últimos, para "cobra o pedido da Fran" achar de qual pedido se
3513
+ // fala sem uma busca a mais.
3514
+ recentOrders: rows.slice(0, 8).map((o) => ({
3515
+ orderNumber: o.orderNumber,
3516
+ customer: o.customerName ?? o.customerEmail ?? null,
3517
+ total: o.total,
3518
+ currency: o.currency,
3519
+ financialStatus: o.financialStatus,
3520
+ fulfillmentStatus: o.fulfillmentStatus,
3521
+ hasAddress: Boolean(o.shippingAddress),
3522
+ createdAt: o.createdAt,
3523
+ ref: { id: o.id, label: `Pedido #${o.orderNumber}`, path: `/orders/${o.id}` }
3524
+ }))
3525
+ }
3526
+ };
3527
+ },
3528
+ tools: createOrderTakingTools({
3529
+ getProvider: () => live.current.provider(),
3530
+ onChanged: () => live.current.onChanged()
3531
+ })
3532
+ }), []);
3533
+ useAgentSurface(contribution);
3534
+ return null;
3535
+ }
2506
3536
  function currency(value, code = "BRL", locale = "pt-BR") {
2507
3537
  return new Intl.NumberFormat(locale, { style: "currency", currency: code }).format(value);
2508
3538
  }
@@ -2779,10 +3809,49 @@ function OrdersTab({ provider }) {
2779
3809
  }, [orders]);
2780
3810
  if (error) return /* @__PURE__ */ jsx(ErrorState, { message: error });
2781
3811
  return /* @__PURE__ */ jsxs(Fragment, { children: [
3812
+ /* @__PURE__ */ jsx(
3813
+ OrdersAgentSurface,
3814
+ {
3815
+ provider: () => resolveShopProvider(provider),
3816
+ orders,
3817
+ onChanged: reload
3818
+ }
3819
+ ),
2782
3820
  /* @__PURE__ */ jsx(
2783
3821
  OrderListView,
2784
3822
  {
2785
- actions: /* @__PURE__ */ jsx(Button, { onClick: () => adminNavigateTo("/orders/new"), "data-testid": "orders-create", children: "+ Criar pedido" }),
3823
+ actions: /* @__PURE__ */ jsx(
3824
+ PageActions,
3825
+ {
3826
+ actions: [{
3827
+ id: "new-order",
3828
+ label: "Criar pedido",
3829
+ icon: Plus,
3830
+ intent: "primary",
3831
+ creates: true,
3832
+ testId: "orders-create",
3833
+ onSelect: () => adminNavigateTo("/orders/new")
3834
+ }, {
3835
+ // O pedido chega escrito — a outra porta para a mesma coisa, para
3836
+ // quem tem a mensagem do cliente na mão em vez de um formulário.
3837
+ //
3838
+ // `creates` porque ela CRIA um pedido, e é isso que decide a cara
3839
+ // do círculo no celular: o conjunto todo inserindo é um "+", e sem
3840
+ // esta linha a varinha rebaixava a tela inteira para o "⋮" neutro
3841
+ // — a lista de pedidos deixava de parecer uma tela em que se põe.
3842
+ id: "new-order-ai",
3843
+ label: "Criar com IA",
3844
+ description: "Cole a mensagem do cliente e eu monto o pedido.",
3845
+ icon: WandSparkles,
3846
+ creates: true,
3847
+ testId: "orders-create-ai",
3848
+ onSelect: () => askAI({
3849
+ hint: "Cole a mensagem do cliente\u2026",
3850
+ intro: ORDER_TAKING_INTRO
3851
+ })
3852
+ }]
3853
+ }
3854
+ ),
2786
3855
  stats,
2787
3856
  columns,
2788
3857
  rows: orders,
@@ -2912,13 +3981,15 @@ var ShopPage = ({ provider, section }) => {
2912
3981
  const active = section ?? tab;
2913
3982
  const path = useAdminPath();
2914
3983
  const orderId = section === "orders" ? path.match(/^\/orders\/([^/]+)$/)?.[1] : void 0;
2915
- if (section === "orders" && orderId === "new") {
3984
+ const editingId = section === "orders" ? path.match(/^\/orders\/([^/]+)\/edit$/)?.[1] : void 0;
3985
+ if (section === "orders" && (orderId === "new" || editingId)) {
2916
3986
  return /* @__PURE__ */ jsx(
2917
3987
  NewOrderPage,
2918
3988
  {
2919
3989
  provider: () => resolveShopProvider(provider),
3990
+ orderId: editingId,
2920
3991
  onDone: (id) => adminNavigateTo(id ? `/orders/${id}` : "/orders"),
2921
- onCancel: () => adminNavigateTo("/orders")
3992
+ onCancel: () => adminNavigateTo(editingId ? `/orders/${editingId}` : "/orders")
2922
3993
  }
2923
3994
  );
2924
3995
  }
@@ -2961,5 +4032,5 @@ var ShopPage = ({ provider, section }) => {
2961
4032
  ShopPage.displayName = "ShopPage";
2962
4033
 
2963
4034
  export { ShopPage };
2964
- //# sourceMappingURL=ShopPage-VVLOH3RF.js.map
2965
- //# sourceMappingURL=ShopPage-VVLOH3RF.js.map
4035
+ //# sourceMappingURL=ShopPage-QFIM2HDU.js.map
4036
+ //# sourceMappingURL=ShopPage-QFIM2HDU.js.map