@fayz-ai/plugin-inventory 0.12.3 → 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/context.d.ts CHANGED
@@ -20,6 +20,8 @@ export interface LocationOption {
20
20
  isHQ?: boolean;
21
21
  }
22
22
  export interface ResolvedInventoryConfig {
23
+ /** Ver `InventoryPluginOptions.moduleNav`. */
24
+ moduleNav?: 'rail' | 'tabs' | 'sidebar';
23
25
  modules: InventoryModules;
24
26
  labels: InventoryPluginLabels;
25
27
  currency: InventoryCurrency;
@@ -1 +1 @@
1
- {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAuB,cAAc,EAAE,KAAK,cAAc,EAAE,MAAM,gBAAgB,CAAA;AACzF,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAA;AACpD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAA;AACzD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAE/C,MAAM,WAAW,gBAAgB;IAC/B,iFAAiF;IACjF,QAAQ,EAAE,OAAO,CAAA;IACjB,OAAO,EAAE,OAAO,CAAA;IAChB,cAAc,EAAE,OAAO,CAAA;IACvB,aAAa,EAAE,OAAO,CAAA;CACvB;AAED,MAAM,MAAM,iBAAiB,GAAG,cAAc,CAAA;AAE9C,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,CAAC,EAAE,OAAO,CAAA;CACf;AAED,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,gBAAgB,CAAA;IACzB,MAAM,EAAE,qBAAqB,CAAA;IAC7B,QAAQ,EAAE,iBAAiB,CAAA;IAC3B,YAAY,EAAE,iBAAiB,EAAE,CAAA;IACjC,SAAS,EAAE,cAAc,EAAE,CAAA;CAC5B;AAID,eAAO,MAAM,wBAAwB;;;;;EAAsB,CAAA;AAC3D,eAAO,MAAM,kBAAkB,+BAAgB,CAAA;AAC/C,eAAO,MAAM,oBAAoB,6BAAkB,CAAA;AACnD,eAAO,MAAM,iBAAiB,oDAAe,CAAA;AAE7C,OAAO,EAAE,cAAc,EAAE,CAAA"}
1
+ {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAuB,cAAc,EAAE,KAAK,cAAc,EAAE,MAAM,gBAAgB,CAAA;AACzF,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAA;AACpD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAA;AACzD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAE/C,MAAM,WAAW,gBAAgB;IAC/B,iFAAiF;IACjF,QAAQ,EAAE,OAAO,CAAA;IACjB,OAAO,EAAE,OAAO,CAAA;IAChB,cAAc,EAAE,OAAO,CAAA;IACvB,aAAa,EAAE,OAAO,CAAA;CACvB;AAED,MAAM,MAAM,iBAAiB,GAAG,cAAc,CAAA;AAE9C,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,CAAC,EAAE,OAAO,CAAA;CACf;AAED,MAAM,WAAW,uBAAuB;IACtC,8CAA8C;IAC9C,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAA;IACvC,OAAO,EAAE,gBAAgB,CAAA;IACzB,MAAM,EAAE,qBAAqB,CAAA;IAC7B,QAAQ,EAAE,iBAAiB,CAAA;IAC3B,YAAY,EAAE,iBAAiB,EAAE,CAAA;IACjC,SAAS,EAAE,cAAc,EAAE,CAAA;CAC5B;AAID,eAAO,MAAM,wBAAwB;;;;;EAAsB,CAAA;AAC3D,eAAO,MAAM,kBAAkB,+BAAgB,CAAA;AAC/C,eAAO,MAAM,oBAAoB,6BAAkB,CAAA;AACnD,eAAO,MAAM,iBAAiB,oDAAe,CAAA;AAE7C,OAAO,EAAE,cAAc,EAAE,CAAA"}
package/dist/index.d.ts CHANGED
@@ -42,6 +42,18 @@ export interface InventoryPluginOptions {
42
42
  navPosition?: number;
43
43
  navSection?: 'main' | 'secondary';
44
44
  scope?: PluginScope;
45
+ /**
46
+ * Como o Estoque desenha a PRÓPRIA navegação (Produtos, Movimentações,
47
+ * Contagens, Receitas).
48
+ *
49
+ * O padrão vem do shell, e num app cujo default é `sidebar` esse padrão diz
50
+ * "o rail lista as minhas sub-telas" — promessa que ninguém cumpre por este
51
+ * módulo: no rail ele é UMA linha, e o que tem dentro é tela irmã, não
52
+ * assunto novo. O resultado era o Estoque abrindo sem faixa de abas nenhuma.
53
+ *
54
+ * Mesma porta que `crm`, `marketing` e `financial` já usam.
55
+ */
56
+ moduleNav?: 'rail' | 'tabs' | 'sidebar';
45
57
  verticalId?: VerticalId;
46
58
  dataProvider?: InventoryDataProvider;
47
59
  locations?: Array<{
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;AAI5E,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAA;AAkBzD,MAAM,WAAW,qBAAqB;IACpC,SAAS,EAAE,MAAM,CAAA;IACjB,YAAY,EAAE,MAAM,CAAA;IACpB,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;IACnB,YAAY,EAAE,MAAM,CAAA;IACpB,KAAK,EAAE,MAAM,CAAA;IACb,UAAU,EAAE,MAAM,CAAA;IAClB,SAAS,EAAE,MAAM,CAAA;IACjB,YAAY,EAAE,MAAM,CAAA;IACpB,OAAO,EAAE,MAAM,CAAA;IACf,UAAU,EAAE,MAAM,CAAA;IAClB,WAAW,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,sBAAsB;IACrC,OAAO,CAAC,EAAE;QACR,OAAO,CAAC,EAAE,OAAO,CAAA;QACjB,cAAc,CAAC,EAAE,OAAO,CAAA;QACxB,aAAa,CAAC,EAAE,OAAO,CAAA;QACvB;;;;;;WAMG;QACH,QAAQ,CAAC,EAAE,OAAO,CAAA;KACnB,CAAA;IACD,MAAM,CAAC,EAAE,OAAO,CAAC,qBAAqB,CAAC,CAAA;IACvC,YAAY,CAAC,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IACtD,QAAQ,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IAC9D,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,UAAU,CAAC,EAAE,MAAM,GAAG,WAAW,CAAA;IACjC,KAAK,CAAC,EAAE,WAAW,CAAA;IACnB,UAAU,CAAC,EAAE,UAAU,CAAA;IACvB,YAAY,CAAC,EAAE,qBAAqB,CAAA;IACpC,SAAS,CAAC,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC,CAAA;CAChE;AAsDD,wFAAwF;AACxF,wBAAgB,2BAA2B,IAAI,qBAAqB,CAKnE;AAED,wBAAgB,qBAAqB,CAAC,OAAO,CAAC,EAAE,sBAAsB,GAAG,cAAc,CAwLtF;AAED,YAAY,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAA;AACzD,YAAY,EAAE,uBAAuB,EAAE,MAAM,WAAW,CAAA;AAIxD,OAAO,EACL,cAAc,EACd,oBAAoB,EACpB,uBAAuB,EACvB,gBAAgB,EAChB,qBAAqB,GACtB,MAAM,2BAA2B,CAAA;AAClC,YAAY,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAA;AAGjE,YAAY,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAC3C,YAAY,EACV,2BAA2B,EAC3B,6BAA6B,EAC7B,0BAA0B,EAC1B,0BAA0B,EAC1B,8BAA8B,GAC/B,MAAM,SAAS,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;AAI5E,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAA;AAkBzD,MAAM,WAAW,qBAAqB;IACpC,SAAS,EAAE,MAAM,CAAA;IACjB,YAAY,EAAE,MAAM,CAAA;IACpB,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;IACnB,YAAY,EAAE,MAAM,CAAA;IACpB,KAAK,EAAE,MAAM,CAAA;IACb,UAAU,EAAE,MAAM,CAAA;IAClB,SAAS,EAAE,MAAM,CAAA;IACjB,YAAY,EAAE,MAAM,CAAA;IACpB,OAAO,EAAE,MAAM,CAAA;IACf,UAAU,EAAE,MAAM,CAAA;IAClB,WAAW,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,sBAAsB;IACrC,OAAO,CAAC,EAAE;QACR,OAAO,CAAC,EAAE,OAAO,CAAA;QACjB,cAAc,CAAC,EAAE,OAAO,CAAA;QACxB,aAAa,CAAC,EAAE,OAAO,CAAA;QACvB;;;;;;WAMG;QACH,QAAQ,CAAC,EAAE,OAAO,CAAA;KACnB,CAAA;IACD,MAAM,CAAC,EAAE,OAAO,CAAC,qBAAqB,CAAC,CAAA;IACvC,YAAY,CAAC,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IACtD,QAAQ,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IAC9D,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,UAAU,CAAC,EAAE,MAAM,GAAG,WAAW,CAAA;IACjC,KAAK,CAAC,EAAE,WAAW,CAAA;IACnB;;;;;;;;;;OAUG;IACH,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAA;IACvC,UAAU,CAAC,EAAE,UAAU,CAAA;IACvB,YAAY,CAAC,EAAE,qBAAqB,CAAA;IACpC,SAAS,CAAC,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC,CAAA;CAChE;AAuDD,wFAAwF;AACxF,wBAAgB,2BAA2B,IAAI,qBAAqB,CAKnE;AAED,wBAAgB,qBAAqB,CAAC,OAAO,CAAC,EAAE,sBAAsB,GAAG,cAAc,CAgMtF;AAED,YAAY,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAA;AACzD,YAAY,EAAE,uBAAuB,EAAE,MAAM,WAAW,CAAA;AAIxD,OAAO,EACL,cAAc,EACd,oBAAoB,EACpB,uBAAuB,EACvB,gBAAgB,EAChB,qBAAqB,GACtB,MAAM,2BAA2B,CAAA;AAClC,YAAY,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAA;AAGjE,YAAY,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAC3C,YAAY,EACV,2BAA2B,EAC3B,6BAA6B,EAC7B,0BAA0B,EAC1B,0BAA0B,EAC1B,8BAA8B,GAC/B,MAAM,SAAS,CAAA"}
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import React3, { useState, useMemo, useEffect, useCallback, useRef } from 'react';
2
- import { toast, defineKpiWidget, defineCustomWidget, PageTransition, ModulePage, Button, SubpageHeader, SearchSelect, cn, DatePicker, ListView, ConfirmDialog, Badge, DraftCommitBar, Switch, useSaveBar, DataTable, DashboardCanvas, Modal, ModalContent, ModalHeader, ModalTitle, ModalDescription, ModalBody, ModalFooter, StockEntryForm, KpiCard, Card, CardHeader, CardTitle, CardContent, Skeleton } from '@fayz-ai/ui';
3
- import { createPluginContext, PluginSettingsPanel, dedup, useModuleNavigation, createViewRouter, ModuleActionBar, useTenantPluginSettings, SettingsGroup, ToggleRow, usePermissionOptional, CrudListView, TeachMeLink, useLimitGuard, invalidateRelationOptions, CrudFormSkeleton, CrudFormPage, CrudDetailPage, formatCurrency, useAgentSurface, askAI, PermissionGate, QuickActionsButton, invalidateLimit } from '@fayz-ai/admin';
2
+ import { toast, defineKpiWidget, defineCustomWidget, PageTransition, ModulePage, Button, SubpageHeader, SearchSelect, cn, DatePicker, ListView, ConfirmDialog, Badge, DraftCommitBar, Switch, useSaveBar, DataTable, DashboardCanvas, Thumbnail, Modal, ModalContent, ModalHeader, ModalTitle, ModalDescription, ModalBody, ModalFooter, StockEntryForm, KpiCard, Card, CardHeader, CardTitle, CardContent, Skeleton } from '@fayz-ai/ui';
3
+ import { createPluginContext, PluginSettingsPanel, dedup, useModuleNavigation, createViewRouter, ModuleActionBar, useTenantPluginSettings, SettingsGroup, ToggleRow, usePermissionOptional, CrudListView, useLimitGuard, invalidateRelationOptions, CrudFormSkeleton, CrudFormPage, CrudDetailPage, formatCurrency, useAgentSurface, askAI, PermissionGate, QuickActionsButton, invalidateLimit } from '@fayz-ai/admin';
4
4
  import { createSafeDataProvider, registerTranslations, countByTenant, useTranslation, getActiveUnitId, createPendingCommand, errorMessage, useDataChanged, getSupabaseClientOptional, getActiveTenantId } from '@fayz-ai/core';
5
5
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
6
6
  import { Trash2, ArrowRightLeft, RefreshCw, ArrowUpRight, ArrowDownRight, Package, Ruler, MapPin, Loader2, Check, ChevronRight, CircleSlash, CircleCheck, ClipboardCheck, AlertTriangle, Plus, BookOpen, Layers, Clock, Coins, Building2, Undo2, PackageX } from 'lucide-react';
@@ -361,13 +361,26 @@ function buildProductEntity(t2, productTypes, currency, onStockChanged, provider
361
361
  searchable: true,
362
362
  showInTable: true,
363
363
  sortable: true,
364
- renderCell: (_v, row) => /* @__PURE__ */ jsxs("div", { children: [
365
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5", children: [
366
- /* @__PURE__ */ jsx("p", { className: `font-medium ${row.isActive === false ? "text-muted-foreground" : ""}`, children: row.name }),
367
- row.isActive === false && /* @__PURE__ */ jsx("span", { className: "inline-flex items-center rounded-full bg-muted px-1.5 py-0.5 text-[9px] font-medium text-muted-foreground", children: t2("inventory.productList.inactive") })
368
- ] }),
369
- row.sku && /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: row.sku })
370
- ] })
364
+ renderCell: (_v, row) => (
365
+ // A foto vem junto do nome, como na lista da loja. O produto de um
366
+ // estoque é uma COISA, e a pessoa que confere a prateleira reconhece a
367
+ // embalagem antes de ler o SKU. `imageUrl` viajava na linha não
368
+ // tinha quem o desenhasse.
369
+ //
370
+ // O quadrado aparece SEMPRE, com foto ou sem: um produto sem imagem
371
+ // que não renderizasse nada puxaria o nome para trás contra as linhas
372
+ // que têm, e a coluna deixaria de ler como coluna.
373
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
374
+ /* @__PURE__ */ jsx(Thumbnail, { src: row.imageUrl, icon: Package }),
375
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
376
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5", children: [
377
+ /* @__PURE__ */ jsx("p", { className: `truncate font-medium ${row.isActive === false ? "text-muted-foreground" : ""}`, children: row.name }),
378
+ row.isActive === false && /* @__PURE__ */ jsx("span", { className: "inline-flex items-center rounded-full bg-muted px-1.5 py-0.5 text-[9px] font-medium text-muted-foreground", children: t2("inventory.productList.inactive") })
379
+ ] }),
380
+ row.sku && /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: row.sku })
381
+ ] })
382
+ ] })
383
+ )
371
384
  },
372
385
  { key: "brand", label: t2("inventory.productForm.brand"), type: "text", group: "general", placeholder: t2("inventory.productForm.brandPlaceholder"), showInTable: false },
373
386
  { key: "sku", label: t2("inventory.productForm.sku"), type: "text", group: "general", placeholder: t2("inventory.productForm.skuPlaceholder"), searchable: true, showInTable: false },
@@ -664,14 +677,6 @@ function ProductListView({ onNew, onOpen }) {
664
677
  return /* @__PURE__ */ jsx(
665
678
  CrudListView,
666
679
  {
667
- headerActions: /* @__PURE__ */ jsx(
668
- TeachMeLink,
669
- {
670
- feature: "inventory.produtos",
671
- title: "Produtos",
672
- about: "O cat\xE1logo do estoque. O tipo de produto \xE9 a divis\xE3o prim\xE1ria, e o estoque m\xEDnimo \xE9 o que faz o alerta de reposi\xE7\xE3o significar alguma coisa \u2014 com m\xEDnimo zero, tudo vira urgente."
673
- }
674
- ),
675
680
  entityDef: entity,
676
681
  items: loadedOnce ? products2 : null,
677
682
  total: productsTotal,
@@ -1607,14 +1612,6 @@ function StockCountsView({ onNew, onOpen }) {
1607
1612
  return /* @__PURE__ */ jsx(
1608
1613
  CrudListView,
1609
1614
  {
1610
- headerActions: /* @__PURE__ */ jsx(
1611
- TeachMeLink,
1612
- {
1613
- feature: "inventory.contagem",
1614
- title: "Contagem de Estoque",
1615
- about: "A contagem f\xEDsica contra o saldo do sistema. A diferen\xE7a vira ajuste com rastro: sobra ou falta, com o n\xFAmero do invent\xE1rio."
1616
- }
1617
- ),
1618
1615
  entityDef: entity,
1619
1616
  items: loadedOnce ? rows : null,
1620
1617
  total: rows.length,
@@ -2267,17 +2264,7 @@ function RecipesView({ onNew, onView, onNewForProduct }) {
2267
2264
  search,
2268
2265
  onSearchChange: setSearch,
2269
2266
  searchPlaceholder: t2("inventory.recipes.searchPlaceholder"),
2270
- headerActions: /* @__PURE__ */ jsxs(Fragment, { children: [
2271
- /* @__PURE__ */ jsx(
2272
- TeachMeLink,
2273
- {
2274
- feature: "inventory.receitas",
2275
- title: "Receitas / Ficha T\xE9cnica",
2276
- about: "O que cada item consome. \xC9 a ficha t\xE9cnica que permite a venda baixar o insumo sozinha."
2277
- }
2278
- ),
2279
- /* @__PURE__ */ jsx(PermissionGate, { feature: "inventory", action: "create", children: /* @__PURE__ */ jsx(QuickActionsButton, { variant: "default", label: t2("inventory.recipes.newRecipe"), actions: createActions }) })
2280
- ] }),
2267
+ headerActions: /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsx(PermissionGate, { feature: "inventory", action: "create", children: /* @__PURE__ */ jsx(QuickActionsButton, { variant: "default", label: t2("inventory.recipes.newRecipe"), actions: createActions }) }) }),
2281
2268
  onRowClick: (row) => {
2282
2269
  const target = row;
2283
2270
  if (target.missing && target.productId) onNewForProduct?.(target.productId);
@@ -3153,15 +3140,7 @@ function UsageAuditView() {
3153
3140
  SubpageHeader,
3154
3141
  {
3155
3142
  title: t2("inventory.usageAudit.title"),
3156
- subtitle: t2("inventory.usageAudit.subtitle"),
3157
- actions: /* @__PURE__ */ jsx(
3158
- TeachMeLink,
3159
- {
3160
- feature: "inventory.auditoria-de-uso",
3161
- title: "Auditoria de Uso",
3162
- about: "O que os atendimentos consumiram de verdade, lado a lado com o que era o padr\xE3o. \xC9 depois da baixa: o atendimento j\xE1 consumiu, e corrigir lan\xE7a a diferen\xE7a no raz\xE3o. Digite na pr\xF3pria linha e aplique tudo de uma vez, com uma observa\xE7\xE3o s\xF3."
3163
- }
3164
- )
3143
+ subtitle: t2("inventory.usageAudit.subtitle")
3165
3144
  }
3166
3145
  ),
3167
3146
  /* @__PURE__ */ jsx(
@@ -3462,6 +3441,7 @@ function InventoryPage({ config, provider, store, registries }) {
3462
3441
  return /* @__PURE__ */ jsx(InventoryContextProvider, { config, provider, store, children: /* @__PURE__ */ jsx(
3463
3442
  ModulePage,
3464
3443
  {
3444
+ navVariant: config.moduleNav,
3465
3445
  title: config.labels.pageTitle,
3466
3446
  subtitle: config.labels.pageSubtitle,
3467
3447
  nav,
@@ -11143,11 +11123,271 @@ BEGIN
11143
11123
  END;
11144
11124
  $$;
11145
11125
  `;
11126
+ var MIGRATION_004_A_POSICAO_DE_ESTOQUE_ATRAVESSA = `-- A posi\xE7\xE3o de estoque atravessa.
11127
+ --
11128
+ -- \`stock_positions\` \u2014 o saldo por produto \xD7 local \xD7 lote \u2014 estava no plano de
11129
+ -- leitura e sem destino: 1.861 linhas do tenant Pertinho do C\xE9u ficaram
11130
+ -- \`pendentes\` em toda rodada, nunca ofertadas a writer nenhum. A nota no
11131
+ -- extrator dizia "no V2 a posi\xE7\xE3o \xE9 derivada do movimento", e essa frase \xE9
11132
+ -- verdadeira e insuficiente.
11133
+ --
11134
+ -- Derivar o saldo do movimento s\xF3 d\xE1 o mesmo n\xFAmero quando o movimento
11135
+ -- atravessa INTEIRO. Neste tenant 2.208 dos 2.697 movimentos estavam em
11136
+ -- quarentena, ent\xE3o o saldo derivado seria o saldo de 18% do hist\xF3rico \u2014
11137
+ -- apresentado com a mesma confian\xE7a do saldo certo. E lote e validade, que a
11138
+ -- posi\xE7\xE3o carrega e o movimento n\xE3o, n\xE3o se derivam de soma nenhuma.
11139
+ --
11140
+ -- Ent\xE3o a posi\xE7\xE3o vem do V1 como fato, e n\xE3o como conta.
11141
+ --
11142
+ -- \u2500\u2500 O que o destino exige e a origem n\xE3o d\xE1 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
11143
+ --
11144
+ -- \`plg_inventory_stock_positions\` tem tr\xEAs colunas NOT NULL que precisam de
11145
+ -- tradu\xE7\xE3o:
11146
+ --
11147
+ -- product_id \u2192 resolvido pelo ledger de \`products\`
11148
+ -- stock_location_id \u2192 pelo ledger de \`stock_locations\`
11149
+ -- unit_id \u2192 a unidade da EMPRESA (app.units), e o V1 guarda duas
11150
+ -- coisas com nome parecido: \`unit_id\` (a unidade de
11151
+ -- MEDIDA do item) e \`company_unit_id\` (a filial). \xC9 a
11152
+ -- segunda. Confundi-las poria a posi\xE7\xE3o na unidade
11153
+ -- errada \u2014 e num tenant de 63 filiais isso \xE9 saldo no
11154
+ -- lugar de outro.
11155
+ --
11156
+ -- Quando a filial n\xE3o vem na linha, ela vem do LOCAL: um local de estoque
11157
+ -- pertence a uma unidade, e o saldo que est\xE1 nele est\xE1 nela.
11158
+ --
11159
+ -- Idempotente.
11160
+
11161
+ -- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
11162
+ -- 1. O writer
11163
+ -- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
11164
+
11165
+ CREATE OR REPLACE FUNCTION migration.upsert_inv_position(
11166
+ p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb
11167
+ ) RETURNS jsonb
11168
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path TO ''
11169
+ AS $function$
11170
+ DECLARE
11171
+ v_meta jsonb := coalesce(p_row -> 'metadata', '{}'::jsonb);
11172
+ v_product uuid := (p_row ->> 'product_id')::uuid;
11173
+ v_location uuid := (p_row ->> 'stock_location_id')::uuid;
11174
+ v_unit uuid := (p_row ->> 'unit_id')::uuid;
11175
+ v_qty numeric := round(coalesce((p_row ->> 'quantity')::numeric, 0), 4);
11176
+ v_existing_tenant uuid;
11177
+ v_created boolean := false;
11178
+ v_standalone boolean := migration._standalone();
11179
+ BEGIN
11180
+ IF v_standalone THEN PERFORM migration._assert_fence(p_tenant); PERFORM set_config('migration.writer', 'on', true); END IF;
11181
+ IF p_id IS NULL THEN
11182
+ RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state', 'reason', 'uma posi\xE7\xE3o precisa de id');
11183
+ END IF;
11184
+ IF v_product IS NULL THEN
11185
+ RETURN jsonb_build_object('status', 'quarantined', 'kind', 'fk',
11186
+ 'reason', 'posi\xE7\xE3o de saldo cujo produto ainda n\xE3o atravessou');
11187
+ END IF;
11188
+ IF v_location IS NULL THEN
11189
+ RETURN jsonb_build_object('status', 'quarantined', 'kind', 'fk',
11190
+ 'reason', 'posi\xE7\xE3o de saldo sem local \u2014 saldo sem lugar n\xE3o \xE9 saldo');
11191
+ END IF;
11192
+
11193
+ -- A filial pelo LOCAL quando a linha n\xE3o a traz: o local pertence a uma
11194
+ -- unidade, e o saldo que est\xE1 nele est\xE1 nela.
11195
+ IF v_unit IS NULL THEN
11196
+ SELECT l.unit_id INTO v_unit
11197
+ FROM public.plg_inventory_stock_locations l
11198
+ WHERE l.id = v_location AND l.tenant_id = p_tenant;
11199
+ END IF;
11200
+ IF v_unit IS NULL THEN
11201
+ RETURN jsonb_build_object('status', 'quarantined', 'kind', 'fk',
11202
+ 'reason', 'posi\xE7\xE3o sem unidade, e o local dela tamb\xE9m n\xE3o diz de qual \xE9');
11203
+ END IF;
11204
+
11205
+ -- \`quantity >= 0\` \xE9 CHECK da tabela. O V1 admite saldo negativo (sa\xEDda que
11206
+ -- n\xE3o conferiu com a entrada); recusar aqui, com a frase, \xE9 melhor que o erro
11207
+ -- cru do banco no meio do lote \u2014 e melhor que gravar zero, que apagaria a
11208
+ -- evid\xEAncia de que a contagem daquele item est\xE1 furada.
11209
+ IF v_qty < 0 THEN
11210
+ RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value',
11211
+ 'reason', format('saldo negativo (%s) \u2014 o V1 admite, o raz\xE3o de estoque do V2 n\xE3o', v_qty));
11212
+ END IF;
11213
+
11214
+ SELECT tenant_id INTO v_existing_tenant FROM public.plg_inventory_stock_positions WHERE id = p_id;
11215
+ IF v_existing_tenant IS NOT NULL AND v_existing_tenant <> p_tenant THEN
11216
+ RETURN jsonb_build_object('status', 'quarantined', 'kind', 'tenant',
11217
+ 'reason', format('%s already exists in another tenant', p_id));
11218
+ END IF;
11219
+
11220
+ IF v_existing_tenant IS NULL THEN
11221
+ INSERT INTO public.plg_inventory_stock_positions
11222
+ (id, tenant_id, unit_id, product_id, stock_location_id, quantity, unit_cost,
11223
+ batch_number, expiration_date, unit_type)
11224
+ VALUES (p_id, p_tenant, v_unit, v_product, v_location, v_qty,
11225
+ round(coalesce((p_row ->> 'unit_cost')::numeric, 0), 4),
11226
+ p_row ->> 'batch_number', (p_row ->> 'expiration_date')::date,
11227
+ coalesce(nullif(btrim(p_row ->> 'unit_type'), ''), 'base'));
11228
+ v_created := true;
11229
+ ELSE
11230
+ UPDATE public.plg_inventory_stock_positions SET
11231
+ unit_id = coalesce(v_unit, unit_id),
11232
+ product_id = coalesce(v_product, product_id),
11233
+ stock_location_id = coalesce(v_location, stock_location_id),
11234
+ quantity = CASE WHEN p_row ? 'quantity' THEN v_qty ELSE quantity END,
11235
+ unit_cost = CASE WHEN p_row ? 'unit_cost' THEN round(coalesce((p_row ->> 'unit_cost')::numeric, 0), 4) ELSE unit_cost END,
11236
+ batch_number = CASE WHEN p_row ? 'batch_number' THEN p_row ->> 'batch_number' ELSE batch_number END,
11237
+ expiration_date = CASE WHEN p_row ? 'expiration_date' THEN (p_row ->> 'expiration_date')::date ELSE expiration_date END,
11238
+ unit_type = CASE WHEN p_row ? 'unit_type' THEN coalesce(nullif(btrim(p_row ->> 'unit_type'), ''), 'base') ELSE unit_type END,
11239
+ updated_at = now()
11240
+ WHERE id = p_id;
11241
+ END IF;
11242
+
11243
+ IF v_standalone THEN PERFORM migration._audit(p_tenant, 'public.plg_inventory_stock_positions', p_id, jsonb_build_object('reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END)); END IF;
11244
+ RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id, 'reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END);
11245
+ END $function$;
11246
+
11247
+ -- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
11248
+ -- 2. O vocabul\xE1rio de writers aceita o nome novo
11249
+ -- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
11250
+
11251
+ DO $$
11252
+ DECLARE v_def text;
11253
+ BEGIN
11254
+ SELECT pg_get_constraintdef(oid) INTO v_def
11255
+ FROM pg_constraint WHERE conname = 'allowlist_writer_check';
11256
+ IF v_def IS NULL OR strpos(v_def, 'inv_position') > 0 THEN RETURN; END IF;
11257
+ ALTER TABLE migration.allowlist DROP CONSTRAINT allowlist_writer_check;
11258
+ EXECUTE replace(
11259
+ 'ALTER TABLE migration.allowlist ADD CONSTRAINT allowlist_writer_check ' || v_def,
11260
+ '''inv_movement''::text',
11261
+ '''inv_movement''::text, ''inv_position''::text');
11262
+ END $$;
11263
+
11264
+ -- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
11265
+ -- 3. O de-para
11266
+ -- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
11267
+ --
11268
+ -- \`unit_id\` do V1 \xE9 a unidade de MEDIDA e n\xE3o a filial \u2014 por isso ela vai para
11269
+ -- metadata e quem responde pela filial \xE9 \`company_unit_id\`.
11270
+
11271
+ -- A allowlist \xE9 CONGELADA (\`trg_allowlist_frozen\`): mudan\xE7a de core-A \xE9
11272
+ -- arquivo de migration, n\xE3o escrita solta \u2014 e esta \xE9 um arquivo. A escotilha
11273
+ -- \xE9 transacional (\`true\` no terceiro argumento), ent\xE3o n\xE3o fica porta aberta.
11274
+ SELECT set_config('migration.allowlist_unlock', 'on', true);
11275
+
11276
+ SELECT migration._allow(
11277
+ 'stock_positions', 'public.plg_inventory_stock_positions', 'inventory',
11278
+ 'inv_position', '{}'::jsonb, ARRAY['quantity', 'unit_cost'],
11279
+ jsonb_build_object(
11280
+ 'uuid', 'id',
11281
+ 'product_id', 'product_id@products',
11282
+ 'stock_location_id', 'stock_location_id@stock_locations',
11283
+ 'company_unit_id', 'unit_id@companies:app.units',
11284
+ 'quantity', 'quantity',
11285
+ 'unit_cost', 'unit_cost',
11286
+ 'batch_number', 'batch_number',
11287
+ 'expiration_date', 'expiration_date',
11288
+ 'unit_type', 'unit_type',
11289
+ 'location', 'jsonb:v1.location_label',
11290
+ 'unit_id', 'jsonb:v1.measurement_unit_ref'
11291
+ ),
11292
+ 'O SALDO por produto \xD7 local \xD7 lote. Vem da origem como FATO e n\xE3o derivado '
11293
+ 'do movimento: o saldo derivado s\xF3 bate quando o movimento atravessa inteiro, '
11294
+ 'e lote e validade a posi\xE7\xE3o carrega e o movimento n\xE3o.'
11295
+ );
11296
+
11297
+ SELECT set_config('migration.allowlist_unlock', 'off', true);
11298
+
11299
+
11300
+ -- \u2500\u2500 quem pode executar \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
11301
+ --
11302
+ -- Fun\xE7\xE3o nova nasce execut\xE1vel por PUBLIC, e \`anon\`/\`authenticated\` herdam
11303
+ -- dali. Estas s\xE3o de PORTE: SECURITY DEFINER, escrevem dados de qualquer
11304
+ -- inquilino, e uma delas alcan\xE7\xE1vel pela chave an\xF4nima \xE9 a base inteira
11305
+ -- alcan\xE7\xE1vel pela chave an\xF4nima. S\xF3 o service_role, que \xE9 quem roda a onda.
11306
+ REVOKE ALL ON FUNCTION migration.upsert_inv_position(uuid, uuid, jsonb, jsonb) FROM PUBLIC, anon, authenticated;
11307
+ GRANT EXECUTE ON FUNCTION migration.upsert_inv_position(uuid, uuid, jsonb, jsonb) TO service_role;
11308
+ `;
11309
+ var MIGRATION_005_A_POSICAO_E_CONSEQUENCIA_DO_MOVIMENTO = `-- A posi\xE7\xE3o \xE9 consequ\xEAncia do movimento.
11310
+ --
11311
+ -- Desfaz a migration 004, que estava errada \u2014 e o registro do porqu\xEA vale mais
11312
+ -- que o c\xF3digo que sai.
11313
+ --
11314
+ -- A 004 criou um writer para trazer \`stock_positions\` do V1 como FATO, com o
11315
+ -- argumento de que derivar o saldo do movimento s\xF3 d\xE1 o mesmo n\xFAmero quando o
11316
+ -- movimento atravessa inteiro. O argumento era bom e a premissa tinha deixado
11317
+ -- de valer no mesmo dia: os 2.697 movimentos passaram a atravessar inteiros
11318
+ -- assim que a entrada por DANFE ganhou o local (a ponte no extrator).
11319
+ --
11320
+ -- E o destino j\xE1 dizia isso, em voz alta:
11321
+ --
11322
+ -- plg_inventory_stock_positions is written only through the inventory RPCs
11323
+ -- (inventory_receive / _transfer \u2026)
11324
+ --
11325
+ -- 339 das 1.861 linhas bateram nesse gatilho. O gatilho est\xE1 certo: no V2 a
11326
+ -- posi\xE7\xE3o n\xE3o \xE9 algo que se escreve, \xE9 o que sobra depois que o movimento
11327
+ -- passou. Escrever por fora seria manter duas verdades sobre o mesmo saldo e
11328
+ -- deixar a diverg\xEAncia para depois.
11329
+ --
11330
+ -- \u2500\u2500 A prova \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
11331
+ --
11332
+ -- Medido em 19/09/2026 no tenant Pertinho do C\xE9u, depois de os movimentos
11333
+ -- entrarem:
11334
+ --
11335
+ -- V1 stock_positions 1.861 linhas 251.561,2484
11336
+ -- V2 plg_inventory_stock_positions 990 linhas 251.561,2484
11337
+ --
11338
+ -- O SALDO \xE9 o mesmo at\xE9 o quarto decimal. A contagem de linhas difere porque o
11339
+ -- V1 guarda uma posi\xE7\xE3o por lote/validade e o V2 consolida por produto \xD7 local
11340
+ -- quando n\xE3o h\xE1 lote \u2014 duas formas de escrever o mesmo estoque, e \xE9 o estoque
11341
+ -- que se confere.
11342
+ --
11343
+ -- \u2500\u2500 O que sai e o que fica \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
11344
+ --
11345
+ -- Sai a linha de \`stock_positions\` da allowlist. Fica \`migration.excluded_tables\`
11346
+ -- com o motivo medido, para que a pr\xF3xima pessoa que vir 1.861 linhas paradas
11347
+ -- no dashboard leia a resposta em vez de refazer a investiga\xE7\xE3o.
11348
+ --
11349
+ -- O writer \`upsert_inv_position\` FICA. Ele n\xE3o faz mal parado, e um pool que
11350
+ -- porte posi\xE7\xE3o sem portar movimento vai precisar dele \u2014 o que muda \xE9 que este
11351
+ -- n\xE3o \xE9 esse caso.
11352
+ --
11353
+ -- Idempotente.
11354
+
11355
+ -- A linha est\xE1 CONGELADA, e o gatilho recusa DELETE nela pelo mesmo motivo que
11356
+ -- recusa UPDATE: mudan\xE7a de core-A \xE9 arquivo de migration. Este \xE9 o arquivo, e
11357
+ -- a escotilha \xE9 transacional \u2014 n\xE3o fica porta aberta depois dele.
11358
+ SELECT set_config('migration.allowlist_unlock', 'on', true);
11359
+ DELETE FROM migration.allowlist WHERE source_table = 'stock_positions';
11360
+ SELECT set_config('migration.allowlist_unlock', 'off', true);
11361
+
11362
+ -- \`WHERE tenant_id IS NULL\` no \xE1rbitro: a 073 da espinha trocou a PK de
11363
+ -- \`migration.excluded_tables\` por dois \xEDndices \xFAnicos PARCIAIS \u2014 um global e
11364
+ -- um por inquilino \u2014, porque "n\xE3o trazer esta tabela" passou a ser decis\xE3o de
11365
+ -- cliente e n\xE3o do cluster. \xCDndice parcial s\xF3 serve de \xE1rbitro de ON CONFLICT
11366
+ -- quando o comando repete o mesmo recorte. Esta decis\xE3o \xE9 global.
11367
+ INSERT INTO migration.excluded_tables (source_table, reason)
11368
+ VALUES ('stock_positions',
11369
+ 'Consequ\xEAncia, n\xE3o fato: no V2 a posi\xE7\xE3o s\xF3 se escreve pelas RPCs de '
11370
+ 'invent\xE1rio (o gatilho recusa INSERT direto), e ela se forma sozinha '
11371
+ 'quando o movimento atravessa. Conferido em 19/09/2026 no Pertinho do '
11372
+ 'C\xE9u \u2014 V1 1.861 linhas / 251.561,2484 contra V2 990 linhas / '
11373
+ '251.561,2484: mesmo saldo, formas diferentes de agrupar (o V1 guarda '
11374
+ 'por lote, o V2 consolida sem lote).')
11375
+ -- \`WHERE tenant_id IS NULL\` porque a PK de \`excluded_tables\` deixou de existir:
11376
+ -- o spine 070 a trocou por dois \xEDndices \xFAnicos PARCIAIS \u2014 um por tabela global,
11377
+ -- outro por tabela e tenant. \`ON CONFLICT (source_table)\` sozinho n\xE3o casa com
11378
+ -- \xEDndice parcial, e a cadeia do zero parou aqui com "there is no unique or
11379
+ -- exclusion constraint matching the ON CONFLICT specification".
11380
+ --
11381
+ -- Esta linha \xE9 global (sem tenant), ent\xE3o \xE9 o \xEDndice global que ela quer.
11382
+ ON CONFLICT (source_table) WHERE tenant_id IS NULL DO UPDATE SET reason = EXCLUDED.reason;
11383
+ `;
11146
11384
  var MIGRATIONS = [
11147
11385
  { id: "000_baseline", sql: MIGRATION_000_BASELINE },
11148
11386
  { id: "001_the_usage_that_left_stock_can_be_audited", sql: MIGRATION_001_THE_USAGE_THAT_LEFT_STOCK_CAN_BE_AUDITED },
11149
11387
  { id: "002_a_auditoria_fecha_em_lote", sql: MIGRATION_002_A_AUDITORIA_FECHA_EM_LOTE },
11150
- { id: "003_stock_count_closes_through_ledger", sql: MIGRATION_003_STOCK_COUNT_CLOSES_THROUGH_LEDGER }
11388
+ { id: "003_stock_count_closes_through_ledger", sql: MIGRATION_003_STOCK_COUNT_CLOSES_THROUGH_LEDGER },
11389
+ { id: "004_a_posicao_de_estoque_atravessa", sql: MIGRATION_004_A_POSICAO_DE_ESTOQUE_ATRAVESSA },
11390
+ { id: "005_a_posicao_e_consequencia_do_movimento", sql: MIGRATION_005_A_POSICAO_E_CONSEQUENCIA_DO_MOVIMENTO }
11151
11391
  ];
11152
11392
 
11153
11393
  // src/index.ts
@@ -11184,7 +11424,8 @@ function resolveConfig(options) {
11184
11424
  labels: { ...DEFAULT_LABELS, ...options?.labels },
11185
11425
  currency: { ...DEFAULT_CURRENCY, ...options?.currency },
11186
11426
  productTypes: options?.productTypes ?? DEFAULT_PRODUCT_TYPES,
11187
- locations: options?.locations ?? []
11427
+ locations: options?.locations ?? [],
11428
+ moduleNav: options?.moduleNav
11188
11429
  };
11189
11430
  }
11190
11431
  function createInventoryDataProvider() {
@@ -11202,6 +11443,13 @@ function createInventoryPlugin(options) {
11202
11443
  const PageComponent = () => React3.createElement(InventoryPage, { config, provider, store, registries: inventoryRegistries });
11203
11444
  return {
11204
11445
  id: "inventory",
11446
+ // Como esta conta refaz o módulo com as opções DELA. Ver PluginRebuild:
11447
+ // a fábrica roda no carregamento do aplicativo, muito antes de a
11448
+ // configuração da conta existir, e sem isto `app.tenant_plugins.config`
11449
+ // não tinha como chegar aqui. A fusão é RASA e por bloco — a conta troca
11450
+ // `modules` inteiro ou não o troca —, que é o mesmo contrato que a chamada
11451
+ // do aplicativo já tem.
11452
+ rebuild: (tenantConfig) => createInventoryPlugin({ ...options, ...tenantConfig }),
11205
11453
  defaultAgentRole: "operations",
11206
11454
  name: config.labels.pageTitle,
11207
11455
  icon: "Package",