@fayz-ai/plugin-orders 0.12.2 → 0.12.4

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/index.js CHANGED
@@ -2,13 +2,13 @@ import { getNextStatus, STATUS_FLOW } from './chunk-TYS3LCKX.js';
2
2
  export { ADVANCE_VERB, DEFAULT_STATUS_CONFIGS, KANBAN_COLUMNS, STATUS_FLOW, getNextStatus, getPreviousStatus, mapToKanbanColumn } from './chunk-TYS3LCKX.js';
3
3
  import * as React2 from 'react';
4
4
  import React2__default from 'react';
5
- import { fayzConnectorOAuth, OrderDetailPage, orderIsEditable, PageActions, adminNavigateTo, PluginSettingsPanel, dedup, usePermissionOptional } from '@fayz-ai/admin';
6
- import { Pencil } from 'lucide-react';
5
+ import { fayzConnectorOAuth, OrderDetailPage, orderIsEditable, PageActions, adminNavigateTo, PluginSettingsPanel, dedup, usePermissionOptional, useConnectors, useConnectorsPanelAvailable, openConnectorsPanel, SettingsGroup, ToggleRow, AddConnectorButton, ConnectorRow, AddConnectorPanel, ConnectorDetail, TextRow } from '@fayz-ai/admin';
6
+ import { Pencil, AlertTriangle, Loader2, Check } from 'lucide-react';
7
7
  import { createConnectionStore, connectionRuns, connectionSettings, connectionStatus, settingsStatus, getSupabaseClientOptional, usePluginRuntimeOptional, emitDomainEvent, errorMessage } from '@fayz-ai/core';
8
8
  import { createStore } from 'zustand/vanilla';
9
9
  import { toast } from 'sonner';
10
10
  import { ReceivePaymentModal } from '@fayz-ai/plugin-financial';
11
- import { Button, toast as toast$1 } from '@fayz-ai/ui';
11
+ import { Button, Skeleton, toast as toast$1 } from '@fayz-ai/ui';
12
12
  import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
13
13
  import { createFayzClient } from '@fayz-ai/sdk';
14
14
 
@@ -755,8 +755,8 @@ async function runLifecycleHook(label, order, hook) {
755
755
  try {
756
756
  await hook(order);
757
757
  } catch (error) {
758
- const message = errorMessage(error);
759
- throw new Error(`Order was ${label}, but downstream synchronization is pending: ${message}`);
758
+ const message2 = errorMessage(error);
759
+ throw new Error(`Order was ${label}, but downstream synchronization is pending: ${message2}`);
760
760
  }
761
761
  }
762
762
  function createOrdersStore(provider, hooks = {}) {
@@ -990,6 +990,7 @@ var en = {
990
990
  "orders.newOrder.selectKind": "Order type",
991
991
  "orders.newOrder.dineIn": "Dine-in",
992
992
  "orders.newOrder.takeout": "Takeout",
993
+ "orders.delivery.settings": "Delivery settings",
993
994
  "orders.newOrder.delivery": "Delivery",
994
995
  "orders.newOrder.selectTable": "Select table",
995
996
  "orders.newOrder.addItems": "Add items",
@@ -1070,6 +1071,7 @@ var ptBR = {
1070
1071
  "orders.newOrder.selectKind": "Tipo de pedido",
1071
1072
  "orders.newOrder.dineIn": "Mesa",
1072
1073
  "orders.newOrder.takeout": "Para Viagem",
1074
+ "orders.delivery.settings": "Configurar entrega",
1073
1075
  "orders.newOrder.delivery": "Delivery",
1074
1076
  "orders.newOrder.selectTable": "Selecionar mesa",
1075
1077
  "orders.newOrder.addItems": "Adicionar itens",
@@ -1616,6 +1618,286 @@ function OrderReceivableActions({ orderId, orderTotal, receivable, reload }) {
1616
1618
  )
1617
1619
  ] });
1618
1620
  }
1621
+ var DEBOUNCE_MS = 400;
1622
+ var KEYS = {
1623
+ enabled: "delivery.enabled",
1624
+ fee: "delivery.fee",
1625
+ freeOver: "delivery.free_over",
1626
+ minOrder: "delivery.min_order",
1627
+ etaMinutes: "delivery.eta_minutes",
1628
+ radiusKm: "delivery.radius_km"
1629
+ };
1630
+ function message(e) {
1631
+ if (e instanceof Error) return e.message;
1632
+ if (typeof e === "object" && e && "message" in e) return String(e.message);
1633
+ return String(e);
1634
+ }
1635
+ function useDeliverySettings(defaults = {}) {
1636
+ const client = React2__default.useMemo(() => getSupabaseClientOptional(), []);
1637
+ const supported = !!client;
1638
+ const [stored, setStored] = React2__default.useState({});
1639
+ const [loading, setLoading] = React2__default.useState(supported);
1640
+ const [saving, setSaving] = React2__default.useState(false);
1641
+ const [error, setError] = React2__default.useState(null);
1642
+ const [loadError, setLoadError] = React2__default.useState(null);
1643
+ React2__default.useEffect(() => {
1644
+ if (!client) return;
1645
+ let cancelled = false;
1646
+ setLoading(true);
1647
+ setLoadError(null);
1648
+ void client.rpc("tenant_settings").then(({ data, error: e }) => {
1649
+ if (cancelled) return;
1650
+ if (e) {
1651
+ setLoadError(message(e));
1652
+ return;
1653
+ }
1654
+ const flat = data ?? {};
1655
+ const next = {};
1656
+ for (const [field, key] of Object.entries(KEYS)) {
1657
+ if (key in flat && flat[key] !== null) next[field] = flat[key];
1658
+ else if (key in flat && flat[key] === null) next[field] = null;
1659
+ }
1660
+ setStored(next);
1661
+ }).catch((e) => {
1662
+ if (!cancelled) setLoadError(message(e));
1663
+ }).finally(() => {
1664
+ if (!cancelled) setLoading(false);
1665
+ });
1666
+ return () => {
1667
+ cancelled = true;
1668
+ };
1669
+ }, [client]);
1670
+ const pendingRef = React2__default.useRef({});
1671
+ const timerRef = React2__default.useRef(null);
1672
+ React2__default.useEffect(() => () => {
1673
+ if (timerRef.current) clearTimeout(timerRef.current);
1674
+ }, []);
1675
+ const set = React2__default.useCallback((patch) => {
1676
+ setStored((current) => ({ ...current, ...patch }));
1677
+ if (!client) return;
1678
+ setError(null);
1679
+ setSaving(true);
1680
+ pendingRef.current = { ...pendingRef.current, ...patch };
1681
+ if (timerRef.current) clearTimeout(timerRef.current);
1682
+ timerRef.current = setTimeout(() => {
1683
+ const payload = pendingRef.current;
1684
+ pendingRef.current = {};
1685
+ void Promise.all(
1686
+ Object.entries(payload).map(
1687
+ ([field, value2]) => client.rpc("tenant_setting_set", { p_key: KEYS[field], p_value: value2 ?? null }).then(({ error: e }) => {
1688
+ if (e) throw e;
1689
+ })
1690
+ )
1691
+ ).catch((e) => setError(message(e))).finally(() => setSaving(false));
1692
+ }, DEBOUNCE_MS);
1693
+ }, [client]);
1694
+ const value = React2__default.useMemo(() => ({ ...defaults, ...stored }), [defaults, stored]);
1695
+ return { value, set, loading, saving, error, loadError, supported };
1696
+ }
1697
+ var MARKETPLACE_CATEGORIES = ["marketplace"];
1698
+ function parseAmount(text) {
1699
+ const clean = text.replace(/\s/g, "").replace(",", ".");
1700
+ if (!clean) return null;
1701
+ const n = Number(clean);
1702
+ return Number.isFinite(n) && n >= 0 ? n : NaN;
1703
+ }
1704
+ function amountText(value) {
1705
+ if (value === null || value === void 0) return "";
1706
+ return String(value).replace(".", ",");
1707
+ }
1708
+ function AmountRow({ label, description, hint, value, onCommit }) {
1709
+ const [text, setText] = React2__default.useState(() => amountText(value));
1710
+ const [invalid, setInvalid] = React2__default.useState(false);
1711
+ React2__default.useEffect(() => {
1712
+ setText(amountText(value));
1713
+ }, [value]);
1714
+ return /* @__PURE__ */ jsx(
1715
+ TextRow,
1716
+ {
1717
+ label,
1718
+ description,
1719
+ value: text,
1720
+ onChange: (next) => {
1721
+ setText(next);
1722
+ setInvalid(false);
1723
+ },
1724
+ onBlurCommit: () => {
1725
+ const parsed = parseAmount(text);
1726
+ if (Number.isNaN(parsed)) {
1727
+ setInvalid(true);
1728
+ return;
1729
+ }
1730
+ setInvalid(false);
1731
+ onCommit(parsed);
1732
+ },
1733
+ hint: invalid ? "Use um n\xFAmero, como 9,90." : hint,
1734
+ invalid,
1735
+ placeholder: "0,00"
1736
+ }
1737
+ );
1738
+ }
1739
+ function DeliverySettings() {
1740
+ const settings = useDeliverySettings({ enabled: true, fee: 0, minOrder: 0, etaMinutes: 45, radiusKm: 0 });
1741
+ const { value, set } = settings;
1742
+ const { connectors } = useConnectors({ hostPluginId: "orders", includeGrants: false });
1743
+ const [adding, setAdding] = React2__default.useState(false);
1744
+ const [selected, setSelected] = React2__default.useState(null);
1745
+ const panelAvailable = useConnectorsPanelAvailable();
1746
+ const openJourney = React2__default.useCallback((connector) => {
1747
+ if (panelAvailable) openConnectorsPanel(connector);
1748
+ else setSelected(connector);
1749
+ }, [panelAvailable]);
1750
+ const marketplaces = React2__default.useMemo(
1751
+ () => (connectors ?? []).filter((c) => c.category && MARKETPLACE_CATEGORIES.includes(c.category)),
1752
+ [connectors]
1753
+ );
1754
+ const patch = (next) => set(next);
1755
+ if (!settings.supported) {
1756
+ return /* @__PURE__ */ jsxs("div", { className: "rounded-lg border border-amber-500/30 bg-amber-500/5 p-4 text-sm", children: [
1757
+ /* @__PURE__ */ jsxs("p", { className: "flex items-center gap-2 font-medium text-amber-700 dark:text-amber-500", children: [
1758
+ /* @__PURE__ */ jsx(AlertTriangle, { className: "h-4 w-4 shrink-0" }),
1759
+ "Este app n\xE3o guarda a configura\xE7\xE3o de entrega"
1760
+ ] }),
1761
+ /* @__PURE__ */ jsx("p", { className: "mt-1 text-muted-foreground", children: "N\xE3o h\xE1 backend montado aqui, ent\xE3o nada abaixo seria salvo \u2014 e por isso n\xE3o \xE9 mostrado." })
1762
+ ] });
1763
+ }
1764
+ if (settings.loading) {
1765
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
1766
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-28 w-full rounded-lg" }),
1767
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-40 w-full rounded-lg" })
1768
+ ] });
1769
+ }
1770
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
1771
+ settings.loadError && /* @__PURE__ */ jsxs("div", { className: "rounded-lg border border-destructive/30 bg-destructive/5 p-3 text-sm", children: [
1772
+ /* @__PURE__ */ jsxs("p", { className: "flex items-center gap-2 font-medium text-destructive", children: [
1773
+ /* @__PURE__ */ jsx(AlertTriangle, { className: "h-4 w-4 shrink-0" }),
1774
+ "N\xE3o foi poss\xEDvel carregar a configura\xE7\xE3o de entrega"
1775
+ ] }),
1776
+ /* @__PURE__ */ jsx("p", { className: "mt-1 text-muted-foreground", children: "O que aparece abaixo s\xE3o os valores padr\xE3o, n\xE3o as suas escolhas. Recarregue a p\xE1gina antes de alterar algo \u2014 salvar agora sobrescreveria o que n\xE3o conseguimos ler." }),
1777
+ /* @__PURE__ */ jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: settings.loadError })
1778
+ ] }),
1779
+ /* @__PURE__ */ jsxs(
1780
+ SettingsGroup,
1781
+ {
1782
+ title: "Entrega",
1783
+ description: "Vale para o pedido que sai da casa. O quadro de despacho continua existindo com a entrega desligada \u2014 o hist\xF3rico n\xE3o some.",
1784
+ children: [
1785
+ /* @__PURE__ */ jsx(
1786
+ ToggleRow,
1787
+ {
1788
+ label: "A casa entrega",
1789
+ description: "Desligado, nenhum canal novo nasce como entrega.",
1790
+ checked: value.enabled !== false,
1791
+ onChange: (checked) => patch({ enabled: checked })
1792
+ }
1793
+ ),
1794
+ /* @__PURE__ */ jsx(
1795
+ AmountRow,
1796
+ {
1797
+ label: "Taxa de entrega",
1798
+ description: "Cobrada por pedido, na moeda da casa.",
1799
+ hint: "Zero = entrega sem taxa.",
1800
+ value: value.fee,
1801
+ onCommit: (next) => patch({ fee: next ?? 0 })
1802
+ }
1803
+ ),
1804
+ /* @__PURE__ */ jsx(
1805
+ AmountRow,
1806
+ {
1807
+ label: "Frete gr\xE1tis acima de",
1808
+ description: "Acima deste valor de pedido a taxa \xE9 zerada.",
1809
+ hint: "Vazio = a taxa vale sempre.",
1810
+ value: value.freeOver,
1811
+ onCommit: (next) => patch({ freeOver: next })
1812
+ }
1813
+ ),
1814
+ /* @__PURE__ */ jsx(
1815
+ AmountRow,
1816
+ {
1817
+ label: "Pedido m\xEDnimo",
1818
+ description: "Abaixo disso a casa n\xE3o entrega.",
1819
+ hint: "Zero = sem m\xEDnimo.",
1820
+ value: value.minOrder,
1821
+ onCommit: (next) => patch({ minOrder: next ?? 0 })
1822
+ }
1823
+ )
1824
+ ]
1825
+ }
1826
+ ),
1827
+ /* @__PURE__ */ jsxs(
1828
+ SettingsGroup,
1829
+ {
1830
+ title: "At\xE9 onde, e em quanto tempo",
1831
+ description: "O que o cliente ouve quando pergunta \u2014 e o que o despacho promete.",
1832
+ children: [
1833
+ /* @__PURE__ */ jsx(
1834
+ AmountRow,
1835
+ {
1836
+ label: "Tempo estimado (min)",
1837
+ description: "Da confirma\xE7\xE3o at\xE9 a porta.",
1838
+ value: value.etaMinutes,
1839
+ onCommit: (next) => patch({ etaMinutes: next === null ? 0 : Math.round(next) })
1840
+ }
1841
+ ),
1842
+ /* @__PURE__ */ jsx(
1843
+ AmountRow,
1844
+ {
1845
+ label: "Raio de entrega (km)",
1846
+ description: "Dist\xE2ncia m\xE1xima a partir da casa.",
1847
+ hint: "Zero = sem limite declarado.",
1848
+ value: value.radiusKm,
1849
+ onCommit: (next) => patch({ radiusKm: next ?? 0 })
1850
+ }
1851
+ )
1852
+ ]
1853
+ }
1854
+ ),
1855
+ /* @__PURE__ */ jsxs("div", { children: [
1856
+ /* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between gap-4 px-1 pb-1 pt-4", children: [
1857
+ /* @__PURE__ */ jsxs("div", { children: [
1858
+ /* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold", children: "Quem traz o pedido" }),
1859
+ /* @__PURE__ */ jsx("p", { className: "mt-0.5 text-[10px] text-muted-foreground", children: "Marketplaces conectados entram no quadro sozinhos, cada pedido marcado com a origem. A credencial vai para o cofre, nunca para o banco da casa." })
1860
+ ] }),
1861
+ /* @__PURE__ */ jsx(AddConnectorButton, { onClick: () => setAdding(true) })
1862
+ ] }),
1863
+ /* @__PURE__ */ jsx("div", { className: "rounded-card border bg-card shadow-sm", children: marketplaces.length === 0 ? /* @__PURE__ */ jsx("p", { className: "p-4 text-sm text-muted-foreground", children: "Nenhum marketplace dispon\xEDvel para este app ainda. A entrega pr\xF3pria funciona sem nenhum." }) : /* @__PURE__ */ jsx("ul", { className: "divide-y", children: marketplaces.map((connector) => /* @__PURE__ */ jsx(ConnectorRow, { connector, onOpen: openJourney }, connector.id)) }) })
1864
+ ] }),
1865
+ /* @__PURE__ */ jsx("div", { className: "flex min-h-5 items-center gap-1.5 px-1 text-xs text-muted-foreground", children: settings.error ? /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-1.5 text-destructive", children: [
1866
+ /* @__PURE__ */ jsx(AlertTriangle, { className: "h-3.5 w-3.5 shrink-0" }),
1867
+ "N\xE3o foi poss\xEDvel salvar: ",
1868
+ settings.error
1869
+ ] }) : settings.saving ? /* @__PURE__ */ jsxs(Fragment, { children: [
1870
+ /* @__PURE__ */ jsx(Loader2, { className: "h-3.5 w-3.5 shrink-0 animate-spin" }),
1871
+ "Salvando\u2026"
1872
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
1873
+ /* @__PURE__ */ jsx(Check, { className: "h-3.5 w-3.5 shrink-0" }),
1874
+ "As altera\xE7\xF5es s\xE3o salvas automaticamente."
1875
+ ] }) }),
1876
+ /* @__PURE__ */ jsx(
1877
+ AddConnectorPanel,
1878
+ {
1879
+ open: adding,
1880
+ onOpenChange: setAdding,
1881
+ connectors,
1882
+ categories: MARKETPLACE_CATEGORIES,
1883
+ onSelect: (connector) => {
1884
+ setAdding(false);
1885
+ openJourney(connector);
1886
+ }
1887
+ }
1888
+ ),
1889
+ /* @__PURE__ */ jsx(
1890
+ ConnectorDetail,
1891
+ {
1892
+ connector: selected,
1893
+ open: !!selected,
1894
+ onOpenChange: (open) => {
1895
+ if (!open) setSelected(null);
1896
+ }
1897
+ }
1898
+ )
1899
+ ] });
1900
+ }
1619
1901
  var DEFAULT_ORDERS_TABLE = "orders";
1620
1902
  var DEFAULT_ORDER_ITEMS_TABLE = "items";
1621
1903
  var ORDERS_METADATA_KEY = "fayzOrders";
@@ -2045,7 +2327,7 @@ function createFayzOrdersProvider(options = {}) {
2045
2327
  }
2046
2328
 
2047
2329
  // src/index.ts
2048
- var OrdersPage = React2__default.lazy(() => import('./OrdersPage-IKIIQALX.js').then((m) => ({ default: m.OrdersPage })));
2330
+ var OrdersPage = React2__default.lazy(() => import('./OrdersPage-ONJJMPY6.js').then((m) => ({ default: m.OrdersPage })));
2049
2331
  var DEFAULT_LABELS = {
2050
2332
  pageTitle: "Orders",
2051
2333
  kanban: "Orders",
@@ -2073,7 +2355,10 @@ function resolveConfig(options) {
2073
2355
  createRoute: options?.createRoute,
2074
2356
  editRoute: options?.editRoute,
2075
2357
  canEditOrder: options?.canEditOrder,
2076
- orderRoute: options?.orderRoute
2358
+ orderRoute: options?.orderRoute,
2359
+ floorView: options?.floorView !== false,
2360
+ deliveryRoute: options?.deliveryRoute,
2361
+ detailRail: options?.detailRail ?? "operate"
2077
2362
  };
2078
2363
  }
2079
2364
  function createOrdersPlugin(options) {
@@ -2083,11 +2368,19 @@ function createOrdersPlugin(options) {
2083
2368
  onOrderCompleted: config.onOrderCompleted,
2084
2369
  onOrderCancelled: config.onOrderCancelled
2085
2370
  });
2086
- const PageComponent = () => React2__default.createElement(
2371
+ const screen = (extra) => () => React2__default.createElement(
2087
2372
  React2__default.Suspense,
2088
2373
  { fallback: null },
2089
- React2__default.createElement(OrdersPage, { config, provider, store: store7, registries: ordersRegistries, variant: options?.variant ?? "restaurant" })
2374
+ React2__default.createElement(OrdersPage, {
2375
+ config: extra ? { ...config, ...extra } : config,
2376
+ provider,
2377
+ store: store7,
2378
+ registries: ordersRegistries,
2379
+ variant: options?.variant ?? "restaurant"
2380
+ })
2090
2381
  );
2382
+ const PageComponent = screen();
2383
+ const DeliveryRoute = screen({ pinnedView: "delivery" });
2091
2384
  const OrderDetailRoute = ({ id }) => id ? React2__default.createElement(OrderDetailPage, {
2092
2385
  orderId: id,
2093
2386
  backTo: "/orders",
@@ -2186,6 +2479,11 @@ function createOrdersPlugin(options) {
2186
2479
  component: PageComponent,
2187
2480
  permission: { feature: "orders", action: "read" }
2188
2481
  },
2482
+ ...options?.deliveryRoute ? [{
2483
+ path: options.deliveryRoute,
2484
+ component: DeliveryRoute,
2485
+ permission: { feature: "orders", action: "read" }
2486
+ }] : [],
2189
2487
  {
2190
2488
  // The page the rail's "Ver pedido completo" points at. It used to exist
2191
2489
  // only in plugin-shop, so the restaurant's link opened a blank screen —
@@ -2272,7 +2570,29 @@ function createOrdersPlugin(options) {
2272
2570
  })(),
2273
2571
  order: 22,
2274
2572
  permission: { feature: "orders", action: "read" }
2275
- }
2573
+ },
2574
+ // Entrega — política da casa (taxa, mínimo, tempo, raio) e os
2575
+ // marketplaces que trazem o pedido. Só existe onde o delivery existe: um
2576
+ // app sem `modules.delivery` não ganha uma tela para configurar um
2577
+ // serviço que não presta.
2578
+ ...config.modules.delivery ? [{
2579
+ id: "orders-delivery",
2580
+ label: "Delivery",
2581
+ icon: "Bike",
2582
+ component: (() => {
2583
+ const DeliverySettingsTab = () => React2__default.createElement(
2584
+ React2__default.Suspense,
2585
+ { fallback: null },
2586
+ React2__default.createElement(DeliverySettings)
2587
+ );
2588
+ DeliverySettingsTab.displayName = "DeliverySettingsTab";
2589
+ return DeliverySettingsTab;
2590
+ })(),
2591
+ order: 23,
2592
+ // Quanto a casa cobra para entregar é decisão do dono, não de quem
2593
+ // despacha: mesma régua da taxa de serviço em plugin-tables.
2594
+ permission: { feature: "orders", action: "edit" }
2595
+ }] : []
2276
2596
  ],
2277
2597
  locales: ordersLocales,
2278
2598
  // Offered regardless of `modules.delivery` — a takeout-only house still takes