@fayz-ai/plugin-shop 0.11.2 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{ShopPage-YB5ZWECJ.js → ShopPage-QFIM2HDU.js} +1516 -406
- package/dist/ShopPage-QFIM2HDU.js.map +1 -0
- package/dist/{chunk-HAKXHHAG.js → chunk-2VMTPRGC.js} +202 -11
- package/dist/chunk-2VMTPRGC.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1814 -442
- package/dist/index.js.map +1 -1
- package/dist/integrations/abacatepay/connectorDef.d.ts.map +1 -1
- package/dist/integrations/abacatepay/data.d.ts +40 -0
- package/dist/integrations/abacatepay/data.d.ts.map +1 -0
- package/dist/integrations/abacatepay/limits.d.ts +26 -0
- package/dist/integrations/abacatepay/limits.d.ts.map +1 -0
- package/dist/integrations/abacatepay/sync.d.ts +88 -0
- package/dist/integrations/abacatepay/sync.d.ts.map +1 -0
- package/dist/integrations/bling/data.d.ts.map +1 -1
- package/dist/integrations/mercadopago/connectorDef.d.ts +41 -0
- package/dist/integrations/mercadopago/connectorDef.d.ts.map +1 -0
- package/dist/integrations/neochef/connectorDef.d.ts +12 -0
- package/dist/integrations/neochef/connectorDef.d.ts.map +1 -0
- package/dist/integrations/neochef/data.d.ts +54 -0
- package/dist/integrations/neochef/data.d.ts.map +1 -0
- package/dist/integrations/neochef/index.d.ts +8 -0
- package/dist/integrations/neochef/index.d.ts.map +1 -0
- package/dist/integrations/neochef/limits.d.ts +26 -0
- package/dist/integrations/neochef/limits.d.ts.map +1 -0
- package/dist/integrations/neochef/mapping.d.ts +31 -0
- package/dist/integrations/neochef/mapping.d.ts.map +1 -0
- package/dist/integrations/neochef/mark.d.ts +21 -0
- package/dist/integrations/neochef/mark.d.ts.map +1 -0
- package/dist/integrations/neochef/types.d.ts +71 -0
- package/dist/integrations/neochef/types.d.ts.map +1 -0
- package/dist/integrations/vindi/connectorDef.d.ts +67 -35
- package/dist/integrations/vindi/connectorDef.d.ts.map +1 -1
- package/dist/views/ContentPage.d.ts.map +1 -1
- package/dist/views/CustomerModal.d.ts +33 -0
- package/dist/views/CustomerModal.d.ts.map +1 -0
- package/dist/views/NewOrderCharge.d.ts +39 -3
- package/dist/views/NewOrderCharge.d.ts.map +1 -1
- package/dist/views/NewOrderPage.d.ts +10 -1
- package/dist/views/NewOrderPage.d.ts.map +1 -1
- package/dist/views/OrderDetailView.d.ts.map +1 -1
- package/dist/views/OrdersAgentSurface.d.ts +10 -0
- package/dist/views/OrdersAgentSurface.d.ts.map +1 -0
- package/dist/views/ProductPositionsTab.d.ts +7 -0
- package/dist/views/ProductPositionsTab.d.ts.map +1 -0
- package/dist/views/ProductStockTab.d.ts +84 -0
- package/dist/views/ProductStockTab.d.ts.map +1 -0
- package/dist/views/ShopPage.d.ts.map +1 -1
- package/dist/views/StockEntryModal.d.ts +32 -0
- package/dist/views/StockEntryModal.d.ts.map +1 -0
- package/dist/views/customerEntity.d.ts.map +1 -1
- package/dist/views/discount-agent.d.ts +2 -0
- package/dist/views/discount-agent.d.ts.map +1 -0
- package/dist/views/new-order-rules.d.ts +23 -1
- package/dist/views/new-order-rules.d.ts.map +1 -1
- package/dist/views/orders-agent.d.ts +11 -0
- package/dist/views/orders-agent.d.ts.map +1 -0
- package/dist/views/product-matching.d.ts +38 -0
- package/dist/views/product-matching.d.ts.map +1 -0
- package/dist/views/productEntity.d.ts.map +1 -1
- package/dist/views/stock-agent.d.ts +82 -0
- package/dist/views/stock-agent.d.ts.map +1 -0
- package/package.json +8 -6
- package/dist/ShopPage-YB5ZWECJ.js.map +0 -1
- package/dist/chunk-HAKXHHAG.js.map +0 -1
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import { errorMessage, shopCustomerLookup, listProductsDetailed, invalidateProductLists, buildStockLocationsRegistry } from './chunk-
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
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 =
|
|
140
|
-
const chosen =
|
|
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
|
{
|
|
@@ -187,17 +189,24 @@ function validateNewOrder(draft, ending) {
|
|
|
187
189
|
return `Para gerar a cobran\xE7a o adquirente exige ${missing.join(", ")} no cadastro do cliente.`;
|
|
188
190
|
}
|
|
189
191
|
}
|
|
192
|
+
if (ending === "link") {
|
|
193
|
+
const missing = [
|
|
194
|
+
!draft.customerEmail.trim() && "e-mail",
|
|
195
|
+
!draft.customerPhone.trim() && "telefone"
|
|
196
|
+
].filter(Boolean);
|
|
197
|
+
if (missing.length) {
|
|
198
|
+
return `Para mandar o link o cadastro do cliente precisa de ${missing.join(" e ")}.`;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
190
201
|
return null;
|
|
191
202
|
}
|
|
192
|
-
function
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
const message = String(error?.message ?? error ?? "");
|
|
196
|
-
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;
|
|
197
206
|
}
|
|
198
207
|
function isSenderRefusal(message) {
|
|
199
|
-
const
|
|
200
|
-
return /\bfrom\./iu.test(
|
|
208
|
+
const text2 = String(message ?? "");
|
|
209
|
+
return /\bfrom\./iu.test(text2) || /remetente/iu.test(text2);
|
|
201
210
|
}
|
|
202
211
|
var money = (value) => new Intl.NumberFormat("pt-BR", { style: "currency", currency: "BRL" }).format(Number(value) || 0);
|
|
203
212
|
var eta = (option) => {
|
|
@@ -822,6 +831,408 @@ async function cancelShopOrder(provider, orderId, reason) {
|
|
|
822
831
|
const { error } = await db.rpc("shop_cancel_order", { p_order_id: orderId, p_reason: reason ?? null });
|
|
823
832
|
if (error) throw new Error(error.message);
|
|
824
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
|
+
}
|
|
825
1236
|
var money2 = (v, currency2 = "BRL") => new Intl.NumberFormat("pt-BR", { style: "currency", currency: currency2 }).format(Number(v) || 0);
|
|
826
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));
|
|
827
1238
|
var dateOnly = (iso) => new Date(iso).toLocaleDateString("pt-BR");
|
|
@@ -930,6 +1341,17 @@ function deliveryFromNotes(notes) {
|
|
|
930
1341
|
return (match?.[1] ?? "").trim() || null;
|
|
931
1342
|
}
|
|
932
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
|
+
}
|
|
933
1355
|
var shopShipmentsCache = /* @__PURE__ */ new Map();
|
|
934
1356
|
var shopReceivableCache = /* @__PURE__ */ new Map();
|
|
935
1357
|
function OrderDetailView({ orderId, provider, onBack }) {
|
|
@@ -946,6 +1368,12 @@ function OrderDetailView({ orderId, provider, onBack }) {
|
|
|
946
1368
|
const [statusError, setStatusError] = useState(null);
|
|
947
1369
|
const [cancelReason, setCancelReason] = useState("");
|
|
948
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);
|
|
949
1377
|
useEffect(() => {
|
|
950
1378
|
let cancelled = false;
|
|
951
1379
|
const cached = shopOrderCache.get(orderId);
|
|
@@ -977,6 +1405,19 @@ function OrderDetailView({ orderId, provider, onBack }) {
|
|
|
977
1405
|
cancelled = true;
|
|
978
1406
|
};
|
|
979
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]);
|
|
980
1421
|
useEffect(() => {
|
|
981
1422
|
let cancelled = false;
|
|
982
1423
|
const db = getSupabaseClientOptional();
|
|
@@ -1014,7 +1455,20 @@ function OrderDetailView({ orderId, provider, onBack }) {
|
|
|
1014
1455
|
useEffect(() => {
|
|
1015
1456
|
void loadReceivable();
|
|
1016
1457
|
}, [loadReceivable]);
|
|
1017
|
-
|
|
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]);
|
|
1471
|
+
if (loading) return /* @__PURE__ */ jsx(OrderDetailSkeleton, { parentLabel: "Pedidos", onBack });
|
|
1018
1472
|
if (error) {
|
|
1019
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: [
|
|
1020
1474
|
/* @__PURE__ */ jsx("p", { className: "font-medium", children: "N\xE3o foi poss\xEDvel carregar o pedido." }),
|
|
@@ -1088,80 +1542,115 @@ function OrderDetailView({ orderId, provider, onBack }) {
|
|
|
1088
1542
|
setStatusSaving(false);
|
|
1089
1543
|
}
|
|
1090
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) : [];
|
|
1091
1565
|
const destructive = pendingStatus === "cancelled";
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
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" }),
|
|
1107
1594
|
/* @__PURE__ */ jsx(
|
|
1108
|
-
|
|
1595
|
+
Button,
|
|
1109
1596
|
{
|
|
1110
|
-
|
|
1597
|
+
variant: destructive ? "destructive" : "default",
|
|
1111
1598
|
onClick: () => applyStatus(pendingStatus),
|
|
1112
1599
|
disabled: statusSaving,
|
|
1113
1600
|
"data-testid": "order-status-confirm",
|
|
1114
|
-
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"}`,
|
|
1115
1601
|
children: statusSaving ? "Salvando\u2026" : LIFECYCLE_CONFIRM[pendingStatus].cta
|
|
1116
1602
|
}
|
|
1117
|
-
),
|
|
1118
|
-
/* @__PURE__ */ jsx(
|
|
1119
|
-
"button",
|
|
1120
|
-
{
|
|
1121
|
-
type: "button",
|
|
1122
|
-
onClick: () => {
|
|
1123
|
-
setPendingStatus(null);
|
|
1124
|
-
setStatusError(null);
|
|
1125
|
-
setCancelReason("");
|
|
1126
|
-
},
|
|
1127
|
-
className: "rounded-md border bg-background px-3 py-1.5 text-sm font-medium transition hover:bg-muted",
|
|
1128
|
-
children: "Voltar"
|
|
1129
|
-
}
|
|
1130
1603
|
)
|
|
1131
1604
|
] })
|
|
1132
|
-
] })
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
children: "Reabrir pedido"
|
|
1141
|
-
}
|
|
1142
|
-
),
|
|
1143
|
-
order.status === "open" && /* @__PURE__ */ jsx(
|
|
1144
|
-
"button",
|
|
1145
|
-
{
|
|
1146
|
-
type: "button",
|
|
1147
|
-
onClick: () => setPendingStatus("archived"),
|
|
1148
|
-
"data-testid": "order-archive",
|
|
1149
|
-
className: "rounded-md border px-3 py-1.5 text-sm font-medium transition hover:bg-muted",
|
|
1150
|
-
children: "Arquivar"
|
|
1151
|
-
}
|
|
1152
|
-
),
|
|
1153
|
-
order.status !== "cancelled" && /* @__PURE__ */ jsx(
|
|
1154
|
-
"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,
|
|
1155
1613
|
{
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
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
|
+
]
|
|
1161
1651
|
}
|
|
1162
|
-
)
|
|
1163
|
-
|
|
1164
|
-
] });
|
|
1652
|
+
)
|
|
1653
|
+
);
|
|
1165
1654
|
const installments = receivable && receivable.bills.length > 0 ? receivable.bills.map((b) => ({
|
|
1166
1655
|
number: b.installment_number ?? 0,
|
|
1167
1656
|
amount: `${money2(Number(b.paid_amount ?? 0), cur)} / ${money2(Number(b.amount), cur)}`,
|
|
@@ -1170,8 +1659,20 @@ function OrderDetailView({ orderId, provider, onBack }) {
|
|
|
1170
1659
|
dueDate: `vence ${dateOnly(b.due_date)}`
|
|
1171
1660
|
})) : [];
|
|
1172
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,
|
|
1173
1672
|
title: `Pedido #${order.orderNumber}`,
|
|
1174
|
-
|
|
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,
|
|
1175
1676
|
badges: [
|
|
1176
1677
|
{ label: ORDER_STATUS_LABEL[order.financialStatus] ?? order.financialStatus, tone: toneOf(order.financialStatus) },
|
|
1177
1678
|
{ label: ORDER_STATUS_LABEL[order.fulfillmentStatus] ?? order.fulfillmentStatus, tone: toneOf(order.fulfillmentStatus) }
|
|
@@ -1189,7 +1690,20 @@ function OrderDetailView({ orderId, provider, onBack }) {
|
|
|
1189
1690
|
className: "font-medium"
|
|
1190
1691
|
}
|
|
1191
1692
|
) : /* @__PURE__ */ jsx("p", { className: "font-medium", children: order.customerName ?? "\u2014" }),
|
|
1192
|
-
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
|
+
)) }),
|
|
1193
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." })
|
|
1194
1708
|
] }),
|
|
1195
1709
|
items: order.items.map((it) => ({
|
|
@@ -1221,17 +1735,7 @@ function OrderDetailView({ orderId, provider, onBack }) {
|
|
|
1221
1735
|
],
|
|
1222
1736
|
installments,
|
|
1223
1737
|
actions: /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1224
|
-
receivable.balance.balance > 0 && receivable.balance.status !== "cancelled" && /* @__PURE__ */ jsx(
|
|
1225
|
-
"button",
|
|
1226
|
-
{
|
|
1227
|
-
type: "button",
|
|
1228
|
-
onClick: registerReceipt,
|
|
1229
|
-
disabled: confirming,
|
|
1230
|
-
"data-testid": "order-register-receipt",
|
|
1231
|
-
className: "rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground transition hover:opacity-90 disabled:opacity-60",
|
|
1232
|
-
children: confirming ? "Registrando\u2026" : "Registrar recebimento"
|
|
1233
|
-
}
|
|
1234
|
-
),
|
|
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" }),
|
|
1235
1739
|
hasFinancial && /* @__PURE__ */ jsx(
|
|
1236
1740
|
"button",
|
|
1237
1741
|
{
|
|
@@ -1299,156 +1803,220 @@ function OrderDetailView({ orderId, provider, onBack }) {
|
|
|
1299
1803
|
] })
|
|
1300
1804
|
] }) }),
|
|
1301
1805
|
/* @__PURE__ */ jsx(OrderProviderLinks, { orderId: order.id }),
|
|
1302
|
-
/* @__PURE__ */ jsx(OrderDetailBlock, { title: "
|
|
1303
|
-
/* @__PURE__ */ jsx(OrderDetailRow, { label: "Criado", value: dateTime2(order.createdAt) }),
|
|
1304
|
-
/* @__PURE__ */ jsx(OrderDetailRow, { label: "Atualizado", value: dateTime2(order.updatedAt) }),
|
|
1305
|
-
/* @__PURE__ */ jsx(OrderDetailRow, { label: "Situa\xE7\xE3o", value: /* @__PURE__ */ jsx(InlineBadge, { value: order.status }) })
|
|
1306
|
-
] }) }),
|
|
1806
|
+
/* @__PURE__ */ jsx(OrderDetailBlock, { title: "Situa\xE7\xE3o", children: /* @__PURE__ */ jsx(InlineBadge, { value: order.status }) }),
|
|
1307
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 }) })
|
|
1308
1808
|
] }),
|
|
1309
1809
|
onBack,
|
|
1310
1810
|
backLabel: "Pedidos"
|
|
1311
1811
|
};
|
|
1312
|
-
return /* @__PURE__ */
|
|
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
|
+
] });
|
|
1313
1874
|
}
|
|
1314
|
-
|
|
1315
|
-
|
|
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({
|
|
1316
1886
|
open,
|
|
1887
|
+
onClose,
|
|
1317
1888
|
provider,
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
onPaid,
|
|
1326
|
-
onClose
|
|
1889
|
+
variantId,
|
|
1890
|
+
productId,
|
|
1891
|
+
productName,
|
|
1892
|
+
sku,
|
|
1893
|
+
available,
|
|
1894
|
+
needed,
|
|
1895
|
+
onReceived
|
|
1327
1896
|
}) {
|
|
1328
|
-
const
|
|
1329
|
-
const
|
|
1330
|
-
const [
|
|
1331
|
-
const
|
|
1332
|
-
const
|
|
1333
|
-
const
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
try {
|
|
1337
|
-
const result = await provider().settleCheckoutSession?.(sessionId);
|
|
1338
|
-
if (!result) return;
|
|
1339
|
-
if (result.state === "paid") {
|
|
1340
|
-
settled.current = true;
|
|
1341
|
-
setState("paid");
|
|
1342
|
-
setTimeout(() => onPaid(result.orderId ?? void 0), 1200);
|
|
1343
|
-
return;
|
|
1344
|
-
}
|
|
1345
|
-
const outcome = chargeOutcome(result.state);
|
|
1346
|
-
if (outcome !== "waiting") {
|
|
1347
|
-
settled.current = true;
|
|
1348
|
-
setState(outcome);
|
|
1349
|
-
}
|
|
1350
|
-
} finally {
|
|
1351
|
-
setChecking(false);
|
|
1352
|
-
}
|
|
1353
|
-
}, [provider, sessionId, 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);
|
|
1354
1905
|
useEffect(() => {
|
|
1355
1906
|
if (!open) return;
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
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;
|
|
1919
|
+
}
|
|
1920
|
+
setSaving(true);
|
|
1921
|
+
setError(null);
|
|
1362
1922
|
try {
|
|
1363
|
-
await
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
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);
|
|
1367
1934
|
}
|
|
1368
1935
|
}
|
|
1369
|
-
const
|
|
1370
|
-
`Ol\xE1${customerName ? ` ${customerName.split(" ")[0]}` : ""}! Segue o Pix de ${money4} para o seu pedido:
|
|
1371
|
-
|
|
1372
|
-
${emv}`
|
|
1373
|
-
)}` : null;
|
|
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";
|
|
1374
1937
|
return /* @__PURE__ */ jsx(Modal, { open, onOpenChange: (next) => {
|
|
1375
1938
|
if (!next) onClose();
|
|
1376
|
-
}, children: /* @__PURE__ */ jsx(ModalPortal, { children: /* @__PURE__ */ jsx(ModalOverlay, { children: /* @__PURE__ */ jsxs(ModalContent, { size: "lg", "data-testid": "
|
|
1939
|
+
}, children: /* @__PURE__ */ jsx(ModalPortal, { children: /* @__PURE__ */ jsx(ModalOverlay, { children: /* @__PURE__ */ jsxs(ModalContent, { size: "lg", "data-testid": "stock-entry-modal", children: [
|
|
1377
1940
|
/* @__PURE__ */ jsxs(ModalHeader, { className: "mb-4", children: [
|
|
1378
|
-
/* @__PURE__ */
|
|
1379
|
-
|
|
1380
|
-
|
|
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}` : ""
|
|
1951
|
+
] })
|
|
1952
|
+
] }),
|
|
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
|
+
)
|
|
1381
2001
|
] }),
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
qrcodeBase64 && /* @__PURE__ */ jsx(
|
|
1387
|
-
"img",
|
|
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",
|
|
1388
2006
|
{
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
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
|
|
1393
2012
|
}
|
|
1394
|
-
)
|
|
1395
|
-
/* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1 space-y-3", children: [
|
|
1396
|
-
/* @__PURE__ */ jsxs("div", { children: [
|
|
1397
|
-
/* @__PURE__ */ jsx("p", { className: "text-xs font-medium text-muted-foreground", children: "Pix copia e cola" }),
|
|
1398
|
-
/* @__PURE__ */ jsx(
|
|
1399
|
-
"p",
|
|
1400
|
-
{
|
|
1401
|
-
"data-testid": "new-order-emv",
|
|
1402
|
-
className: "mt-1 max-h-20 overflow-auto break-all rounded-md border bg-muted/30 p-2 font-mono text-[11px]",
|
|
1403
|
-
children: emv
|
|
1404
|
-
}
|
|
1405
|
-
)
|
|
1406
|
-
] }),
|
|
1407
|
-
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-2", children: [
|
|
1408
|
-
/* @__PURE__ */ jsxs(Button, { size: "sm", onClick: copy, "data-testid": "new-order-copy", children: [
|
|
1409
|
-
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" }),
|
|
1410
|
-
copied ? "Copiado" : "Copiar c\xF3digo"
|
|
1411
|
-
] }),
|
|
1412
|
-
whatsapp && /* @__PURE__ */ jsxs(
|
|
1413
|
-
"a",
|
|
1414
|
-
{
|
|
1415
|
-
href: whatsapp,
|
|
1416
|
-
target: "_blank",
|
|
1417
|
-
rel: "noreferrer",
|
|
1418
|
-
"data-testid": "new-order-whatsapp",
|
|
1419
|
-
className: "inline-flex items-center rounded-md border px-3 py-1.5 text-sm font-medium transition hover:bg-muted",
|
|
1420
|
-
children: [
|
|
1421
|
-
/* @__PURE__ */ jsx(MessageCircle, { className: "mr-1.5 h-3.5 w-3.5" }),
|
|
1422
|
-
"Mandar no WhatsApp"
|
|
1423
|
-
]
|
|
1424
|
-
}
|
|
1425
|
-
)
|
|
1426
|
-
] })
|
|
1427
|
-
] })
|
|
2013
|
+
)
|
|
1428
2014
|
] }),
|
|
1429
|
-
/* @__PURE__ */
|
|
1430
|
-
state === "waiting" && /* @__PURE__ */ jsxs("p", { className: "flex items-center gap-2 text-muted-foreground", "data-testid": "new-order-waiting", children: [
|
|
1431
|
-
/* @__PURE__ */ jsx("span", { className: "h-2 w-2 animate-pulse rounded-full bg-amber-500" }),
|
|
1432
|
-
"Aguardando o pagamento\u2026 a tela confirma sozinha."
|
|
1433
|
-
] }),
|
|
1434
|
-
state === "paid" && /* @__PURE__ */ jsxs("p", { className: "flex items-center gap-2 font-medium text-emerald-700", "data-testid": "new-order-paid", children: [
|
|
1435
|
-
/* @__PURE__ */ jsx(Check, { className: "h-4 w-4" }),
|
|
1436
|
-
" Pagamento confirmado \u2014 abrindo o pedido\u2026"
|
|
1437
|
-
] }),
|
|
1438
|
-
(state === "expired" || state === "failed") && /* @__PURE__ */ jsx("p", { className: "text-red-700", "data-testid": "new-order-dead", children: state === "expired" ? "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." })
|
|
1439
|
-
] })
|
|
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 })
|
|
1440
2016
|
] }),
|
|
1441
2017
|
/* @__PURE__ */ jsxs(ModalFooter, { className: "mt-4 gap-2", children: [
|
|
1442
|
-
|
|
1443
|
-
/* @__PURE__ */ jsx(
|
|
1444
|
-
Button,
|
|
1445
|
-
{
|
|
1446
|
-
variant: state === "waiting" ? "ghost" : "default",
|
|
1447
|
-
onClick: onClose,
|
|
1448
|
-
"data-testid": "new-order-charge-close",
|
|
1449
|
-
children: state === "waiting" ? "Fechar" : "Voltar ao pedido"
|
|
1450
|
-
}
|
|
1451
|
-
)
|
|
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" })
|
|
1452
2020
|
] })
|
|
1453
2021
|
] }) }) }) });
|
|
1454
2022
|
}
|
|
@@ -1470,12 +2038,11 @@ var PAYMENT_METHODS = [
|
|
|
1470
2038
|
{ id: "boleto", label: "Boleto" },
|
|
1471
2039
|
{ id: "other", label: "Outro" }
|
|
1472
2040
|
];
|
|
1473
|
-
var CHARGEABLE_PROVIDERS = ["abacatepay"];
|
|
1474
2041
|
var money3 = (value, currency2 = "BRL") => new Intl.NumberFormat("pt-BR", { style: "currency", currency: currency2 }).format(Number(value) || 0);
|
|
1475
|
-
var
|
|
2042
|
+
var onlyDigits2 = (value) => value.replace(/\D/gu, "");
|
|
1476
2043
|
var maskPostal = (value) => {
|
|
1477
|
-
const
|
|
1478
|
-
return
|
|
2044
|
+
const digits2 = onlyDigits2(value).slice(0, 8);
|
|
2045
|
+
return digits2.length > 5 ? `${digits2.slice(0, 5)}-${digits2.slice(5)}` : digits2;
|
|
1479
2046
|
};
|
|
1480
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";
|
|
1481
2048
|
function Field({ label, hint, children }) {
|
|
@@ -1485,7 +2052,10 @@ function Field({ label, hint, children }) {
|
|
|
1485
2052
|
hint && /* @__PURE__ */ jsx("span", { className: "mt-0.5 block text-[11px] text-muted-foreground", children: hint })
|
|
1486
2053
|
] });
|
|
1487
2054
|
}
|
|
1488
|
-
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);
|
|
1489
2059
|
const [lines, setLines] = useState([]);
|
|
1490
2060
|
const [customer, setCustomer] = useState(EMPTY_CUSTOMER);
|
|
1491
2061
|
const [address, setAddress] = useState(EMPTY_ADDRESS);
|
|
@@ -1501,7 +2071,68 @@ function NewOrderPage({ provider, onDone, onCancel }) {
|
|
|
1501
2071
|
const [error, setError] = useState(null);
|
|
1502
2072
|
const [session, setSession] = useState(null);
|
|
1503
2073
|
const [chargeOpen, setChargeOpen] = useState(false);
|
|
2074
|
+
const [charging, setCharging] = useState(false);
|
|
2075
|
+
const [lastEnding, setLastEnding] = useState(null);
|
|
2076
|
+
const [entryOpen, setEntryOpen] = useState(false);
|
|
1504
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]);
|
|
1505
2136
|
useEffect(() => {
|
|
1506
2137
|
let cancelled = false;
|
|
1507
2138
|
provider().getStoreSettings?.().then((value) => {
|
|
@@ -1514,12 +2145,13 @@ function NewOrderPage({ provider, onDone, onCancel }) {
|
|
|
1514
2145
|
}, [provider]);
|
|
1515
2146
|
const acquirer = settings?.paymentProvider ?? null;
|
|
1516
2147
|
const canCharge = Boolean(acquirer && CHARGEABLE_PROVIDERS.includes(acquirer));
|
|
2148
|
+
const canLink = canCharge && supportsPaymentLink(acquirer);
|
|
1517
2149
|
const currency2 = settings?.currency ?? "BRL";
|
|
1518
2150
|
const subtotal = useMemo(
|
|
1519
2151
|
() => lines.reduce((sum, line) => sum + line.unitPrice * line.quantity, 0),
|
|
1520
2152
|
[lines]
|
|
1521
2153
|
);
|
|
1522
|
-
const postalDigits =
|
|
2154
|
+
const postalDigits = onlyDigits2(address.postalCode);
|
|
1523
2155
|
const delivers = postalDigits.length === 8;
|
|
1524
2156
|
const shipping = delivers ? freight?.price ?? 0 : 0;
|
|
1525
2157
|
const quoteFreight = useCallback(async (postal) => {
|
|
@@ -1556,7 +2188,8 @@ function NewOrderPage({ provider, onDone, onCancel }) {
|
|
|
1556
2188
|
quantity: 1,
|
|
1557
2189
|
variants: sellable.length > 1 ? sellable : [],
|
|
1558
2190
|
variantId: only?.id ?? null,
|
|
1559
|
-
optionsLabel: null
|
|
2191
|
+
optionsLabel: null,
|
|
2192
|
+
stock: only?.inventoryCount ?? product.inventoryCount ?? null
|
|
1560
2193
|
}];
|
|
1561
2194
|
});
|
|
1562
2195
|
}
|
|
@@ -1582,8 +2215,8 @@ function NewOrderPage({ provider, onDone, onCancel }) {
|
|
|
1582
2215
|
state: address.state.trim().toUpperCase(),
|
|
1583
2216
|
country: "BR",
|
|
1584
2217
|
recipient: customer.name.trim() || void 0,
|
|
1585
|
-
phone:
|
|
1586
|
-
document:
|
|
2218
|
+
phone: onlyDigits2(customer.phone) || void 0,
|
|
2219
|
+
document: onlyDigits2(customer.document) || void 0
|
|
1587
2220
|
} : void 0;
|
|
1588
2221
|
function validate(ending) {
|
|
1589
2222
|
return validateNewOrder({
|
|
@@ -1591,14 +2224,14 @@ function NewOrderPage({ provider, onDone, onCancel }) {
|
|
|
1591
2224
|
unresolvedVariants: lines.filter((line) => line.variants.length > 0 && !line.variantId).length,
|
|
1592
2225
|
customerName: customer.name,
|
|
1593
2226
|
customerEmail: customer.email,
|
|
1594
|
-
customerPhone:
|
|
2227
|
+
customerPhone: onlyDigits2(customer.phone),
|
|
1595
2228
|
customerDocument: customer.document.trim(),
|
|
1596
2229
|
documentValid: isValidDocument(customer.document),
|
|
1597
2230
|
delivers,
|
|
1598
2231
|
addressComplete: Boolean(address.street.trim() && address.city.trim() && address.state.trim())
|
|
1599
2232
|
}, ending);
|
|
1600
2233
|
}
|
|
1601
|
-
async function
|
|
2234
|
+
async function resolveCustomerId2() {
|
|
1602
2235
|
if (customer.id) return customer.id;
|
|
1603
2236
|
const email = customer.email.trim().toLowerCase();
|
|
1604
2237
|
const [first, ...rest] = customer.name.trim().split(/\s+/u);
|
|
@@ -1611,7 +2244,7 @@ function NewOrderPage({ provider, onDone, onCancel }) {
|
|
|
1611
2244
|
firstName: first ?? customer.name.trim(),
|
|
1612
2245
|
lastName: rest.join(" "),
|
|
1613
2246
|
email: email || void 0,
|
|
1614
|
-
phone:
|
|
2247
|
+
phone: onlyDigits2(customer.phone) || void 0
|
|
1615
2248
|
});
|
|
1616
2249
|
return created.id;
|
|
1617
2250
|
}
|
|
@@ -1621,10 +2254,11 @@ function NewOrderPage({ provider, onDone, onCancel }) {
|
|
|
1621
2254
|
setError(invalid);
|
|
1622
2255
|
return;
|
|
1623
2256
|
}
|
|
2257
|
+
setLastEnding(ending);
|
|
1624
2258
|
setSaving(true);
|
|
1625
2259
|
setError(null);
|
|
1626
2260
|
try {
|
|
1627
|
-
if (ending === "charge") {
|
|
2261
|
+
if (ending === "charge" || ending === "link") {
|
|
1628
2262
|
const opened = await provider().openCheckoutSession?.({
|
|
1629
2263
|
items: lines.map((line) => ({
|
|
1630
2264
|
productId: line.productId,
|
|
@@ -1634,20 +2268,32 @@ function NewOrderPage({ provider, onDone, onCancel }) {
|
|
|
1634
2268
|
})),
|
|
1635
2269
|
customer: { name: customer.name.trim(), email: customer.email.trim().toLowerCase() },
|
|
1636
2270
|
shippingAddress,
|
|
1637
|
-
discountCode: discountCode.trim() || null
|
|
2271
|
+
discountCode: discountCode.trim() || null,
|
|
2272
|
+
// The one difference between the two endings, all the way down: same
|
|
2273
|
+
// repricing, same frozen session, same settlement by webhook. What
|
|
2274
|
+
// changes is whether the acquirer answers with a code to read out or
|
|
2275
|
+
// an address to send.
|
|
2276
|
+
asLink: ending === "link"
|
|
1638
2277
|
});
|
|
1639
|
-
if (
|
|
2278
|
+
if (ending === "link" && !opened?.paymentUrl) {
|
|
2279
|
+
throw new Error("O adquirente n\xE3o devolveu o link de pagamento. Tente de novo.");
|
|
2280
|
+
}
|
|
2281
|
+
if (ending === "charge" && !opened?.charge?.emv) {
|
|
2282
|
+
throw new Error("A cobran\xE7a foi aberta sem c\xF3digo Pix. Tente de novo.");
|
|
2283
|
+
}
|
|
1640
2284
|
setSession({
|
|
1641
2285
|
sessionId: opened.sessionId,
|
|
1642
|
-
emv: opened.charge
|
|
1643
|
-
qrcode: opened.charge
|
|
2286
|
+
emv: opened.charge?.emv ?? null,
|
|
2287
|
+
qrcode: opened.charge?.qrcodeBase64 ?? null,
|
|
2288
|
+
qrcodeUrl: opened.charge?.qrcodeUrl ?? null,
|
|
2289
|
+
paymentUrl: opened.paymentUrl ?? null,
|
|
1644
2290
|
total: opened.total
|
|
1645
2291
|
});
|
|
1646
2292
|
setChargeOpen(true);
|
|
1647
2293
|
return;
|
|
1648
2294
|
}
|
|
1649
|
-
const customerId = await
|
|
1650
|
-
const
|
|
2295
|
+
const customerId = await resolveCustomerId2();
|
|
2296
|
+
const payload = {
|
|
1651
2297
|
customerId,
|
|
1652
2298
|
customer: { name: customer.name.trim(), email: customer.email.trim().toLowerCase() || void 0 },
|
|
1653
2299
|
items: lines.map((line) => ({
|
|
@@ -1660,10 +2306,14 @@ function NewOrderPage({ provider, onDone, onCancel }) {
|
|
|
1660
2306
|
discountCode: discountCode.trim() || void 0,
|
|
1661
2307
|
paymentMethod: ending === "received" ? method : void 0,
|
|
1662
2308
|
notes: notes.trim() || void 0
|
|
1663
|
-
}
|
|
2309
|
+
};
|
|
2310
|
+
const order = orderId ? await provider().reviseOrder(orderId, payload) : await provider().placeOrder(payload);
|
|
1664
2311
|
if (ending === "received") {
|
|
1665
2312
|
await provider().confirmPayment?.(order.id, reference.trim() || `manual:${method}`);
|
|
1666
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
|
+
);
|
|
1667
2317
|
onDone(order.id);
|
|
1668
2318
|
} catch (err) {
|
|
1669
2319
|
setError(errorMessage(err));
|
|
@@ -1671,6 +2321,35 @@ function NewOrderPage({ provider, onDone, onCancel }) {
|
|
|
1671
2321
|
setSaving(false);
|
|
1672
2322
|
}
|
|
1673
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]);
|
|
1674
2353
|
const sideSections = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1675
2354
|
/* @__PURE__ */ jsx(OrderFormBlock, { title: "Cliente", children: /* @__PURE__ */ jsx(
|
|
1676
2355
|
CustomerPicker,
|
|
@@ -1707,10 +2386,10 @@ function NewOrderPage({ provider, onDone, onCancel }) {
|
|
|
1707
2386
|
/* @__PURE__ */ jsxs(
|
|
1708
2387
|
OrderFormView,
|
|
1709
2388
|
{
|
|
1710
|
-
title: "Novo pedido",
|
|
2389
|
+
title: orderId ? `Editar pedido${orderNumber ? ` #${orderNumber}` : ""}` : "Novo pedido",
|
|
1711
2390
|
parentLabel: "Pedidos",
|
|
1712
2391
|
onBack: onCancel,
|
|
1713
|
-
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",
|
|
1714
2393
|
sideSections,
|
|
1715
2394
|
children: [
|
|
1716
2395
|
/* @__PURE__ */ jsxs(OrderFormBlock, { title: "Produtos", children: [
|
|
@@ -1752,7 +2431,7 @@ function NewOrderPage({ provider, onDone, onCancel }) {
|
|
|
1752
2431
|
"input",
|
|
1753
2432
|
{
|
|
1754
2433
|
value: line.quantity,
|
|
1755
|
-
onChange: (event) => setQuantity(line.key, Number(
|
|
2434
|
+
onChange: (event) => setQuantity(line.key, Number(onlyDigits2(event.target.value) || 0)),
|
|
1756
2435
|
"aria-label": "Quantidade",
|
|
1757
2436
|
"data-testid": "new-order-quantity",
|
|
1758
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"
|
|
@@ -1865,47 +2544,85 @@ function NewOrderPage({ provider, onDone, onCancel }) {
|
|
|
1865
2544
|
customer.name
|
|
1866
2545
|
]
|
|
1867
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
|
+
}
|
|
1868
2560
|
)
|
|
1869
2561
|
]
|
|
1870
2562
|
}
|
|
1871
2563
|
),
|
|
1872
|
-
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: [
|
|
1873
2565
|
/* @__PURE__ */ jsx(Button, { onClick: () => void finish("received"), disabled: saving, "data-testid": "new-order-confirm-received", children: saving ? "Criando\u2026" : "Confirmar pagamento" }),
|
|
1874
2566
|
/* @__PURE__ */ jsx(Button, { variant: "outline", onClick: () => setSettling(false), children: "Voltar" })
|
|
1875
|
-
] }) :
|
|
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: [
|
|
1876
2583
|
canCharge && /* @__PURE__ */ jsx(
|
|
1877
2584
|
Button,
|
|
1878
2585
|
{
|
|
1879
|
-
onClick: () =>
|
|
2586
|
+
onClick: () => canLink ? setCharging(true) : void finish("charge"),
|
|
1880
2587
|
disabled: saving,
|
|
1881
2588
|
"data-testid": "new-order-charge",
|
|
1882
|
-
children: saving ? "Abrindo\u2026" :
|
|
2589
|
+
children: saving ? "Abrindo\u2026" : canLink ? "Cobrar" : "Cobrar por Pix"
|
|
1883
2590
|
}
|
|
1884
2591
|
),
|
|
1885
2592
|
/* @__PURE__ */ jsx(
|
|
1886
2593
|
Button,
|
|
1887
2594
|
{
|
|
1888
|
-
variant: canCharge ? "
|
|
2595
|
+
variant: canCharge ? "ghost" : "default",
|
|
1889
2596
|
onClick: () => setSettling(true),
|
|
1890
2597
|
"data-testid": "new-order-received",
|
|
1891
2598
|
children: "J\xE1 recebi"
|
|
1892
2599
|
}
|
|
1893
|
-
),
|
|
1894
|
-
/* @__PURE__ */ jsx(
|
|
1895
|
-
Button,
|
|
1896
|
-
{
|
|
1897
|
-
variant: "ghost",
|
|
1898
|
-
onClick: () => void finish("later"),
|
|
1899
|
-
disabled: saving,
|
|
1900
|
-
"data-testid": "new-order-later",
|
|
1901
|
-
children: "Cobrar depois"
|
|
1902
|
-
}
|
|
1903
2600
|
)
|
|
1904
2601
|
] }) })
|
|
1905
2602
|
] })
|
|
1906
2603
|
]
|
|
1907
2604
|
}
|
|
1908
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
|
+
),
|
|
1909
2626
|
session && /* @__PURE__ */ jsx(
|
|
1910
2627
|
NewOrderCharge,
|
|
1911
2628
|
{
|
|
@@ -1914,9 +2631,12 @@ function NewOrderPage({ provider, onDone, onCancel }) {
|
|
|
1914
2631
|
sessionId: session.sessionId,
|
|
1915
2632
|
emv: session.emv,
|
|
1916
2633
|
qrcodeBase64: session.qrcode,
|
|
2634
|
+
qrcodeUrl: session.qrcodeUrl,
|
|
2635
|
+
paymentUrl: session.paymentUrl,
|
|
2636
|
+
acquirer,
|
|
1917
2637
|
total: session.total,
|
|
1918
2638
|
currency: currency2,
|
|
1919
|
-
customerPhone:
|
|
2639
|
+
customerPhone: onlyDigits2(customer.phone),
|
|
1920
2640
|
customerName: customer.name,
|
|
1921
2641
|
onPaid: onDone,
|
|
1922
2642
|
onClose: () => setChargeOpen(false)
|
|
@@ -2113,147 +2833,6 @@ function CustomerPicker({ provider, value, onChange, editing, onEditing }) {
|
|
|
2113
2833
|
)
|
|
2114
2834
|
] });
|
|
2115
2835
|
}
|
|
2116
|
-
function CustomerModal({ open, initialName, customerId, initial, provider, onClose, onSaved }) {
|
|
2117
|
-
const [first, setFirst] = useState("");
|
|
2118
|
-
const [last, setLast] = useState("");
|
|
2119
|
-
const [email, setEmail] = useState("");
|
|
2120
|
-
const [phone, setPhone] = useState("");
|
|
2121
|
-
const [document, setDocument] = useState("");
|
|
2122
|
-
const [saving, setSaving] = useState(false);
|
|
2123
|
-
const [error, setError] = useState(null);
|
|
2124
|
-
const [duplicate, setDuplicate] = useState(null);
|
|
2125
|
-
useEffect(() => {
|
|
2126
|
-
if (!open) return;
|
|
2127
|
-
const [head, ...rest] = String(initial?.name ?? initialName ?? "").trim().split(/\s+/u);
|
|
2128
|
-
setFirst(head ?? "");
|
|
2129
|
-
setLast(rest.join(" "));
|
|
2130
|
-
setEmail(initial?.email ?? "");
|
|
2131
|
-
setPhone(initial?.phone ?? "");
|
|
2132
|
-
setDocument(initial?.document ?? "");
|
|
2133
|
-
setError(null);
|
|
2134
|
-
setDuplicate(null);
|
|
2135
|
-
}, [open, initialName, initial]);
|
|
2136
|
-
async function save() {
|
|
2137
|
-
if (!first.trim()) {
|
|
2138
|
-
setError("O nome \xE9 obrigat\xF3rio.");
|
|
2139
|
-
return;
|
|
2140
|
-
}
|
|
2141
|
-
if (document && !isValidDocument(document)) {
|
|
2142
|
-
setError("Esse CPF/CNPJ n\xE3o confere.");
|
|
2143
|
-
return;
|
|
2144
|
-
}
|
|
2145
|
-
setSaving(true);
|
|
2146
|
-
setError(null);
|
|
2147
|
-
setDuplicate(null);
|
|
2148
|
-
try {
|
|
2149
|
-
const fields = {
|
|
2150
|
-
firstName: first.trim(),
|
|
2151
|
-
lastName: last.trim(),
|
|
2152
|
-
email: email.trim().toLowerCase() || void 0,
|
|
2153
|
-
phone: onlyDigits(phone) || void 0,
|
|
2154
|
-
document: documentDigits(document) || void 0
|
|
2155
|
-
};
|
|
2156
|
-
const saved = customerId ? await provider().updateCustomer(customerId, fields) : await provider().createCustomer(fields);
|
|
2157
|
-
onSaved(saved);
|
|
2158
|
-
} catch (err) {
|
|
2159
|
-
if (isDuplicateEmail(err)) {
|
|
2160
|
-
const existing = await provider().listCustomers({ search: email.trim().toLowerCase(), limit: 5 }).catch(() => []);
|
|
2161
|
-
const hit = existing.find((c) => (c.email ?? "").toLowerCase() === email.trim().toLowerCase());
|
|
2162
|
-
if (hit) {
|
|
2163
|
-
setDuplicate(hit);
|
|
2164
|
-
return;
|
|
2165
|
-
}
|
|
2166
|
-
setError("J\xE1 existe um cliente com esse e-mail nesta loja. Procure por ele na busca.");
|
|
2167
|
-
return;
|
|
2168
|
-
}
|
|
2169
|
-
setError(errorMessage(err));
|
|
2170
|
-
} finally {
|
|
2171
|
-
setSaving(false);
|
|
2172
|
-
}
|
|
2173
|
-
}
|
|
2174
|
-
return /* @__PURE__ */ jsx(Modal, { open, onOpenChange: (next) => {
|
|
2175
|
-
if (!next) onClose();
|
|
2176
|
-
}, children: /* @__PURE__ */ jsx(ModalPortal, { children: /* @__PURE__ */ jsx(ModalOverlay, { children: /* @__PURE__ */ jsxs(ModalContent, { size: "lg", children: [
|
|
2177
|
-
/* @__PURE__ */ jsx(ModalHeader, { className: "mb-4", children: /* @__PURE__ */ jsx(ModalTitle, { children: customerId ? "Cadastro do cliente" : "Novo cliente" }) }),
|
|
2178
|
-
/* @__PURE__ */ jsxs(ModalBody, { className: "space-y-3", children: [
|
|
2179
|
-
/* @__PURE__ */ jsxs("div", { className: "fz-fields fz-fields-2 gap-3", children: [
|
|
2180
|
-
/* @__PURE__ */ jsx(Field, { label: "Nome", children: /* @__PURE__ */ jsx(
|
|
2181
|
-
"input",
|
|
2182
|
-
{
|
|
2183
|
-
value: first,
|
|
2184
|
-
onChange: (e) => setFirst(e.target.value),
|
|
2185
|
-
autoFocus: true,
|
|
2186
|
-
"data-testid": "new-customer-first",
|
|
2187
|
-
className: fieldClass
|
|
2188
|
-
}
|
|
2189
|
-
) }),
|
|
2190
|
-
/* @__PURE__ */ jsx(Field, { label: "Sobrenome", children: /* @__PURE__ */ jsx(
|
|
2191
|
-
"input",
|
|
2192
|
-
{
|
|
2193
|
-
value: last,
|
|
2194
|
-
onChange: (e) => setLast(e.target.value),
|
|
2195
|
-
"data-testid": "new-customer-last",
|
|
2196
|
-
className: fieldClass
|
|
2197
|
-
}
|
|
2198
|
-
) })
|
|
2199
|
-
] }),
|
|
2200
|
-
/* @__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" }) }) }),
|
|
2201
|
-
/* @__PURE__ */ jsx(Field, { label: "E-mail", children: /* @__PURE__ */ jsx(
|
|
2202
|
-
"input",
|
|
2203
|
-
{
|
|
2204
|
-
type: "email",
|
|
2205
|
-
value: email,
|
|
2206
|
-
onChange: (e) => setEmail(e.target.value),
|
|
2207
|
-
"data-testid": "new-customer-email",
|
|
2208
|
-
className: fieldClass
|
|
2209
|
-
}
|
|
2210
|
-
) }),
|
|
2211
|
-
/* @__PURE__ */ jsx(Field, { label: "CPF / CNPJ", hint: "Necess\xE1rio para cobran\xE7a online e para a nota fiscal", children: /* @__PURE__ */ jsx(
|
|
2212
|
-
"input",
|
|
2213
|
-
{
|
|
2214
|
-
value: formatDocument(document),
|
|
2215
|
-
onChange: (e) => setDocument(documentDigits(e.target.value)),
|
|
2216
|
-
placeholder: "000.000.000-00",
|
|
2217
|
-
"data-testid": "new-customer-document",
|
|
2218
|
-
className: fieldClass
|
|
2219
|
-
}
|
|
2220
|
-
) }),
|
|
2221
|
-
duplicate && /* @__PURE__ */ jsxs(
|
|
2222
|
-
"div",
|
|
2223
|
-
{
|
|
2224
|
-
role: "alert",
|
|
2225
|
-
"data-testid": "new-customer-duplicate",
|
|
2226
|
-
className: "rounded-md border border-amber-200 bg-amber-50 px-3 py-2 text-sm text-amber-900",
|
|
2227
|
-
children: [
|
|
2228
|
-
/* @__PURE__ */ jsxs("p", { children: [
|
|
2229
|
-
/* @__PURE__ */ jsxs("strong", { children: [
|
|
2230
|
-
duplicate.firstName,
|
|
2231
|
-
" ",
|
|
2232
|
-
duplicate.lastName
|
|
2233
|
-
] }),
|
|
2234
|
-
" j\xE1 \xE9 cliente com esse e-mail."
|
|
2235
|
-
] }),
|
|
2236
|
-
/* @__PURE__ */ jsx(
|
|
2237
|
-
"button",
|
|
2238
|
-
{
|
|
2239
|
-
type: "button",
|
|
2240
|
-
onClick: () => onSaved(duplicate),
|
|
2241
|
-
"data-testid": "new-customer-use-existing",
|
|
2242
|
-
className: "mt-1 font-medium underline underline-offset-2",
|
|
2243
|
-
children: "Usar este cliente"
|
|
2244
|
-
}
|
|
2245
|
-
)
|
|
2246
|
-
]
|
|
2247
|
-
}
|
|
2248
|
-
),
|
|
2249
|
-
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 })
|
|
2250
|
-
] }),
|
|
2251
|
-
/* @__PURE__ */ jsxs(ModalFooter, { className: "mt-4 gap-2", children: [
|
|
2252
|
-
/* @__PURE__ */ jsx(Button, { variant: "outline", onClick: onClose, children: "Cancelar" }),
|
|
2253
|
-
/* @__PURE__ */ jsx(Button, { onClick: save, disabled: saving, "data-testid": "new-customer-save", children: saving ? "Salvando\u2026" : customerId ? "Salvar" : "Criar cliente" })
|
|
2254
|
-
] })
|
|
2255
|
-
] }) }) }) });
|
|
2256
|
-
}
|
|
2257
2836
|
function DeliveryAddress({ provider, customerId, address, onChange }) {
|
|
2258
2837
|
const [saved, setSaved] = useState([]);
|
|
2259
2838
|
const [chosen, setChosen] = useState(null);
|
|
@@ -2424,14 +3003,14 @@ function PostalField({ value, onChange, onResolved }) {
|
|
|
2424
3003
|
const [looking, setLooking] = useState(false);
|
|
2425
3004
|
const [missing, setMissing] = useState(false);
|
|
2426
3005
|
useEffect(() => {
|
|
2427
|
-
const
|
|
2428
|
-
if (
|
|
3006
|
+
const digits2 = onlyDigits2(value);
|
|
3007
|
+
if (digits2.length !== 8) {
|
|
2429
3008
|
setMissing(false);
|
|
2430
3009
|
return;
|
|
2431
3010
|
}
|
|
2432
3011
|
let cancelled = false;
|
|
2433
3012
|
setLooking(true);
|
|
2434
|
-
fetch(`https://viacep.com.br/ws/${
|
|
3013
|
+
fetch(`https://viacep.com.br/ws/${digits2}/json/`).then((response) => response.json()).then((found) => {
|
|
2435
3014
|
if (cancelled) return;
|
|
2436
3015
|
if (found?.erro) {
|
|
2437
3016
|
setMissing(true);
|
|
@@ -2464,6 +3043,496 @@ function PostalField({ value, onChange, onResolved }) {
|
|
|
2464
3043
|
}
|
|
2465
3044
|
) });
|
|
2466
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
|
+
}
|
|
2467
3536
|
function currency(value, code = "BRL", locale = "pt-BR") {
|
|
2468
3537
|
return new Intl.NumberFormat(locale, { style: "currency", currency: code }).format(value);
|
|
2469
3538
|
}
|
|
@@ -2740,10 +3809,49 @@ function OrdersTab({ provider }) {
|
|
|
2740
3809
|
}, [orders]);
|
|
2741
3810
|
if (error) return /* @__PURE__ */ jsx(ErrorState, { message: error });
|
|
2742
3811
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
3812
|
+
/* @__PURE__ */ jsx(
|
|
3813
|
+
OrdersAgentSurface,
|
|
3814
|
+
{
|
|
3815
|
+
provider: () => resolveShopProvider(provider),
|
|
3816
|
+
orders,
|
|
3817
|
+
onChanged: reload
|
|
3818
|
+
}
|
|
3819
|
+
),
|
|
2743
3820
|
/* @__PURE__ */ jsx(
|
|
2744
3821
|
OrderListView,
|
|
2745
3822
|
{
|
|
2746
|
-
actions: /* @__PURE__ */ jsx(
|
|
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
|
+
),
|
|
2747
3855
|
stats,
|
|
2748
3856
|
columns,
|
|
2749
3857
|
rows: orders,
|
|
@@ -2873,13 +3981,15 @@ var ShopPage = ({ provider, section }) => {
|
|
|
2873
3981
|
const active = section ?? tab;
|
|
2874
3982
|
const path = useAdminPath();
|
|
2875
3983
|
const orderId = section === "orders" ? path.match(/^\/orders\/([^/]+)$/)?.[1] : void 0;
|
|
2876
|
-
|
|
3984
|
+
const editingId = section === "orders" ? path.match(/^\/orders\/([^/]+)\/edit$/)?.[1] : void 0;
|
|
3985
|
+
if (section === "orders" && (orderId === "new" || editingId)) {
|
|
2877
3986
|
return /* @__PURE__ */ jsx(
|
|
2878
3987
|
NewOrderPage,
|
|
2879
3988
|
{
|
|
2880
3989
|
provider: () => resolveShopProvider(provider),
|
|
3990
|
+
orderId: editingId,
|
|
2881
3991
|
onDone: (id) => adminNavigateTo(id ? `/orders/${id}` : "/orders"),
|
|
2882
|
-
onCancel: () => adminNavigateTo("/orders")
|
|
3992
|
+
onCancel: () => adminNavigateTo(editingId ? `/orders/${editingId}` : "/orders")
|
|
2883
3993
|
}
|
|
2884
3994
|
);
|
|
2885
3995
|
}
|
|
@@ -2922,5 +4032,5 @@ var ShopPage = ({ provider, section }) => {
|
|
|
2922
4032
|
ShopPage.displayName = "ShopPage";
|
|
2923
4033
|
|
|
2924
4034
|
export { ShopPage };
|
|
2925
|
-
//# sourceMappingURL=ShopPage-
|
|
2926
|
-
//# sourceMappingURL=ShopPage-
|
|
4035
|
+
//# sourceMappingURL=ShopPage-QFIM2HDU.js.map
|
|
4036
|
+
//# sourceMappingURL=ShopPage-QFIM2HDU.js.map
|