@fayz-ai/plugin-inventory 0.13.4 → 0.13.5

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
@@ -1,9 +1,9 @@
1
1
  import React3, { useMemo, useState, 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, Thumbnail, Modal, ModalContent, ModalHeader, ModalTitle, ModalDescription, ModalBody, ModalFooter, StockEntryForm, KpiCard, Card, CardHeader, CardTitle, CardContent, Skeleton } from '@fayz-ai/ui';
3
- import { createPluginContext, PluginSettingsPanel, dedup, useTenantPluginSettings, useModuleNavigation, usePluginOnboardingGate, PluginOnboardingGateSkeleton, createViewRouter, ModuleActionBar, SettingsGroup, ToggleRow, usePermissionOptional, CrudListView, useLimitGuard, invalidateRelationOptions, CrudFormSkeleton, CrudFormPage, CrudDetailPage, formatCurrency, useAgentSurface, askAI, PermissionGate, QuickActionsButton, invalidateLimit } from '@fayz-ai/admin';
2
+ import { toast, defineKpiWidget, defineCustomWidget, PageTransition, ComingSoon, ModulePage, Button, FilterPanel, FilterPanelField, RequiredFilter, FilterMenuRow, SubpageHeader, SearchSelect, cn, DatePicker, ListView, Card, Input, DataTable, SegmentedControl, ConfirmDialog, Badge, DraftCommitBar, Switch, useSaveBar, DashboardCanvas, Thumbnail, Modal, ModalContent, ModalHeader, ModalTitle, ModalDescription, ModalBody, ModalFooter, StockEntryForm, KpiCard, CardHeader, CardTitle, CardContent, Skeleton } from '@fayz-ai/ui';
3
+ import { createPluginContext, PluginSettingsPanel, dedup, useTenantPluginSettings, useModuleNavigation, usePluginOnboardingGate, PluginOnboardingGateSkeleton, createViewRouter, RegistryCrudView, ModuleActionBar, SettingsGroup, ToggleRow, usePermissionOptional, CrudListView, useLimitGuard, invalidateRelationOptions, CrudFormSkeleton, CrudFormPage, CrudDetailPage, formatCurrency, useAgentSurface, askAI, PermissionGate, QuickActionsButton, invalidateLimit } from '@fayz-ai/admin';
4
4
  import { recordRailFields, RECORD_GROUP, activeFacet, 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
- import { Trash2, ArrowRightLeft, RefreshCw, ArrowUpRight, ArrowDownRight, Package, Ruler, MapPin, Loader2, Check, ChevronRight, CircleSlash, CircleCheck, ListFilter, CalendarRange, PackageCheck, ClipboardCheck, AlertTriangle, Plus, BookOpen, Layers, Clock, Coins, Building2, Undo2, PackageX } from 'lucide-react';
6
+ import { Trash2, ArrowRightLeft, RefreshCw, ArrowUpRight, ArrowDownRight, Package, Ruler, MapPin, Loader2, Check, ChevronRight, CircleSlash, CircleCheck, Boxes, Layers, CircleDashed, Search, ListFilter, CalendarRange, PackageCheck, ClipboardCheck, AlertTriangle, Plus, BookOpen, Clock, Coins, Building2, Undo2, PackageX } from 'lucide-react';
7
7
  import { createStore as createStore$1 } from 'zustand/vanilla';
8
8
 
9
9
  // src/index.ts
@@ -605,21 +605,53 @@ function ProductListView({ onNew, onOpen }) {
605
605
  const { currency, productTypes } = useInventoryConfig();
606
606
  const products2 = useInventoryStore((s) => s.products);
607
607
  const productsTotal = useInventoryStore((s) => s.productsTotal);
608
- useInventoryStore((s) => s.productsLoading);
608
+ const productsLoading = useInventoryStore((s) => s.productsLoading);
609
609
  const fetchProducts = useInventoryStore((s) => s.fetchProducts);
610
610
  const [search, setSearch] = useState("");
611
611
  const [typeFilter, setTypeFilter] = useState();
612
612
  const [activeFilter, setActiveFilter] = useState();
613
+ const [categoryId, setCategoryId] = useState("");
614
+ const [categories, setCategories] = useState([]);
615
+ useEffect(() => {
616
+ let vivo = true;
617
+ provider.getCategories().then((rows) => {
618
+ if (vivo) setCategories(rows);
619
+ }).catch(() => {
620
+ });
621
+ return () => {
622
+ vivo = false;
623
+ };
624
+ }, [provider]);
613
625
  const [page, setPage] = useState(1);
614
626
  const [loadedOnce, setLoadedOnce] = useState(false);
615
627
  const [bulkBusy, setBulkBusy] = useState(false);
628
+ const rascunho = useMemo(
629
+ () => ({ productType: typeFilter, categoryId, isActive: activeFilter }),
630
+ [typeFilter, categoryId, activeFilter]
631
+ );
632
+ const [emVigor, setEmVigor] = useState(rascunho);
633
+ const filtrosPendentes = useMemo(
634
+ () => JSON.stringify(rascunho) !== JSON.stringify(emVigor),
635
+ [rascunho, emVigor]
636
+ );
637
+ const aplicarFiltros = useCallback(() => {
638
+ setPage(1);
639
+ setEmVigor(() => rascunho);
640
+ }, [rascunho]);
641
+ const limparFiltros = () => {
642
+ setTypeFilter(void 0);
643
+ setCategoryId("");
644
+ setActiveFilter(void 0);
645
+ };
646
+ const filtrosAtivos = (typeFilter ? 1 : 0) + (categoryId ? 1 : 0) + (activeFilter !== void 0 ? 1 : 0);
616
647
  const query = useMemo(() => ({
617
- productType: typeFilter,
648
+ productType: emVigor.productType,
649
+ categoryId: emVigor.categoryId || void 0,
618
650
  search: search || void 0,
619
- isActive: activeFilter === void 0 ? void 0 : activeFilter === "true",
651
+ isActive: emVigor.isActive === void 0 ? void 0 : emVigor.isActive === "true",
620
652
  page,
621
653
  pageSize: PAGE_SIZE
622
- }), [typeFilter, search, activeFilter, page]);
654
+ }), [emVigor, search, page]);
623
655
  useEffect(() => {
624
656
  let active = true;
625
657
  Promise.resolve(fetchProducts(query)).finally(() => {
@@ -630,22 +662,6 @@ function ProductListView({ onNew, onOpen }) {
630
662
  };
631
663
  }, [query]);
632
664
  const entity = useMemo(() => buildProductEntity(t2, productTypes, currency), [t2, productTypes, currency]);
633
- const facets = useMemo(() => [
634
- {
635
- field: "productType",
636
- allLabel: t2("crud.list.allFacet"),
637
- options: productTypes.map((p) => ({ value: p.value, label: p.label }))
638
- },
639
- {
640
- field: "isActive",
641
- label: t2("inventory.productList.filterStatus"),
642
- allLabel: t2("inventory.productList.filterAllStatuses"),
643
- options: [
644
- { value: "true", label: t2("inventory.productList.filterActive") },
645
- { value: "false", label: t2("inventory.productList.filterInactive") }
646
- ]
647
- }
648
- ], [productTypes, t2]);
649
665
  const runBulk = useCallback(async (rows, isActive) => {
650
666
  const targets = rows.filter((p) => (p.isActive ?? true) !== isActive);
651
667
  if (targets.length === 0) {
@@ -689,46 +705,122 @@ function ProductListView({ onNew, onOpen }) {
689
705
  }
690
706
  ];
691
707
  }, [can, bulkBusy, runBulk, t2]);
692
- return /* @__PURE__ */ jsx(
693
- CrudListView,
708
+ const filtros = /* @__PURE__ */ jsxs(
709
+ FilterPanel,
694
710
  {
695
- entityDef: entity,
696
- items: loadedOnce ? products2 : null,
697
- total: productsTotal,
698
- search,
699
- onSearchChange: (value) => {
700
- setPage(1);
701
- setSearch(value);
702
- },
703
- searchPlaceholder: t2("inventory.productList.searchPlaceholder"),
704
- facets,
705
- activeFilters: { productType: typeFilter, isActive: activeFilter },
706
- onFacetChange: (field, value) => {
707
- setPage(1);
708
- if (field === "isActive") setActiveFilter(value);
709
- else setTypeFilter(value);
710
- },
711
- page,
712
- pageSize: PAGE_SIZE,
713
- onPageChange: setPage,
714
- bulkActions,
715
- onNew,
716
- aiCreate: {
717
- hint: t2("inventory.productList.askPlaceholder"),
718
- description: t2("inventory.productList.aiHint"),
719
- intro: {
720
- role: "operations",
721
- persona: t2("inventory.productList.askPersona"),
722
- pitch: t2("inventory.productList.askPitch"),
723
- description: t2("inventory.productList.askDescription"),
724
- suggestions: [0, 1, 2].map((i) => ({ label: t2(`inventory.productList.askSuggestion.${i}`) }))
725
- }
726
- },
727
- addLabel: t2("inventory.productList.newProduct"),
728
- onRowClick: (row) => onOpen?.(row.id),
729
- feature: "inventory"
711
+ title: t2("inventory.filters.title"),
712
+ dirty: filtrosPendentes,
713
+ onApply: aplicarFiltros,
714
+ applyLabel: t2("inventory.filters.apply"),
715
+ pendingTitle: t2("inventory.filters.pending"),
716
+ pendingHint: t2("inventory.filters.pendingHint"),
717
+ onClear: filtrosAtivos > 0 ? limparFiltros : void 0,
718
+ clearLabel: t2("inventory.filters.clear"),
719
+ busy: productsLoading,
720
+ children: [
721
+ /* @__PURE__ */ jsx(FilterPanelField, { label: t2("inventory.filters.kind"), children: /* @__PURE__ */ jsx(
722
+ RequiredFilter,
723
+ {
724
+ label: t2("inventory.filters.kind"),
725
+ value: productTypes.find((p) => p.value === typeFilter)?.label ?? t2("inventory.filters.allKinds"),
726
+ icon: Boxes,
727
+ className: "h-9 w-full justify-start",
728
+ align: "start",
729
+ children: (close) => /* @__PURE__ */ jsxs("div", { className: "max-h-72 space-y-0.5 overflow-y-auto", children: [
730
+ /* @__PURE__ */ jsx(FilterMenuRow, { active: !typeFilter, onClick: () => {
731
+ setTypeFilter(void 0);
732
+ close();
733
+ }, children: t2("inventory.filters.allKinds") }),
734
+ productTypes.map((p) => /* @__PURE__ */ jsx(FilterMenuRow, { active: typeFilter === p.value, onClick: () => {
735
+ setTypeFilter(p.value);
736
+ close();
737
+ }, children: /* @__PURE__ */ jsx("span", { className: "truncate", children: p.label }) }, p.value))
738
+ ] })
739
+ }
740
+ ) }),
741
+ categories.length > 0 && /* @__PURE__ */ jsx(FilterPanelField, { label: t2("inventory.filters.category"), children: /* @__PURE__ */ jsx(
742
+ RequiredFilter,
743
+ {
744
+ label: t2("inventory.filters.category"),
745
+ value: categories.find((c) => c.id === categoryId)?.name ?? t2("inventory.filters.allCategories"),
746
+ icon: Layers,
747
+ className: "h-9 w-full justify-start",
748
+ align: "start",
749
+ children: (close) => /* @__PURE__ */ jsxs("div", { className: "max-h-72 space-y-0.5 overflow-y-auto", children: [
750
+ /* @__PURE__ */ jsx(FilterMenuRow, { active: !categoryId, onClick: () => {
751
+ setCategoryId("");
752
+ close();
753
+ }, children: t2("inventory.filters.allCategories") }),
754
+ categories.map((c) => /* @__PURE__ */ jsx(FilterMenuRow, { active: categoryId === c.id, onClick: () => {
755
+ setCategoryId(c.id);
756
+ close();
757
+ }, children: /* @__PURE__ */ jsx("span", { className: "truncate", children: c.name }) }, c.id))
758
+ ] })
759
+ }
760
+ ) }),
761
+ /* @__PURE__ */ jsx(FilterPanelField, { label: t2("inventory.filters.status"), children: /* @__PURE__ */ jsx(
762
+ RequiredFilter,
763
+ {
764
+ label: t2("inventory.filters.status"),
765
+ value: activeFilter === void 0 ? t2("inventory.filters.allStatuses") : activeFilter === "true" ? t2("inventory.productList.active") : t2("inventory.productList.inactive"),
766
+ icon: CircleCheck,
767
+ className: "h-9 w-full justify-start",
768
+ align: "start",
769
+ children: (close) => /* @__PURE__ */ jsxs("div", { className: "space-y-0.5", children: [
770
+ /* @__PURE__ */ jsx(FilterMenuRow, { active: activeFilter === void 0, onClick: () => {
771
+ setActiveFilter(void 0);
772
+ close();
773
+ }, children: t2("inventory.filters.allStatuses") }),
774
+ /* @__PURE__ */ jsx(FilterMenuRow, { active: activeFilter === "true", onClick: () => {
775
+ setActiveFilter("true");
776
+ close();
777
+ }, children: t2("inventory.productList.active") }),
778
+ /* @__PURE__ */ jsx(FilterMenuRow, { active: activeFilter === "false", onClick: () => {
779
+ setActiveFilter("false");
780
+ close();
781
+ }, children: t2("inventory.productList.inactive") })
782
+ ] })
783
+ }
784
+ ) })
785
+ ]
730
786
  }
731
787
  );
788
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
789
+ filtros,
790
+ /* @__PURE__ */ jsx(
791
+ CrudListView,
792
+ {
793
+ entityDef: entity,
794
+ items: loadedOnce ? products2 : null,
795
+ total: productsTotal,
796
+ search,
797
+ onSearchChange: (value) => {
798
+ setPage(1);
799
+ setSearch(value);
800
+ },
801
+ searchPlaceholder: t2("inventory.productList.searchPlaceholder"),
802
+ page,
803
+ pageSize: PAGE_SIZE,
804
+ onPageChange: setPage,
805
+ bulkActions,
806
+ onNew,
807
+ aiCreate: {
808
+ hint: t2("inventory.productList.askPlaceholder"),
809
+ description: t2("inventory.productList.aiHint"),
810
+ intro: {
811
+ role: "operations",
812
+ persona: t2("inventory.productList.askPersona"),
813
+ pitch: t2("inventory.productList.askPitch"),
814
+ description: t2("inventory.productList.askDescription"),
815
+ suggestions: [0, 1, 2].map((i) => ({ label: t2(`inventory.productList.askSuggestion.${i}`) }))
816
+ }
817
+ },
818
+ addLabel: t2("inventory.productList.newProduct"),
819
+ onRowClick: (row) => onOpen?.(row.id),
820
+ feature: "inventory"
821
+ }
822
+ )
823
+ ] });
732
824
  }
733
825
  function ProductCrudForm({ editId, onSaved }) {
734
826
  const t2 = useTranslation();
@@ -1438,10 +1530,49 @@ function MovementHistoryView({ onViewDetail, onNew, onNewExit } = {}) {
1438
1530
  const movements2 = useInventoryStore((s) => s.movements);
1439
1531
  const movementsLoading = useInventoryStore((s) => s.movementsLoading);
1440
1532
  const fetchMovements = useInventoryStore((s) => s.fetchMovements);
1533
+ const locations = useInventoryStore((s) => s.locations);
1534
+ const fetchLocations = useInventoryStore((s) => s.fetchLocations);
1535
+ useEffect(() => {
1536
+ void fetchLocations();
1537
+ }, [fetchLocations]);
1441
1538
  const [search, setSearch] = useState("");
1539
+ const hoje = () => {
1540
+ const d = /* @__PURE__ */ new Date();
1541
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
1542
+ };
1543
+ const trintaDiasAtras = useMemo(() => {
1544
+ const d = /* @__PURE__ */ new Date();
1545
+ d.setDate(d.getDate() - 29);
1546
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
1547
+ }, []);
1548
+ const [tipos, setTipos] = useState([]);
1549
+ const [locationId, setLocationId] = useState("");
1550
+ const [dateFrom, setDateFrom] = useState(trintaDiasAtras);
1551
+ const [dateTo, setDateTo] = useState(hoje);
1552
+ const rascunho = useMemo(
1553
+ () => ({ tipos, locationId, from: dateFrom, to: dateTo }),
1554
+ [tipos, locationId, dateFrom, dateTo]
1555
+ );
1556
+ const [emVigor, setEmVigor] = useState(rascunho);
1557
+ const filtrosPendentes = useMemo(
1558
+ () => JSON.stringify(rascunho) !== JSON.stringify(emVigor),
1559
+ [rascunho, emVigor]
1560
+ );
1561
+ const aplicarFiltros = useCallback(() => setEmVigor(() => rascunho), [rascunho]);
1562
+ const limparFiltros = () => {
1563
+ setTipos([]);
1564
+ setLocationId("");
1565
+ setDateFrom(trintaDiasAtras);
1566
+ setDateTo(hoje());
1567
+ };
1442
1568
  useEffect(() => {
1443
- fetchMovements({ search: search || void 0 });
1444
- }, [search]);
1569
+ fetchMovements({
1570
+ search: search || void 0,
1571
+ movementType: emVigor.tipos.length > 0 ? emVigor.tipos : void 0,
1572
+ stockLocationId: emVigor.locationId || void 0,
1573
+ dateRange: { from: emVigor.from, to: emVigor.to }
1574
+ });
1575
+ }, [search, emVigor]);
1445
1576
  const columns = useMemo(() => [
1446
1577
  {
1447
1578
  accessorKey: "movementDate",
@@ -1529,6 +1660,69 @@ function MovementHistoryView({ onViewDetail, onNew, onNewExit } = {}) {
1529
1660
  ], [currency, t2]);
1530
1661
  return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
1531
1662
  /* @__PURE__ */ jsx(SubpageHeader, { title: t2("inventory.history.title"), subtitle: t2("inventory.history.subtitle") }),
1663
+ /* @__PURE__ */ jsxs(
1664
+ FilterPanel,
1665
+ {
1666
+ title: t2("inventory.filters.title"),
1667
+ dirty: filtrosPendentes,
1668
+ onApply: aplicarFiltros,
1669
+ applyLabel: t2("inventory.filters.apply"),
1670
+ pendingTitle: t2("inventory.filters.pending"),
1671
+ pendingHint: t2("inventory.filters.pendingHint"),
1672
+ onClear: tipos.length > 0 || locationId ? limparFiltros : void 0,
1673
+ clearLabel: t2("inventory.filters.clear"),
1674
+ busy: movementsLoading,
1675
+ children: [
1676
+ /* @__PURE__ */ jsx(FilterPanelField, { label: t2("inventory.history.columnType"), children: /* @__PURE__ */ jsx(
1677
+ RequiredFilter,
1678
+ {
1679
+ label: t2("inventory.history.columnType"),
1680
+ value: tipos.length === 0 ? t2("inventory.filters.allTypes") : tipos.length === 1 ? movementTypeLabel(tipos[0], t2) : t2("crud.list.selectedMany", { count: tipos.length }),
1681
+ icon: CircleDashed,
1682
+ className: "h-9 w-full justify-start",
1683
+ align: "start",
1684
+ children: /* @__PURE__ */ jsxs("div", { className: "space-y-0.5", children: [
1685
+ /* @__PURE__ */ jsx(FilterMenuRow, { active: tipos.length === 0, onClick: () => setTipos([]), children: t2("inventory.filters.allTypes") }),
1686
+ MOVEMENT_TYPES.map((spec) => {
1687
+ const on = tipos.includes(spec.value);
1688
+ return /* @__PURE__ */ jsx(
1689
+ FilterMenuRow,
1690
+ {
1691
+ active: on,
1692
+ onClick: () => setTipos(on ? tipos.filter((v) => v !== spec.value) : [...tipos, spec.value]),
1693
+ children: /* @__PURE__ */ jsx("span", { className: "truncate", children: t2(spec.labelKey) })
1694
+ },
1695
+ spec.value
1696
+ );
1697
+ })
1698
+ ] })
1699
+ }
1700
+ ) }),
1701
+ locations.length > 0 && /* @__PURE__ */ jsx(FilterPanelField, { label: t2("inventory.filters.location"), children: /* @__PURE__ */ jsx(
1702
+ RequiredFilter,
1703
+ {
1704
+ label: t2("inventory.filters.location"),
1705
+ value: locations.find((l) => l.id === locationId)?.name ?? t2("inventory.filters.allLocations"),
1706
+ icon: MapPin,
1707
+ className: "h-9 w-full justify-start",
1708
+ align: "start",
1709
+ children: (close) => /* @__PURE__ */ jsxs("div", { className: "max-h-72 space-y-0.5 overflow-y-auto", children: [
1710
+ /* @__PURE__ */ jsx(FilterMenuRow, { active: !locationId, onClick: () => {
1711
+ setLocationId("");
1712
+ close();
1713
+ }, children: t2("inventory.filters.allLocations") }),
1714
+ locations.map((l) => /* @__PURE__ */ jsx(FilterMenuRow, { active: locationId === l.id, onClick: () => {
1715
+ setLocationId(l.id);
1716
+ close();
1717
+ }, children: /* @__PURE__ */ jsx("span", { className: "truncate", children: l.name }) }, l.id))
1718
+ ] })
1719
+ }
1720
+ ) }),
1721
+ /* @__PURE__ */ jsx(FilterPanelField, { label: t2("inventory.filters.from"), children: /* @__PURE__ */ jsx(DatePicker, { variant: "input", format: "numeric", className: "w-full", value: dateFrom, onChange: (v) => v && setDateFrom(v) }) }),
1722
+ /* @__PURE__ */ jsx(FilterPanelField, { label: t2("inventory.filters.to"), children: /* @__PURE__ */ jsx(DatePicker, { variant: "input", format: "numeric", className: "w-full", value: dateTo, onChange: (v) => v && setDateTo(v) }) })
1723
+ ]
1724
+ }
1725
+ ),
1532
1726
  /* @__PURE__ */ jsx(
1533
1727
  ListView,
1534
1728
  {
@@ -1553,6 +1747,290 @@ function MovementHistoryView({ onViewDetail, onNew, onNewExit } = {}) {
1553
1747
  )
1554
1748
  ] });
1555
1749
  }
1750
+ var HEALTH_ORDER = ["negative", "below_min", "zero", "ok"];
1751
+ var HEALTH_TONE = {
1752
+ negative: "bg-destructive-soft text-destructive-soft-foreground",
1753
+ below_min: "bg-warning-soft text-warning-soft-foreground",
1754
+ zero: "bg-muted text-muted-foreground",
1755
+ ok: "bg-success-soft text-success-soft-foreground"
1756
+ };
1757
+ var EXPIRY_OPTIONS = ["all", "expired", "le30", "le60", "le90", "none"];
1758
+ function StockPanelView() {
1759
+ const t2 = useTranslation();
1760
+ const provider = useInventoryProvider();
1761
+ const { currency } = useInventoryConfig();
1762
+ const locations = useInventoryStore((s) => s.locations);
1763
+ const fetchLocations = useInventoryStore((s) => s.fetchLocations);
1764
+ const [categories, setCategories] = useState([]);
1765
+ const [rows, setRows] = useState([]);
1766
+ const [loading, setLoading] = useState(false);
1767
+ const [suportado, setSuportado] = useState(true);
1768
+ useEffect(() => {
1769
+ void fetchLocations();
1770
+ }, [fetchLocations]);
1771
+ useEffect(() => {
1772
+ let vivo = true;
1773
+ provider.getCategories().then((c) => {
1774
+ if (vivo) setCategories(c);
1775
+ }).catch(() => {
1776
+ });
1777
+ return () => {
1778
+ vivo = false;
1779
+ };
1780
+ }, [provider]);
1781
+ const [locationId, setLocationId] = useState("");
1782
+ const [categoryId, setCategoryId] = useState("");
1783
+ const [produto, setProduto] = useState("");
1784
+ const [lote, setLote] = useState("");
1785
+ const [validade, setValidade] = useState("all");
1786
+ const [saude, setSaude] = useState([]);
1787
+ const rascunho = useMemo(
1788
+ () => ({ locationId, categoryId, produto: produto.trim(), lote: lote.trim(), validade, saude }),
1789
+ [locationId, categoryId, produto, lote, validade, saude]
1790
+ );
1791
+ const [emVigor, setEmVigor] = useState(rascunho);
1792
+ const filtrosPendentes = useMemo(
1793
+ () => JSON.stringify(rascunho) !== JSON.stringify(emVigor),
1794
+ [rascunho, emVigor]
1795
+ );
1796
+ const aplicarFiltros = useCallback(() => setEmVigor(() => rascunho), [rascunho]);
1797
+ const limparFiltros = () => {
1798
+ setLocationId("");
1799
+ setCategoryId("");
1800
+ setProduto("");
1801
+ setLote("");
1802
+ setValidade("all");
1803
+ setSaude([]);
1804
+ };
1805
+ const filtrosAtivos = (locationId ? 1 : 0) + (categoryId ? 1 : 0) + (produto.trim() ? 1 : 0) + (lote.trim() ? 1 : 0) + (validade !== "all" ? 1 : 0) + (saude.length > 0 ? 1 : 0);
1806
+ useEffect(() => {
1807
+ if (!provider.getStockPanel) {
1808
+ setSuportado(false);
1809
+ return;
1810
+ }
1811
+ let vivo = true;
1812
+ setLoading(true);
1813
+ provider.getStockPanel({
1814
+ stockLocationId: emVigor.locationId || void 0,
1815
+ categoryId: emVigor.categoryId || void 0,
1816
+ productSearch: emVigor.produto || void 0,
1817
+ batch: emVigor.lote || void 0,
1818
+ expiry: emVigor.validade,
1819
+ status: emVigor.saude.length > 0 ? emVigor.saude : void 0
1820
+ }).then((data) => {
1821
+ if (vivo) setRows(data);
1822
+ }).catch(() => {
1823
+ if (vivo) setRows([]);
1824
+ }).finally(() => {
1825
+ if (vivo) setLoading(false);
1826
+ });
1827
+ return () => {
1828
+ vivo = false;
1829
+ };
1830
+ }, [provider, emVigor]);
1831
+ const totais = useMemo(() => ({
1832
+ linhas: rows.length,
1833
+ valor: rows.reduce((soma, r) => soma + r.totalValue, 0)
1834
+ }), [rows]);
1835
+ const columns = useMemo(() => [
1836
+ {
1837
+ accessorKey: "productName",
1838
+ header: t2("inventory.panel.columnProduct"),
1839
+ cell: ({ row }) => /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
1840
+ /* @__PURE__ */ jsx("p", { className: "truncate font-medium", children: row.original.productName }),
1841
+ row.original.categoryName && /* @__PURE__ */ jsx("p", { className: "truncate text-[10px] text-muted-foreground", children: row.original.categoryName })
1842
+ ] })
1843
+ },
1844
+ {
1845
+ accessorKey: "stockLocationName",
1846
+ header: t2("inventory.panel.columnLocation"),
1847
+ cell: ({ getValue }) => /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: getValue() ?? "\u2014" })
1848
+ },
1849
+ {
1850
+ accessorKey: "batchNumber",
1851
+ header: t2("inventory.panel.columnBatch"),
1852
+ cell: ({ getValue }) => /* @__PURE__ */ jsx("span", { className: "font-mono text-xs text-muted-foreground", children: getValue() ?? "\u2014" })
1853
+ },
1854
+ {
1855
+ accessorKey: "expirationDate",
1856
+ header: t2("inventory.panel.columnExpiry"),
1857
+ cell: ({ getValue }) => /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: getValue() ?? "\u2014" })
1858
+ },
1859
+ {
1860
+ accessorKey: "quantity",
1861
+ header: t2("inventory.panel.columnQuantity"),
1862
+ enableSorting: true,
1863
+ cell: ({ row }) => /* @__PURE__ */ jsxs("span", { className: "block text-right tabular-nums", children: [
1864
+ row.original.quantity,
1865
+ row.original.minQuantity > 0 && /* @__PURE__ */ jsxs("span", { className: "ml-1 text-[10px] text-muted-foreground", children: [
1866
+ "/ ",
1867
+ row.original.minQuantity
1868
+ ] })
1869
+ ] })
1870
+ },
1871
+ {
1872
+ accessorKey: "totalValue",
1873
+ header: t2("inventory.panel.columnValue"),
1874
+ enableSorting: true,
1875
+ cell: ({ getValue }) => /* @__PURE__ */ jsx("span", { className: "block text-right tabular-nums", children: formatCurrency(Number(getValue()) || 0, currency) })
1876
+ },
1877
+ {
1878
+ accessorKey: "health",
1879
+ header: t2("inventory.filters.stockState"),
1880
+ cell: ({ getValue }) => {
1881
+ const h = getValue();
1882
+ return /* @__PURE__ */ jsx("span", { className: `inline-flex items-center rounded-full px-2 py-0.5 text-[10px] font-medium ${HEALTH_TONE[h]}`, children: t2(`inventory.filters.health.${h}`) });
1883
+ }
1884
+ }
1885
+ ], [currency, t2]);
1886
+ if (!suportado) {
1887
+ return /* @__PURE__ */ jsxs(Card, { className: "py-16 text-center", children: [
1888
+ /* @__PURE__ */ jsx(Boxes, { className: "mx-auto mb-2 h-8 w-8 text-muted-foreground/30" }),
1889
+ /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: t2("inventory.panel.unsupported") })
1890
+ ] });
1891
+ }
1892
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
1893
+ /* @__PURE__ */ jsxs(
1894
+ FilterPanel,
1895
+ {
1896
+ title: t2("inventory.filters.title"),
1897
+ dirty: filtrosPendentes,
1898
+ onApply: aplicarFiltros,
1899
+ applyLabel: t2("inventory.filters.apply"),
1900
+ pendingTitle: t2("inventory.filters.pending"),
1901
+ pendingHint: t2("inventory.filters.pendingHint"),
1902
+ onClear: filtrosAtivos > 0 ? limparFiltros : void 0,
1903
+ clearLabel: t2("inventory.filters.clear"),
1904
+ busy: loading,
1905
+ moreLabel: t2("inventory.filters.more"),
1906
+ lessLabel: t2("inventory.filters.less"),
1907
+ advancedCount: (lote.trim() ? 1 : 0) + (validade !== "all" ? 1 : 0),
1908
+ advanced: /* @__PURE__ */ jsxs(Fragment, { children: [
1909
+ /* @__PURE__ */ jsx(FilterPanelField, { label: t2("inventory.panel.columnBatch"), children: /* @__PURE__ */ jsx(
1910
+ Input,
1911
+ {
1912
+ value: lote,
1913
+ onChange: (e) => setLote(e.target.value),
1914
+ onKeyDown: (e) => {
1915
+ if (e.key === "Enter") aplicarFiltros();
1916
+ },
1917
+ placeholder: t2("inventory.filters.batchPlaceholder"),
1918
+ className: "h-9"
1919
+ }
1920
+ ) }),
1921
+ /* @__PURE__ */ jsx(FilterPanelField, { label: t2("inventory.filters.expiry"), children: /* @__PURE__ */ jsx(
1922
+ RequiredFilter,
1923
+ {
1924
+ label: t2("inventory.filters.expiry"),
1925
+ value: t2(`inventory.filters.expiry.${validade}`),
1926
+ icon: CircleDashed,
1927
+ className: "h-9 w-full justify-start",
1928
+ align: "start",
1929
+ children: (close) => /* @__PURE__ */ jsx("div", { className: "space-y-0.5", children: EXPIRY_OPTIONS.map((op) => /* @__PURE__ */ jsx(FilterMenuRow, { active: validade === op, onClick: () => {
1930
+ setValidade(op);
1931
+ close();
1932
+ }, children: t2(`inventory.filters.expiry.${op}`) }, op)) })
1933
+ }
1934
+ ) })
1935
+ ] }),
1936
+ children: [
1937
+ locations.length > 0 && /* @__PURE__ */ jsx(FilterPanelField, { label: t2("inventory.filters.location"), children: /* @__PURE__ */ jsx(
1938
+ RequiredFilter,
1939
+ {
1940
+ label: t2("inventory.filters.location"),
1941
+ value: locations.find((l) => l.id === locationId)?.name ?? t2("inventory.filters.allLocations"),
1942
+ icon: MapPin,
1943
+ className: "h-9 w-full justify-start",
1944
+ align: "start",
1945
+ children: (close) => /* @__PURE__ */ jsxs("div", { className: "max-h-72 space-y-0.5 overflow-y-auto", children: [
1946
+ /* @__PURE__ */ jsx(FilterMenuRow, { active: !locationId, onClick: () => {
1947
+ setLocationId("");
1948
+ close();
1949
+ }, children: t2("inventory.filters.allLocations") }),
1950
+ locations.map((l) => /* @__PURE__ */ jsx(FilterMenuRow, { active: locationId === l.id, onClick: () => {
1951
+ setLocationId(l.id);
1952
+ close();
1953
+ }, children: /* @__PURE__ */ jsx("span", { className: "truncate", children: l.name }) }, l.id))
1954
+ ] })
1955
+ }
1956
+ ) }),
1957
+ categories.length > 0 && /* @__PURE__ */ jsx(FilterPanelField, { label: t2("inventory.filters.category"), children: /* @__PURE__ */ jsx(
1958
+ RequiredFilter,
1959
+ {
1960
+ label: t2("inventory.filters.category"),
1961
+ value: categories.find((c) => c.id === categoryId)?.name ?? t2("inventory.filters.allCategories"),
1962
+ icon: Layers,
1963
+ className: "h-9 w-full justify-start",
1964
+ align: "start",
1965
+ children: (close) => /* @__PURE__ */ jsxs("div", { className: "max-h-72 space-y-0.5 overflow-y-auto", children: [
1966
+ /* @__PURE__ */ jsx(FilterMenuRow, { active: !categoryId, onClick: () => {
1967
+ setCategoryId("");
1968
+ close();
1969
+ }, children: t2("inventory.filters.allCategories") }),
1970
+ categories.map((c) => /* @__PURE__ */ jsx(FilterMenuRow, { active: categoryId === c.id, onClick: () => {
1971
+ setCategoryId(c.id);
1972
+ close();
1973
+ }, children: /* @__PURE__ */ jsx("span", { className: "truncate", children: c.name }) }, c.id))
1974
+ ] })
1975
+ }
1976
+ ) }),
1977
+ /* @__PURE__ */ jsx(FilterPanelField, { label: t2("inventory.filters.stockState"), className: "sm:col-span-2", children: /* @__PURE__ */ jsx("div", { className: "flex flex-wrap items-center gap-1.5", children: HEALTH_ORDER.map((h) => {
1978
+ const on = saude.includes(h);
1979
+ return /* @__PURE__ */ jsx(
1980
+ "button",
1981
+ {
1982
+ type: "button",
1983
+ "aria-pressed": on,
1984
+ onClick: () => setSaude(on ? saude.filter((v) => v !== h) : [...saude, h]),
1985
+ className: `rounded-full border px-2.5 py-1 text-xs transition-colors ${on ? "border-primary bg-primary/10 text-primary" : "border-input bg-muted/40 text-muted-foreground hover:bg-muted"}`,
1986
+ children: t2(`inventory.filters.health.${h}`)
1987
+ },
1988
+ h
1989
+ );
1990
+ }) }) })
1991
+ ]
1992
+ }
1993
+ ),
1994
+ /* @__PURE__ */ jsxs(Card, { className: "relative px-3 py-1.5", children: [
1995
+ /* @__PURE__ */ jsx(Search, { className: "pointer-events-none absolute left-5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground" }),
1996
+ /* @__PURE__ */ jsx(
1997
+ "input",
1998
+ {
1999
+ type: "text",
2000
+ value: produto,
2001
+ onChange: (e) => setProduto(e.target.value),
2002
+ onKeyDown: (e) => {
2003
+ if (e.key === "Enter") aplicarFiltros();
2004
+ },
2005
+ placeholder: t2("inventory.filters.productPlaceholder"),
2006
+ "aria-label": t2("inventory.filters.productPlaceholder"),
2007
+ className: "w-full bg-transparent py-1.5 pl-8 pr-3 text-sm placeholder:text-muted-foreground focus:outline-none"
2008
+ }
2009
+ )
2010
+ ] }),
2011
+ /* @__PURE__ */ jsx(
2012
+ DataTable,
2013
+ {
2014
+ columns,
2015
+ data: loading ? [] : rows,
2016
+ loading,
2017
+ emptyMessage: t2("inventory.panel.empty")
2018
+ }
2019
+ ),
2020
+ rows.length > 0 && !loading && /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-end gap-6 rounded-lg border bg-muted/30 px-5 py-3 text-sm", children: [
2021
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
2022
+ /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: t2("inventory.panel.totalLines") }),
2023
+ /* @__PURE__ */ jsx("span", { className: "font-semibold tabular-nums", children: totais.linhas })
2024
+ ] }),
2025
+ /* @__PURE__ */ jsx("div", { className: "h-4 w-px bg-border" }),
2026
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
2027
+ /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: t2("inventory.panel.totalValue") }),
2028
+ /* @__PURE__ */ jsx("span", { className: "font-semibold tabular-nums", children: formatCurrency(totais.valor, currency) })
2029
+ ] })
2030
+ ] })
2031
+ ] });
2032
+ }
2033
+ var COUNT_STATUSES = ["open", "counting", "closed", "cancelled"];
1556
2034
  var STATUS_TONE = {
1557
2035
  open: "bg-muted text-muted-foreground",
1558
2036
  counting: "bg-info-soft text-info-soft-foreground",
@@ -1609,6 +2087,35 @@ function StockCountsView({ onNew, onOpen }) {
1609
2087
  const [sessions, setSessions] = useState([]);
1610
2088
  const [loadedOnce, setLoadedOnce] = useState(false);
1611
2089
  const [search, setSearch] = useState("");
2090
+ const [locationId, setLocationId] = useState("");
2091
+ const [categoryId, setCategoryId] = useState("");
2092
+ const [statusFilter, setStatusFilter] = useState([]);
2093
+ const [dateFrom, setDateFrom] = useState("");
2094
+ const [dateTo, setDateTo] = useState("");
2095
+ const locais = useMemo(() => {
2096
+ const vistos = /* @__PURE__ */ new Map();
2097
+ for (const s of sessions) {
2098
+ if (s.stockLocationId && !vistos.has(s.stockLocationId)) {
2099
+ vistos.set(s.stockLocationId, s.stockLocationName ?? s.stockLocationId);
2100
+ }
2101
+ }
2102
+ return [...vistos].map(([id, name]) => ({ id, name })).sort((a, b) => a.name.localeCompare(b.name, "pt-BR"));
2103
+ }, [sessions]);
2104
+ const categorias = useMemo(() => {
2105
+ const vistos = /* @__PURE__ */ new Map();
2106
+ for (const s of sessions) {
2107
+ if (s.categoryId && !vistos.has(s.categoryId)) vistos.set(s.categoryId, s.categoryName ?? s.categoryId);
2108
+ }
2109
+ return [...vistos].map(([id, name]) => ({ id, name })).sort((a, b) => a.name.localeCompare(b.name, "pt-BR"));
2110
+ }, [sessions]);
2111
+ const filtrosAtivos = (locationId ? 1 : 0) + (categoryId ? 1 : 0) + (statusFilter.length > 0 ? 1 : 0) + (dateFrom ? 1 : 0) + (dateTo ? 1 : 0);
2112
+ const limparFiltros = () => {
2113
+ setLocationId("");
2114
+ setCategoryId("");
2115
+ setStatusFilter([]);
2116
+ setDateFrom("");
2117
+ setDateTo("");
2118
+ };
1612
2119
  const load = useCallback(async () => {
1613
2120
  setSessions(await provider.getCountSessions());
1614
2121
  setLoadedOnce(true);
@@ -1618,29 +2125,114 @@ function StockCountsView({ onNew, onOpen }) {
1618
2125
  }, [load]);
1619
2126
  const rows = useMemo(() => {
1620
2127
  const term = search.trim().toLowerCase();
1621
- return sessions.map((session) => ({
2128
+ return sessions.filter((session) => !locationId || session.stockLocationId === locationId).filter((session) => !categoryId || session.categoryId === categoryId).filter((session) => statusFilter.length === 0 || statusFilter.includes(session.status)).filter((session) => !dateFrom || (session.openedAt ?? "").slice(0, 10) >= dateFrom).filter((session) => !dateTo || (session.openedAt ?? "").slice(0, 10) <= dateTo).map((session) => ({
1622
2129
  ...session,
1623
2130
  label: session.reference || `${session.stockLocationName ?? ""} \xB7 ${session.openedAt.slice(0, 10)}`.trim()
1624
2131
  })).filter((row) => !term || row.label.toLowerCase().includes(term));
1625
- }, [sessions, search]);
2132
+ }, [sessions, search, locationId, categoryId, statusFilter, dateFrom, dateTo]);
1626
2133
  const entity = useMemo(() => buildCountEntity(t2), [t2]);
1627
- return /* @__PURE__ */ jsx(
1628
- CrudListView,
2134
+ const filtros = /* @__PURE__ */ jsxs(
2135
+ FilterPanel,
1629
2136
  {
1630
- entityDef: entity,
1631
- items: loadedOnce ? rows : null,
1632
- total: rows.length,
1633
- search,
1634
- onSearchChange: setSearch,
1635
- searchPlaceholder: t2("inventory.counts.searchPlaceholder"),
1636
- onNew,
1637
- addLabel: t2("inventory.counts.newCount"),
1638
- onRowClick: (row) => onOpen?.(row.id),
1639
- emptyMessage: t2("inventory.counts.empty"),
1640
- onRefresh: () => load(),
1641
- feature: "inventory"
2137
+ title: t2("inventory.filters.title"),
2138
+ dirty: false,
2139
+ onApply: () => {
2140
+ },
2141
+ applyLabel: t2("inventory.filters.apply"),
2142
+ pendingTitle: t2("inventory.filters.pending"),
2143
+ onClear: filtrosAtivos > 0 ? limparFiltros : void 0,
2144
+ clearLabel: t2("inventory.filters.clear"),
2145
+ children: [
2146
+ /* @__PURE__ */ jsx(FilterPanelField, { label: t2("inventory.filters.status"), children: /* @__PURE__ */ jsx(
2147
+ RequiredFilter,
2148
+ {
2149
+ label: t2("inventory.filters.status"),
2150
+ value: statusFilter.length === 0 ? t2("inventory.filters.allStatuses") : statusFilter.length === 1 ? t2(`inventory.counts.status.${statusFilter[0]}`) : t2("crud.list.selectedMany", { count: statusFilter.length }),
2151
+ icon: CircleDashed,
2152
+ className: "h-9 w-full justify-start",
2153
+ align: "start",
2154
+ children: /* @__PURE__ */ jsxs("div", { className: "space-y-0.5", children: [
2155
+ /* @__PURE__ */ jsx(FilterMenuRow, { active: statusFilter.length === 0, onClick: () => setStatusFilter([]), children: t2("inventory.filters.allStatuses") }),
2156
+ COUNT_STATUSES.map((st) => {
2157
+ const on = statusFilter.includes(st);
2158
+ return /* @__PURE__ */ jsx(
2159
+ FilterMenuRow,
2160
+ {
2161
+ active: on,
2162
+ onClick: () => setStatusFilter(on ? statusFilter.filter((v) => v !== st) : [...statusFilter, st]),
2163
+ children: /* @__PURE__ */ jsx("span", { className: "truncate", children: t2(`inventory.counts.status.${st}`) })
2164
+ },
2165
+ st
2166
+ );
2167
+ })
2168
+ ] })
2169
+ }
2170
+ ) }),
2171
+ locais.length > 1 && /* @__PURE__ */ jsx(FilterPanelField, { label: t2("inventory.filters.location"), children: /* @__PURE__ */ jsx(
2172
+ RequiredFilter,
2173
+ {
2174
+ label: t2("inventory.filters.location"),
2175
+ value: locais.find((l) => l.id === locationId)?.name ?? t2("inventory.filters.allLocations"),
2176
+ icon: MapPin,
2177
+ className: "h-9 w-full justify-start",
2178
+ align: "start",
2179
+ children: (close) => /* @__PURE__ */ jsxs("div", { className: "max-h-72 space-y-0.5 overflow-y-auto", children: [
2180
+ /* @__PURE__ */ jsx(FilterMenuRow, { active: !locationId, onClick: () => {
2181
+ setLocationId("");
2182
+ close();
2183
+ }, children: t2("inventory.filters.allLocations") }),
2184
+ locais.map((l) => /* @__PURE__ */ jsx(FilterMenuRow, { active: locationId === l.id, onClick: () => {
2185
+ setLocationId(l.id);
2186
+ close();
2187
+ }, children: /* @__PURE__ */ jsx("span", { className: "truncate", children: l.name }) }, l.id))
2188
+ ] })
2189
+ }
2190
+ ) }),
2191
+ categorias.length > 1 && /* @__PURE__ */ jsx(FilterPanelField, { label: t2("inventory.filters.category"), children: /* @__PURE__ */ jsx(
2192
+ RequiredFilter,
2193
+ {
2194
+ label: t2("inventory.filters.category"),
2195
+ value: categorias.find((c) => c.id === categoryId)?.name ?? t2("inventory.filters.allCategories"),
2196
+ icon: Layers,
2197
+ className: "h-9 w-full justify-start",
2198
+ align: "start",
2199
+ children: (close) => /* @__PURE__ */ jsxs("div", { className: "max-h-72 space-y-0.5 overflow-y-auto", children: [
2200
+ /* @__PURE__ */ jsx(FilterMenuRow, { active: !categoryId, onClick: () => {
2201
+ setCategoryId("");
2202
+ close();
2203
+ }, children: t2("inventory.filters.allCategories") }),
2204
+ categorias.map((c) => /* @__PURE__ */ jsx(FilterMenuRow, { active: categoryId === c.id, onClick: () => {
2205
+ setCategoryId(c.id);
2206
+ close();
2207
+ }, children: /* @__PURE__ */ jsx("span", { className: "truncate", children: c.name }) }, c.id))
2208
+ ] })
2209
+ }
2210
+ ) }),
2211
+ /* @__PURE__ */ jsx(FilterPanelField, { label: t2("inventory.filters.from"), children: /* @__PURE__ */ jsx(DatePicker, { variant: "input", format: "numeric", className: "w-full", value: dateFrom, onChange: (v) => setDateFrom(v ?? "") }) }),
2212
+ /* @__PURE__ */ jsx(FilterPanelField, { label: t2("inventory.filters.to"), children: /* @__PURE__ */ jsx(DatePicker, { variant: "input", format: "numeric", className: "w-full", value: dateTo, onChange: (v) => setDateTo(v ?? "") }) })
2213
+ ]
1642
2214
  }
1643
2215
  );
2216
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
2217
+ filtros,
2218
+ /* @__PURE__ */ jsx(
2219
+ CrudListView,
2220
+ {
2221
+ entityDef: entity,
2222
+ items: loadedOnce ? rows : null,
2223
+ total: rows.length,
2224
+ search,
2225
+ onSearchChange: setSearch,
2226
+ searchPlaceholder: t2("inventory.counts.searchPlaceholder"),
2227
+ onNew,
2228
+ addLabel: t2("inventory.counts.newCount"),
2229
+ onRowClick: (row) => onOpen?.(row.id),
2230
+ emptyMessage: t2("inventory.counts.empty"),
2231
+ onRefresh: () => load(),
2232
+ feature: "inventory"
2233
+ }
2234
+ )
2235
+ ] });
1644
2236
  }
1645
2237
  var FIELD = "w-full mt-1 rounded-input border border-input bg-card shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring";
1646
2238
  function StockCountFormView({ onOpened, onCancel }) {
@@ -1815,6 +2407,17 @@ function StockCountSessionView({ sessionId, onBack }) {
1815
2407
  void load();
1816
2408
  }, [load]);
1817
2409
  const summary = useMemo(() => summarizeCount(items), [items]);
2410
+ const [situacao, setSituacao] = useState("all");
2411
+ const [buscaItem, setBuscaItem] = useState("");
2412
+ const itensVisiveis = useMemo(() => {
2413
+ const termo = buscaItem.trim().toLocaleLowerCase("pt-BR");
2414
+ return items.filter((item) => {
2415
+ if (situacao === "pending") return item.countedQuantity === void 0;
2416
+ if (situacao === "counted") return item.countedQuantity !== void 0;
2417
+ if (situacao === "divergent") return lineVariance(item) !== 0;
2418
+ return true;
2419
+ }).filter((item) => !termo || (item.productName ?? "").toLocaleLowerCase("pt-BR").includes(termo) || (item.productSku ?? "").toLocaleLowerCase("pt-BR").includes(termo));
2420
+ }, [items, situacao, buscaItem]);
1818
2421
  const closed = session?.status === "closed" || session?.status === "cancelled";
1819
2422
  const hideVariance = (session?.blind ?? false) && !closed;
1820
2423
  async function commit(item, raw) {
@@ -1926,8 +2529,49 @@ function StockCountSessionView({ sessionId, onBack }) {
1926
2529
  /* @__PURE__ */ jsx(Button, { size: "sm", onClick: requestClose, children: t2("inventory.counts.close") })
1927
2530
  ] })
1928
2531
  ] }),
2532
+ /* @__PURE__ */ jsx(
2533
+ FilterPanel,
2534
+ {
2535
+ title: t2("inventory.filters.title"),
2536
+ dirty: false,
2537
+ onApply: () => {
2538
+ },
2539
+ applyLabel: t2("inventory.filters.apply"),
2540
+ pendingTitle: t2("inventory.filters.pending"),
2541
+ onClear: situacao !== "all" ? () => setSituacao("all") : void 0,
2542
+ clearLabel: t2("inventory.filters.clear"),
2543
+ children: /* @__PURE__ */ jsx(FilterPanelField, { label: t2("inventory.filters.countState"), className: "sm:col-span-2", children: /* @__PURE__ */ jsx(
2544
+ SegmentedControl,
2545
+ {
2546
+ "aria-label": t2("inventory.filters.countState"),
2547
+ value: situacao,
2548
+ onChange: (v) => setSituacao(v),
2549
+ options: [
2550
+ { value: "all", label: t2("inventory.filters.countAll") },
2551
+ { value: "pending", label: t2("inventory.filters.countPending") },
2552
+ { value: "counted", label: t2("inventory.filters.countDone") },
2553
+ { value: "divergent", label: t2("inventory.counts.divergent") }
2554
+ ]
2555
+ }
2556
+ ) })
2557
+ }
2558
+ ),
2559
+ /* @__PURE__ */ jsxs(Card, { className: "relative px-3 py-1.5", children: [
2560
+ /* @__PURE__ */ jsx(Search, { className: "pointer-events-none absolute left-5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground" }),
2561
+ /* @__PURE__ */ jsx(
2562
+ "input",
2563
+ {
2564
+ type: "text",
2565
+ value: buscaItem,
2566
+ onChange: (e) => setBuscaItem(e.target.value),
2567
+ placeholder: t2("inventory.filters.itemSearchPlaceholder"),
2568
+ "aria-label": t2("inventory.filters.itemSearchPlaceholder"),
2569
+ className: "w-full bg-transparent py-1.5 pl-8 pr-3 text-sm placeholder:text-muted-foreground focus:outline-none"
2570
+ }
2571
+ )
2572
+ ] }),
1929
2573
  /* @__PURE__ */ jsxs("div", { className: "rounded-card border bg-card shadow-sm divide-y", children: [
1930
- items.map((item, index) => {
2574
+ itensVisiveis.map((item, index) => {
1931
2575
  const variance = lineVariance(item);
1932
2576
  return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 px-4 py-3", children: [
1933
2577
  /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
@@ -1962,7 +2606,7 @@ function StockCountSessionView({ sessionId, onBack }) {
1962
2606
  )
1963
2607
  ] }, item.id);
1964
2608
  }),
1965
- items.length === 0 && /* @__PURE__ */ jsx("p", { className: "px-4 py-8 text-center text-xs text-muted-foreground", children: t2("inventory.counts.noItems") })
2609
+ itensVisiveis.length === 0 && /* @__PURE__ */ jsx("p", { className: "px-4 py-8 text-center text-xs text-muted-foreground", children: t2("inventory.counts.noItems") })
1966
2610
  ] }),
1967
2611
  /* @__PURE__ */ jsx(
1968
2612
  ConfirmDialog,
@@ -3387,6 +4031,102 @@ function UsageAuditView() {
3387
4031
  )
3388
4032
  ] });
3389
4033
  }
4034
+ function TransfersView() {
4035
+ return /* @__PURE__ */ jsx(
4036
+ ComingSoon,
4037
+ {
4038
+ title: "Transfer\xEAncias",
4039
+ summary: "Mover saldo de um local de estoque para outro \u2014 da c\xE2mara fria para o sal\xE3o, do dep\xF3sito para a cozinha \u2014 sem que isso vire entrada nem sa\xEDda.",
4040
+ v1Path: "Estoque \u2192 Transfer\xEAncias",
4041
+ measured: "7 transfer\xEAncias no V1 inteiro, nenhuma da Artorius. Eles t\xEAm 5 locais de estoque e movimentam tudo por entrada de nota.",
4042
+ missing: [
4043
+ "A transfer\xEAncia \xE9 DOIS movimentos ligados, n\xE3o um: sai de um local e entra no outro pelo mesmo custo",
4044
+ "O movimento de estoque do V2 j\xE1 tem `destination_location_id` \u2014 falta a tela e a regra de aprova\xE7\xE3o",
4045
+ "O V1 tem aprovador de transfer\xEAncia (stock_transfer_approvers); decidir se isso vem junto"
4046
+ ]
4047
+ }
4048
+ );
4049
+ }
4050
+ function RequisitionsView() {
4051
+ return /* @__PURE__ */ jsx(
4052
+ ComingSoon,
4053
+ {
4054
+ title: "Requisi\xE7\xF5es",
4055
+ summary: "O pedido interno: a cozinha requisita do dep\xF3sito, algu\xE9m separa, e a baixa acontece quando sai \u2014 n\xE3o quando foi pedido.",
4056
+ v1Path: "Estoque \u2192 Requisi\xE7\xF5es",
4057
+ measured: "ZERO requisi\xE7\xF5es no V1 inteiro, em nenhum cliente. A tela existe l\xE1 e nunca foi usada.",
4058
+ missing: [
4059
+ "Antes de construir: confirmar que algu\xE9m quer. Nenhum cliente do V1 usou isto em nenhum momento",
4060
+ "O V1 tem sete tabelas para o assunto (requisi\xE7\xE3o, item, lote, modelo, evento, ajuste, idempot\xEAncia) e nenhuma linha",
4061
+ "Se vier, \xE9 fluxo de pedido\u2192separa\xE7\xE3o\u2192baixa, com o saldo reservado no meio"
4062
+ ]
4063
+ }
4064
+ );
4065
+ }
4066
+ function StockLocationsView() {
4067
+ return /* @__PURE__ */ jsx(
4068
+ ComingSoon,
4069
+ {
4070
+ title: "Locais de Estoque",
4071
+ summary: "Onde a mercadoria fica: dep\xF3sito, c\xE2mara congelada, sal\xE3o. Cada movimento acontece em um local, e o saldo \xE9 por local.",
4072
+ v1Path: "Estoque \u2192 Locais de Estoque",
4073
+ measured: "A Artorius tem 5 locais no V1 e 6 j\xE1 migrados para c\xE1 (Estoque principal, C\xC2MARA CONGELADA\u2026). O dado atravessou; falta a tela para mexer nele.",
4074
+ missing: [
4075
+ "A tabela `plg_inventory_stock_locations` j\xE1 existe e j\xE1 recebeu os locais \u2014 \xE9 CRUD, n\xE3o porte de regra",
4076
+ "A concilia\xE7\xE3o de nota j\xE1 escolhe o local; hoje quem cria um novo tem de pedir a algu\xE9m",
4077
+ "Ligar local \xE0 unidade, que \xE9 o que a RLS usa para decidir quem v\xEA qual saldo"
4078
+ ]
4079
+ }
4080
+ );
4081
+ }
4082
+ function TemperatureView() {
4083
+ return /* @__PURE__ */ jsx(
4084
+ ComingSoon,
4085
+ {
4086
+ title: "Controle de Temperatura",
4087
+ summary: "O registro de temperatura dos equipamentos \u2014 c\xE2mara, freezer, balc\xE3o \u2014 com a leitura de cada turno e o alerta quando sai da faixa.",
4088
+ v1Path: "Estoque \u2192 Controle de Temperatura",
4089
+ measured: "70 pontos de controle e 138 leituras no V1, nenhum da Artorius. \xC9 a vigil\xE2ncia sanit\xE1ria de quem j\xE1 foi cobrado por ela.",
4090
+ missing: [
4091
+ "Cadastrar o ponto de controle (equipamento, faixa aceit\xE1vel, quantas leituras por dia)",
4092
+ "A leitura do turno, com quem leu e quando \u2014 \xE9 prova para a fiscaliza\xE7\xE3o",
4093
+ "O alerta quando sai da faixa; sem isso o registro vira papel passado"
4094
+ ]
4095
+ }
4096
+ );
4097
+ }
4098
+ function StockIndicatorsView() {
4099
+ return /* @__PURE__ */ jsx(
4100
+ ComingSoon,
4101
+ {
4102
+ title: "Indicadores",
4103
+ summary: "Ponto de pedido, estoque m\xEDnimo e m\xE1ximo por produto e por local \u2014 e o que est\xE1 abaixo do m\xEDnimo agora.",
4104
+ v1Path: "Estoque \u2192 Indicadores",
4105
+ measured: "Nenhum par\xE2metro cadastrado no V1, em nenhum cliente (`stock_center_product_params` est\xE1 vazia). A Artorius compra por h\xE1bito, n\xE3o por ponto de pedido.",
4106
+ missing: [
4107
+ "O par\xE2metro por produto \xD7 local: m\xEDnimo, m\xE1ximo, ponto de pedido",
4108
+ "A lista do que est\xE1 abaixo do m\xEDnimo \u2014 o painel do Estoque j\xE1 mostra \xABestoque baixo\xBB, mas com limiar chutado",
4109
+ "Sugest\xE3o de compra a partir disso \xE9 o passo seguinte, e \xE9 o que liga o estoque \xE0 Entrada de Notas"
4110
+ ]
4111
+ }
4112
+ );
4113
+ }
4114
+ function BulkProductEditView() {
4115
+ return /* @__PURE__ */ jsx(
4116
+ ComingSoon,
4117
+ {
4118
+ title: "Altera\xE7\xE3o em massa",
4119
+ summary: "Mudar pre\xE7o, custo, categoria ou unidade de muitos produtos de uma vez, sem abrir um por um.",
4120
+ v1Path: "Estoque \u2192 Produtos \u2192 Altera\xE7\xE3o em massa",
4121
+ measured: "A Tati editou 184 produtos no V1 em quatro dias. Um por um.",
4122
+ missing: [
4123
+ "Selecionar por filtro (categoria, tipo, fornecedor) e aplicar a mudan\xE7a ao conjunto",
4124
+ "Pr\xE9-visualizar o que vai mudar ANTES de gravar \u2014 altera\xE7\xE3o em massa errada \xE9 dif\xEDcil de desfazer",
4125
+ "Registrar quem mudou o qu\xEA: \xE9 a diferen\xE7a entre corre\xE7\xE3o e acidente"
4126
+ ]
4127
+ }
4128
+ );
4129
+ }
3390
4130
  var INVENTORY_MODULE_DEFAULTS = {
3391
4131
  recipes: true
3392
4132
  };
@@ -3479,54 +4219,106 @@ function InventoryOnboarding({ onComplete }) {
3479
4219
  }
3480
4220
  function buildNav(config, view, navigate, t2) {
3481
4221
  const items = [
4222
+ // Tudo o que se faz COM o catálogo mora aqui: a lista, como ela se agrupa
4223
+ // (Categorias) e como se mexe em muitos de uma vez. A primeira filha
4224
+ // chama-se «Produtos» e não «Lista» porque o nome de uma tela é o que ela
4225
+ // mostra, e «Lista» só diz o formato.
4226
+ // O Resumo abre o módulo: é a única tela daqui que responde sem que se peça
4227
+ // nada — quanto vale o estoque, quantos itens, o que está abaixo do mínimo.
3482
4228
  { id: "dashboard", label: t2("inventory.nav.dashboard"), icon: "BarChart3", active: view === "dashboard", onClick: () => navigate("dashboard") },
4229
+ // Depois dele, o catálogo: a lista, como ela se agrupa (Categorias) e como
4230
+ // se mexe em muitos de uma vez.
3483
4231
  ...config.modules.products ? [{
3484
4232
  id: "products",
3485
4233
  label: t2("inventory.nav.products"),
3486
4234
  icon: "Package",
3487
- active: view.startsWith("products"),
4235
+ active: view.startsWith("products") || view === "product-categories" || view === "bulk-products",
3488
4236
  children: [
3489
4237
  { id: "products-new", kind: "action", label: t2("inventory.nav.new"), active: view === "products-new", onClick: () => navigate("products-new") },
3490
- { id: "products-list", label: t2("inventory.nav.list"), active: view === "products-list" || view.startsWith("products-detail:") || view.startsWith("products-stock:"), onClick: () => navigate("products-list") }
4238
+ { id: "products-list", label: t2("inventory.nav.products"), defaultChild: true, active: view === "products-list" || view.startsWith("products-detail:") || view.startsWith("products-stock:"), onClick: () => navigate("products-list") },
4239
+ { id: "product-categories", label: t2("registry.product-categories"), active: view === "product-categories", onClick: () => navigate("product-categories") },
4240
+ { id: "bulk-products", label: "Altera\xE7\xE3o em massa", active: view === "bulk-products", onClick: () => navigate("bulk-products") }
3491
4241
  ]
3492
4242
  }] : [],
3493
- // Movimentações é UMA tela: o que entrou e o que saiu. A pílula "Histórico"
3494
- // ao lado da aba repetia o nome da própria aba (e o título da página já diz
3495
- // "Histórico de Estoque"), então a aba passa a SER o destino — filha única
3496
- // não desenha controle.
4243
+ // «Inventário» é a CONTAGEM, e não um agrupamento: é assim que a palavra é
4244
+ // usada em quem veio do V1 — «vou fazer o inventário» quer dizer ir à
4245
+ // prateleira conferir. O saldo que o sistema calcula sozinho é o Resumo, e
4246
+ // ele abre o módulo lá em cima.
3497
4247
  {
3498
- id: "stock",
3499
- label: t2("inventory.nav.stock"),
3500
- icon: "ArrowUpCircle",
4248
+ // DUAS ABAS, como no V1: o Painel diz QUANTO HÁ; as Contagens dizem o
4249
+ // que já foi conferido. O inventário abria direto na segunda, e a
4250
+ // pergunta que todo mundo faz primeiro não tinha tela.
4251
+ id: "inventory",
4252
+ label: t2("inventory.nav.inventory"),
4253
+ icon: "ClipboardCheck",
4254
+ active: view === "stock-panel" || view.startsWith("stock-counts"),
3501
4255
  children: [
3502
- { id: "stock-entry", kind: "action", label: t2("inventory.nav.entry"), active: view === "stock-entry", onClick: () => navigate("stock-entry") },
3503
- { id: "stock-exit", kind: "action", label: t2("inventory.nav.exit"), active: view === "stock-exit", onClick: () => navigate("stock-exit") },
3504
- { id: "stock-history", label: t2("inventory.nav.history"), defaultChild: true, active: view === "stock-history", onClick: () => navigate("stock-history") }
4256
+ { id: "stock-panel", label: t2("inventory.nav.panel"), defaultChild: true, active: view === "stock-panel", onClick: () => navigate("stock-panel") },
4257
+ { id: "stock-counts", label: t2("inventory.nav.counts"), active: view.startsWith("stock-counts"), onClick: () => navigate("stock-counts") }
3505
4258
  ]
3506
4259
  },
3507
- // Contagem e Auditoria de Uso são ABAS, não uma rubrica com pílula dentro.
3508
- //
3509
- // Conferir o que o sistema diz contra o que existe na prateleira são duas
3510
- // TELAS, cada uma com a sua lista e o seu fechamento — e agrupá-las sob
3511
- // "Inventário" punha o segundo nível num controle segmentado à direita da
3512
- // faixa, que é onde ninguém procura uma tela. A faixa é o lugar de uma
3513
- // tela; a pílula é o lugar de um recorte da mesma tela.
4260
+ // MOVIMENTAÇÕES É UMA LINHA SÓ, e ela leva direto para a lista.
3514
4261
  //
3515
- // "Nova contagem" saiu junto: era uma AÇÃO dentro de navegação, e a lista
3516
- // de contagens já tem o "+" no cabeçalho dela.
4262
+ // Entrada e saída eram filhas «ação» da aba: um grupo que abre para
4263
+ // oferecer dois formulários e o destino real no meio deles. Quem clica em
4264
+ // «Movimentações» quer ver o que entrou e o que saiu — lançar é o botão
4265
+ // que a própria lista já tem no cabeçalho, e é lá que a ação pertence.
3517
4266
  {
3518
- id: "stock-counts",
3519
- label: t2("inventory.nav.counts"),
3520
- icon: "ClipboardCheck",
3521
- active: view === "stock-counts" || view.startsWith("stock-counts"),
3522
- onClick: () => navigate("stock-counts")
4267
+ id: "stock-history",
4268
+ label: t2("inventory.nav.stock"),
4269
+ icon: "ArrowUpCircle",
4270
+ active: view === "stock-history" || view === "stock-entry" || view === "stock-exit",
4271
+ onClick: () => navigate("stock-history")
3523
4272
  },
4273
+ // Auditoria de Uso fica FORA do Inventário: ela não pergunta quanto há,
4274
+ // pergunta se o que saiu bate com o que foi vendido. É a única tela daqui
4275
+ // que olha para o consumo e não para o saldo.
4276
+ //
4277
+ // "Nova contagem" não está na navegação: era uma AÇÃO dentro de um menu, e
4278
+ // a lista de contagens já tem o "+" no cabeçalho dela.
3524
4279
  {
3525
4280
  id: "stock-usage-audit",
3526
4281
  label: t2("inventory.nav.usageAudit"),
3527
4282
  icon: "ScanSearch",
3528
4283
  active: view === "stock-usage-audit",
3529
4284
  onClick: () => navigate("stock-usage-audit")
4285
+ },
4286
+ // ── O que o Estoque do V1 tem e este ainda não ────────────────────────
4287
+ //
4288
+ // Agrupadas por PERGUNTA, não por tabela. Quem procura «onde fica a câmara
4289
+ // fria» e quem procura «quanto tenho de mínimo» estão fazendo coisas
4290
+ // diferentes, e por isso Cadastros e Reposição são duas abas.
4291
+ //
4292
+ // Produção fica de fora por decisão do founder: são seis telas e um assunto
4293
+ // próprio, não um buraco do estoque.
4294
+ // «Locais» e não «Cadastros»: cadastro é o que TODA tela desta lista faz.
4295
+ // O que junta estas duas é o lugar físico — onde a mercadoria está e a que
4296
+ // temperatura ela está lá. A alteração em massa saiu daqui: ela mexe no
4297
+ // catálogo, não no armazém, e agora mora com os Produtos.
4298
+ {
4299
+ id: "stock-setup",
4300
+ label: "Locais",
4301
+ icon: "Warehouse",
4302
+ active: ["stock-locations", "temperature"].some((v) => view === v),
4303
+ children: [
4304
+ { id: "stock-locations", label: "Locais", defaultChild: true, active: view === "stock-locations", onClick: () => navigate("stock-locations") },
4305
+ { id: "temperature", label: "Temperatura", active: view === "temperature", onClick: () => navigate("temperature") }
4306
+ ]
4307
+ },
4308
+ {
4309
+ // «Reposição» e não «Planejamento»: a Produção tem um Planejamento que é
4310
+ // plano de FABRICAÇÃO, e dois menus com o mesmo nome a uma rubrica de
4311
+ // distância fazem o leitor perguntar se são a mesma tela. As três aqui
4312
+ // respondem à mesma pergunta — o que falta e como trazer.
4313
+ id: "stock-planning",
4314
+ label: "Reposi\xE7\xE3o",
4315
+ icon: "Target",
4316
+ active: ["stock-indicators", "transfers", "requisitions"].some((v) => view === v),
4317
+ children: [
4318
+ { id: "stock-indicators", label: "Indicadores", defaultChild: true, active: view === "stock-indicators", onClick: () => navigate("stock-indicators") },
4319
+ { id: "transfers", label: "Transfer\xEAncias", active: view === "transfers", onClick: () => navigate("transfers") },
4320
+ { id: "requisitions", label: "Requisi\xE7\xF5es", active: view === "requisitions", onClick: () => navigate("requisitions") }
4321
+ ]
3530
4322
  }
3531
4323
  ];
3532
4324
  if (config.modules.recipes) {
@@ -3541,6 +4333,26 @@ function buildNav(config, view, navigate, t2) {
3541
4333
  ]
3542
4334
  });
3543
4335
  }
4336
+ for (const extra of config.extraViews) {
4337
+ const own = [extra.id, ...(extra.children ?? []).map((c) => c.id)];
4338
+ const hits = (id) => view === id || view.startsWith(`${id}/`) || view.startsWith(`${id}:`);
4339
+ items.push({
4340
+ id: extra.id,
4341
+ label: extra.label,
4342
+ icon: extra.icon,
4343
+ active: own.some(hits),
4344
+ // Com filhos, o PAI não navega — quem navega é o filho padrão, como o grupo
4345
+ // «Estoque» já faz com `stock-history`. Dar `onClick` ao pai E injetá-lo
4346
+ // como primeiro filho deslocava a pílula em um: a tela certa aparecia com o
4347
+ // nome da vizinha em destaque.
4348
+ ...extra.children?.length ? {
4349
+ children: [
4350
+ { id: extra.id, label: "Entrada", defaultChild: true, active: hits(extra.id), onClick: () => navigate(extra.id) },
4351
+ ...extra.children.map((c) => ({ id: c.id, label: c.label, active: hits(c.id), onClick: () => navigate(c.id) }))
4352
+ ]
4353
+ } : { onClick: () => navigate(extra.id) }
4354
+ });
4355
+ }
3544
4356
  return items;
3545
4357
  }
3546
4358
  function InventoryPage({ config: appConfig, provider, store, registries }) {
@@ -3653,6 +4465,7 @@ function InventoryPage({ config: appConfig, provider, store, registries }) {
3653
4465
  } },
3654
4466
  // One route id for both: `/inventory/stock/counts/<uuid>` parses back as
3655
4467
  // `stock-counts:<id>`, not `stock-counts-detail:<id>`.
4468
+ { id: "stock-panel", render: () => /* @__PURE__ */ jsx(StockPanelView, {}) },
3656
4469
  { id: "stock-counts", render: ({ id }) => id ? /* @__PURE__ */ jsx(StockCountSessionView, { sessionId: id, onBack: () => navigate("stock-counts") }) : /* @__PURE__ */ jsx(StockCountsView, { onNew: () => navigate("stock-counts-new"), onOpen: (sid) => navigate(`stock-counts:${sid}`) }) },
3657
4470
  { id: "stock-usage-audit", render: () => /* @__PURE__ */ jsx(UsageAuditView, {}) },
3658
4471
  { id: "stock-counts-new", render: () => /* @__PURE__ */ jsx(StockCountFormView, { onOpened: (sid) => navigate(`stock-counts:${sid}`), onCancel: () => navigate("stock-counts") }) },
@@ -3660,6 +4473,40 @@ function InventoryPage({ config: appConfig, provider, store, registries }) {
3660
4473
  { id: "recipes-list", render: () => /* @__PURE__ */ jsx(RecipesView, { onNew: () => navigate("recipes-new"), onView: (id) => navigate(`recipes-detail:${id}`), onNewForProduct: (pid) => navigate(`recipes-new:${pid}`) }) },
3661
4474
  { id: "recipes-new", render: ({ id }) => /* @__PURE__ */ jsx(RecipeFormView, { productId: id, onSaved: (rid) => rid ? navigate(`recipes-detail:${rid}`) : navigate("recipes-list") }) },
3662
4475
  { id: "recipes-detail", render: ({ id }) => /* @__PURE__ */ jsx(RecipeDetailView, { recipeId: id, onBack: () => navigate("recipes-list") }) },
4476
+ // Telas emprestadas por outro plugin — hoje a Entrada de Notas, que é assunto
4477
+ // de estoque e não linha de rail (no V1: Estoque → Notas Fiscais → Entrada).
4478
+ ...config.extraViews.flatMap((v) => [
4479
+ { id: v.id, render: () => /* @__PURE__ */ jsx(Fragment, { children: v.render() }) },
4480
+ ...(v.children ?? []).map((c) => ({ id: c.id, render: () => /* @__PURE__ */ jsx(Fragment, { children: c.render() }) }))
4481
+ ]),
4482
+ // As telas que ainda não existem. Cada uma diz o que falta e o tamanho do
4483
+ // buraco, medido no V1 — ver `./GapScreens`.
4484
+ // Categorias e Locais NÃO são «em breve»: o plugin já declara os dois como
4485
+ // cadastro (`data/registries`), e eles só viviam atrás de Configurações —
4486
+ // onde ninguém procura o lugar onde a carne fica. `RegistryCrudView` é o
4487
+ // mesmo CRUD que a página de ajustes desenha, agora na aba onde o assunto
4488
+ // está. Sem o cadastro declarado (um app que corte o registro), cai no
4489
+ // «em breve» de antes.
4490
+ //
4491
+ // Sem `embedded`: aqui o cadastro É a tela — a aba do módulo leva a ele e a
4492
+ // nada mais. `embedded` é para o cadastro montado NO MEIO de outra tela, ao
4493
+ // lado de outros blocos, e o que ele faz é tirar o «Adicionar» da barra do
4494
+ // topo e largá-lo numa fileira própria, encostado à direita do corpo. Com
4495
+ // isso, Locais e Categorias eram as duas únicas listas do Estoque cujo botão
4496
+ // de criar ficava em outro lugar que o de Produtos e o de Contagens.
4497
+ { id: "product-categories", render: () => {
4498
+ const reg = registries?.find((r) => r.id === "product-categories");
4499
+ return reg ? /* @__PURE__ */ jsx(RegistryCrudView, { registry: reg, basePath: "/inventory/product-categories" }) : /* @__PURE__ */ jsx(ComingSoon, { title: "Categorias", summary: "Como o cat\xE1logo se agrupa.", v1Path: "Estoque \u2192 Produtos \u2192 Categorias", missing: ["Este aplicativo n\xE3o declarou o cadastro de categorias"] });
4500
+ } },
4501
+ { id: "stock-locations", render: () => {
4502
+ const reg = registries?.find((r) => r.id === "stock-locations");
4503
+ return reg ? /* @__PURE__ */ jsx(RegistryCrudView, { registry: reg, basePath: "/inventory/stock-locations" }) : /* @__PURE__ */ jsx(StockLocationsView, {});
4504
+ } },
4505
+ { id: "temperature", render: () => /* @__PURE__ */ jsx(TemperatureView, {}) },
4506
+ { id: "bulk-products", render: () => /* @__PURE__ */ jsx(BulkProductEditView, {}) },
4507
+ { id: "stock-indicators", render: () => /* @__PURE__ */ jsx(StockIndicatorsView, {}) },
4508
+ { id: "transfers", render: () => /* @__PURE__ */ jsx(TransfersView, {}) },
4509
+ { id: "requisitions", render: () => /* @__PURE__ */ jsx(RequisitionsView, {}) },
3663
4510
  { id: "dashboard", render: () => /* @__PURE__ */ jsx(DashboardView, {}) }
3664
4511
  ], "dashboard");
3665
4512
  return /* @__PURE__ */ jsx(InventoryContextProvider, { config, provider, store, children: /* @__PURE__ */ jsx(
@@ -3881,6 +4728,14 @@ function createInventoryDashboardWidgets(ctx2) {
3881
4728
  ];
3882
4729
  }
3883
4730
 
4731
+ // src/lib/stock-health.ts
4732
+ function stockHealth(quantity, minQuantity) {
4733
+ if (quantity < 0) return "negative";
4734
+ if (quantity === 0) return "zero";
4735
+ if (minQuantity > 0 && quantity < minQuantity) return "below_min";
4736
+ return "ok";
4737
+ }
4738
+
3884
4739
  // src/data/mock.ts
3885
4740
  var nextId2 = 1;
3886
4741
  function uid() {
@@ -4134,6 +4989,53 @@ function createMockInventoryProvider() {
4134
4989
  async getPositions(productId) {
4135
4990
  return store.positions.filter((p) => p.productId === productId);
4136
4991
  },
4992
+ async getStockPanel(query) {
4993
+ const hoje = /* @__PURE__ */ new Date();
4994
+ const emDias = (d) => {
4995
+ const x = new Date(hoje);
4996
+ x.setDate(x.getDate() + d);
4997
+ return x.toISOString().slice(0, 10);
4998
+ };
4999
+ const linhas = store.positions.map((pos) => {
5000
+ const produto = store.products.find((pr) => pr.id === pos.productId);
5001
+ const quantity = Number(pos.quantity) || 0;
5002
+ const minQuantity = produto?.minQuantity ?? 0;
5003
+ const unitCost = Number(pos.unitCost) || 0;
5004
+ return {
5005
+ id: pos.id,
5006
+ productId: pos.productId,
5007
+ productName: produto?.name ?? "\u2014",
5008
+ categoryId: produto?.categoryId,
5009
+ categoryName: produto?.categoryName,
5010
+ stockLocationId: pos.stockLocationId,
5011
+ stockLocationName: pos.stockLocationName ?? store.locations.find((l) => l.id === pos.stockLocationId)?.name,
5012
+ batchNumber: pos.batchNumber,
5013
+ expirationDate: pos.expirationDate,
5014
+ quantity,
5015
+ minQuantity,
5016
+ unitCost,
5017
+ totalValue: quantity * unitCost,
5018
+ health: stockHealth(quantity, minQuantity)
5019
+ };
5020
+ });
5021
+ const termo = query.productSearch?.trim().toLowerCase();
5022
+ return linhas.filter((l) => !query.stockLocationId || l.stockLocationId === query.stockLocationId).filter((l) => !query.categoryId || l.categoryId === query.categoryId).filter((l) => !termo || l.productName.toLowerCase().includes(termo)).filter((l) => !query.batch || (l.batchNumber ?? "").toLowerCase().includes(query.batch.toLowerCase())).filter((l) => {
5023
+ switch (query.expiry) {
5024
+ case "expired":
5025
+ return !!l.expirationDate && l.expirationDate < emDias(0);
5026
+ case "none":
5027
+ return !l.expirationDate;
5028
+ case "le30":
5029
+ return !!l.expirationDate && l.expirationDate >= emDias(0) && l.expirationDate <= emDias(30);
5030
+ case "le60":
5031
+ return !!l.expirationDate && l.expirationDate >= emDias(0) && l.expirationDate <= emDias(60);
5032
+ case "le90":
5033
+ return !!l.expirationDate && l.expirationDate >= emDias(0) && l.expirationDate <= emDias(90);
5034
+ default:
5035
+ return true;
5036
+ }
5037
+ }).filter((l) => !query.status || query.status.length === 0 || query.status.includes(l.health)).slice(0, query.limit ?? 500);
5038
+ },
4137
5039
  // --- Stock Locations ---
4138
5040
  async getLocations() {
4139
5041
  return store.locations.filter((l) => l.isActive);
@@ -5148,6 +6050,77 @@ function createSupabaseInventoryProvider() {
5148
6050
  }
5149
6051
  return positions;
5150
6052
  },
6053
+ /**
6054
+ * O PAINEL DE ESTOQUE — o «Painel» do inventário do V1.
6055
+ *
6056
+ * Uma linha por POSIÇÃO (produto × local × lote × validade), que é a única
6057
+ * granularidade em que «Locais», «Lote» e «Validade» querem dizer alguma
6058
+ * coisa: somado por produto, um lote vencido desaparece dentro de um saldo
6059
+ * saudável.
6060
+ *
6061
+ * Produto e categoria recortam ANTES, resolvendo a lista de ids: filtrar
6062
+ * depois de paginar devolveria páginas pela metade, e o rodapé mentiria
6063
+ * sobre quantas linhas existem.
6064
+ */
6065
+ async getStockPanel(query) {
6066
+ const { core, pub } = getClients();
6067
+ const limite = query.limit ?? 500;
6068
+ const precisaRecortarProduto = Boolean(query.productSearch || query.categoryId);
6069
+ const selectProduto = query.categoryId ? `*, ${T.productDetails}!inner(*)` : PRODUCT_SELECT;
6070
+ let qp = soMercadoria(core.from(PRODUCTS_READ).select(selectProduto));
6071
+ if (query.productSearch) qp = qp.ilike("name", `%${query.productSearch}%`);
6072
+ if (query.categoryId) qp = qp.eq(`${T.productDetails}.category_id`, query.categoryId);
6073
+ const { data: produtoRows } = await qp.limit(precisaRecortarProduto ? 1e3 : 5e3);
6074
+ const produtos = (produtoRows ?? []).map(mapProductRow);
6075
+ const porProduto = new Map(produtos.map((pr) => [pr.id, pr]));
6076
+ if (precisaRecortarProduto && produtos.length === 0) return [];
6077
+ let qb = pub.from(T.stockPositions).select("*");
6078
+ if (precisaRecortarProduto) qb = qb.in("product_id", [...porProduto.keys()]);
6079
+ if (query.stockLocationId) qb = qb.eq("stock_location_id", query.stockLocationId);
6080
+ if (query.batch) qb = qb.ilike("batch_number", `%${query.batch}%`);
6081
+ const hoje = /* @__PURE__ */ new Date();
6082
+ const emDias = (d) => {
6083
+ const x = new Date(hoje);
6084
+ x.setDate(x.getDate() + d);
6085
+ return `${x.getFullYear()}-${String(x.getMonth() + 1).padStart(2, "0")}-${String(x.getDate()).padStart(2, "0")}`;
6086
+ };
6087
+ if (query.expiry === "expired") qb = qb.lt("expiration_date", emDias(0));
6088
+ else if (query.expiry === "none") qb = qb.is("expiration_date", null);
6089
+ else if (query.expiry === "le30") qb = qb.gte("expiration_date", emDias(0)).lte("expiration_date", emDias(30));
6090
+ else if (query.expiry === "le60") qb = qb.gte("expiration_date", emDias(0)).lte("expiration_date", emDias(60));
6091
+ else if (query.expiry === "le90") qb = qb.gte("expiration_date", emDias(0)).lte("expiration_date", emDias(90));
6092
+ const { data: posRows } = await qb.limit(limite);
6093
+ const locationIds = [...new Set((posRows ?? []).map((r) => r.stock_location_id).filter(Boolean))];
6094
+ const nomeDoLocal = /* @__PURE__ */ new Map();
6095
+ if (locationIds.length > 0) {
6096
+ const { data: locs } = await pub.from(T.stockLocations).select("id, name").in("id", locationIds);
6097
+ for (const l of locs ?? []) nomeDoLocal.set(l.id, l.name);
6098
+ }
6099
+ const linhas = (posRows ?? []).map((r) => {
6100
+ const produto = porProduto.get(r.product_id);
6101
+ const quantity = Number(r.quantity) || 0;
6102
+ const minQuantity = produto?.minQuantity ?? 0;
6103
+ const unitCost = Number(r.unit_cost) || 0;
6104
+ return {
6105
+ id: r.id,
6106
+ productId: r.product_id,
6107
+ productName: produto?.name ?? "\u2014",
6108
+ categoryId: produto?.categoryId,
6109
+ categoryName: produto?.categoryName,
6110
+ stockLocationId: r.stock_location_id ?? void 0,
6111
+ stockLocationName: r.stock_location_id ? nomeDoLocal.get(r.stock_location_id) : void 0,
6112
+ batchNumber: r.batch_number ?? void 0,
6113
+ expirationDate: r.expiration_date ?? void 0,
6114
+ quantity,
6115
+ minQuantity,
6116
+ unitCost,
6117
+ totalValue: quantity * unitCost,
6118
+ health: stockHealth(quantity, minQuantity)
6119
+ };
6120
+ });
6121
+ const porEstado = query.status && query.status.length > 0 ? linhas.filter((l) => query.status.includes(l.health)) : linhas;
6122
+ return porEstado.sort((a, b) => a.productName.localeCompare(b.productName, "pt-BR"));
6123
+ },
5151
6124
  // --- Stock Locations ---
5152
6125
  async getLocations() {
5153
6126
  const { pub } = getClients();
@@ -5878,6 +6851,11 @@ var measurementUnitEntity = {
5878
6851
  var productCategoryEntity = {
5879
6852
  name: "Product Category",
5880
6853
  namePlural: "Product Categories",
6854
+ // O botao de criar interpola o SUBSTANTIVO, e `name` e um literal ingles:
6855
+ // sem estas chaves o CrudListView omite o nome e escreve so «Adicionar»,
6856
+ // enquanto a lista ao lado escreve «Novo Produto». Ver EntityDef.nameKey.
6857
+ nameKey: "inventory.entity.productCategory",
6858
+ namePluralKey: "inventory.entity.productCategory.plural",
5881
6859
  icon: "Tag",
5882
6860
  displayField: "name",
5883
6861
  formColumns: 2,
@@ -5892,8 +6870,10 @@ var productCategoryEntity = {
5892
6870
  data: { table: T.productCategories, tenantScoped: true }
5893
6871
  };
5894
6872
  var stockLocationEntity = {
5895
- name: "Stock Location",
5896
- namePlural: "Stock Locations",
6873
+ name: "Local de Estoque",
6874
+ namePlural: "Locais de Estoque",
6875
+ nameKey: "inventory.entity.stockLocation",
6876
+ namePluralKey: "inventory.entity.stockLocation.plural",
5897
6877
  icon: "Warehouse",
5898
6878
  displayField: "name",
5899
6879
  formColumns: 2,
@@ -5901,8 +6881,8 @@ var stockLocationEntity = {
5901
6881
  fieldGroups: [{ id: "identity", label: "Identifica\xE7\xE3o", columns: 2 }, { ...RECORD_GROUP }],
5902
6882
  defaultSort: "name",
5903
6883
  fields: [
5904
- { key: "name", label: "Name", type: "text", required: true, showInTable: true, group: "identity" },
5905
- { key: "description", label: "Description", type: "textarea", showInTable: true, group: "identity" },
6884
+ { key: "name", label: "Nome", type: "text", required: true, showInTable: true, group: "identity" },
6885
+ { key: "description", label: "Descri\xE7\xE3o", type: "textarea", showInTable: true, group: "identity" },
5906
6886
  ...recordRailFields({ tagsTable: T.stockLocations, activeLabel: "Ativo", unit: true })
5907
6887
  ],
5908
6888
  data: { table: T.stockLocations, tenantScoped: true }
@@ -5927,7 +6907,7 @@ var inventoryRegistries = [
5927
6907
  id: "stock-locations",
5928
6908
  entity: stockLocationEntity,
5929
6909
  icon: "Warehouse",
5930
- description: "Physical storage locations for inventory",
6910
+ description: "Onde a mercadoria fica guardada",
5931
6911
  display: "table"
5932
6912
  },
5933
6913
  // --- Seeded defaults, still editable by the tenant ---
@@ -6233,6 +7213,54 @@ function buildInventoryOnboarding() {
6233
7213
 
6234
7214
  // src/locales/en.ts
6235
7215
  var en = {
7216
+ "inventory.nav.panel": "Panel",
7217
+ "inventory.panel.columnProduct": "Product",
7218
+ "inventory.panel.columnLocation": "Location",
7219
+ "inventory.panel.columnBatch": "Batch",
7220
+ "inventory.panel.columnExpiry": "Expiry",
7221
+ "inventory.panel.columnQuantity": "Qty / min",
7222
+ "inventory.panel.columnValue": "Value",
7223
+ "inventory.panel.empty": "No stock position for this selection.",
7224
+ "inventory.panel.totalLines": "Lines",
7225
+ "inventory.panel.totalValue": "Stock value",
7226
+ "inventory.panel.unsupported": "This app does not expose the stock panel.",
7227
+ "inventory.filters.stockState": "Stock status",
7228
+ "inventory.filters.health.negative": "Negative",
7229
+ "inventory.filters.health.below_min": "Below min",
7230
+ "inventory.filters.health.zero": "Zero",
7231
+ "inventory.filters.health.ok": "OK",
7232
+ "inventory.filters.expiry": "Expiry",
7233
+ "inventory.filters.expiry.all": "All",
7234
+ "inventory.filters.expiry.expired": "Expired",
7235
+ "inventory.filters.expiry.le30": "Expires in 30 days",
7236
+ "inventory.filters.expiry.le60": "Expires in 60 days",
7237
+ "inventory.filters.expiry.le90": "Expires in 90 days",
7238
+ "inventory.filters.expiry.none": "No expiry",
7239
+ "inventory.filters.batchPlaceholder": "Batch",
7240
+ "inventory.filters.productPlaceholder": "Search product...",
7241
+ "inventory.filters.countState": "Count state",
7242
+ "inventory.filters.countAll": "All",
7243
+ "inventory.filters.countPending": "To count",
7244
+ "inventory.filters.countDone": "Counted",
7245
+ "inventory.filters.itemSearchPlaceholder": "Search product or code...",
7246
+ "inventory.filters.title": "Filters",
7247
+ "inventory.filters.apply": "Search",
7248
+ "inventory.filters.pending": "Filters changed",
7249
+ "inventory.filters.pendingHint": "The list still shows the previous selection. Click Search to apply.",
7250
+ "inventory.filters.clear": "Clear",
7251
+ "inventory.filters.more": "More filters",
7252
+ "inventory.filters.less": "Fewer filters",
7253
+ "inventory.filters.from": "From",
7254
+ "inventory.filters.to": "To",
7255
+ "inventory.filters.allTypes": "All types",
7256
+ "inventory.filters.location": "Location",
7257
+ "inventory.filters.allLocations": "All locations",
7258
+ "inventory.filters.category": "Category",
7259
+ "inventory.filters.allCategories": "All categories",
7260
+ "inventory.filters.kind": "Item type",
7261
+ "inventory.filters.allKinds": "All",
7262
+ "inventory.filters.status": "Status",
7263
+ "inventory.filters.allStatuses": "All",
6236
7264
  "inventory.stock.pendingRetry": "Confirmation is pending. Do not create another entry. Retry the same details before leaving this screen.",
6237
7265
  "inventory.stock.refreshAfterCommit": "Movement recorded. Refresh the screen to see the balance.",
6238
7266
  "inventory.dashboard.activeItems": "Active items",
@@ -6259,6 +7287,10 @@ var en = {
6259
7287
  "inventory.history.newEntry": "New entry",
6260
7288
  "inventory.history.newExit": "Exit",
6261
7289
  "inventory.nav.recipes": "Recipes",
7290
+ "inventory.entity.stockLocation": "Stock Location",
7291
+ "inventory.entity.stockLocation.plural": "Stock Locations",
7292
+ "inventory.entity.productCategory": "Category",
7293
+ "inventory.entity.productCategory.plural": "Categories",
6262
7294
  "registry.suppliers": "Suppliers",
6263
7295
  "registry.suppliers.description": "Who you buy from",
6264
7296
  "registry.product-categories": "Categories",
@@ -6270,7 +7302,7 @@ var en = {
6270
7302
  "inventory.history.searchPlaceholder": "Search movements...",
6271
7303
  "inventory.history.subtitle": "All stock movements",
6272
7304
  "inventory.history.title": "Stock History",
6273
- "inventory.nav.dashboard": "Dashboard",
7305
+ "inventory.nav.dashboard": "Summary",
6274
7306
  "inventory.nav.entry": "Entry",
6275
7307
  "inventory.nav.exit": "Exit",
6276
7308
  "inventory.nav.history": "History",
@@ -6709,6 +7741,54 @@ var en = {
6709
7741
 
6710
7742
  // src/locales/pt-BR.ts
6711
7743
  var ptBR = {
7744
+ "inventory.nav.panel": "Painel",
7745
+ "inventory.panel.columnProduct": "Produto",
7746
+ "inventory.panel.columnLocation": "Local",
7747
+ "inventory.panel.columnBatch": "Lote",
7748
+ "inventory.panel.columnExpiry": "Validade",
7749
+ "inventory.panel.columnQuantity": "Qtd / m\xEDn.",
7750
+ "inventory.panel.columnValue": "Valor",
7751
+ "inventory.panel.empty": "Nenhuma posi\xE7\xE3o de estoque para este recorte.",
7752
+ "inventory.panel.totalLines": "Linhas",
7753
+ "inventory.panel.totalValue": "Valor em estoque",
7754
+ "inventory.panel.unsupported": "Este aplicativo n\xE3o exp\xF5e o painel de estoque.",
7755
+ "inventory.filters.stockState": "Status do estoque",
7756
+ "inventory.filters.health.negative": "Negativo",
7757
+ "inventory.filters.health.below_min": "Abaixo do m\xEDn.",
7758
+ "inventory.filters.health.zero": "Zerado",
7759
+ "inventory.filters.health.ok": "OK",
7760
+ "inventory.filters.expiry": "Validade",
7761
+ "inventory.filters.expiry.all": "Todas",
7762
+ "inventory.filters.expiry.expired": "Vencidos",
7763
+ "inventory.filters.expiry.le30": "Vence em 30 dias",
7764
+ "inventory.filters.expiry.le60": "Vence em 60 dias",
7765
+ "inventory.filters.expiry.le90": "Vence em 90 dias",
7766
+ "inventory.filters.expiry.none": "Sem validade",
7767
+ "inventory.filters.batchPlaceholder": "Lote",
7768
+ "inventory.filters.productPlaceholder": "Buscar produto...",
7769
+ "inventory.filters.countState": "Situa\xE7\xE3o da contagem",
7770
+ "inventory.filters.countAll": "Todas",
7771
+ "inventory.filters.countPending": "A contar",
7772
+ "inventory.filters.countDone": "Contadas",
7773
+ "inventory.filters.itemSearchPlaceholder": "Buscar produto ou c\xF3digo...",
7774
+ "inventory.filters.title": "Filtros",
7775
+ "inventory.filters.apply": "Buscar",
7776
+ "inventory.filters.pending": "Filtros alterados",
7777
+ "inventory.filters.pendingHint": "A lista ainda mostra o recorte anterior. Clique em Buscar para aplicar.",
7778
+ "inventory.filters.clear": "Limpar",
7779
+ "inventory.filters.more": "Mais filtros",
7780
+ "inventory.filters.less": "Menos filtros",
7781
+ "inventory.filters.from": "De",
7782
+ "inventory.filters.to": "At\xE9",
7783
+ "inventory.filters.allTypes": "Todos os tipos",
7784
+ "inventory.filters.location": "Local",
7785
+ "inventory.filters.allLocations": "Todos os locais",
7786
+ "inventory.filters.category": "Categoria",
7787
+ "inventory.filters.allCategories": "Todas as categorias",
7788
+ "inventory.filters.kind": "Tipo de item",
7789
+ "inventory.filters.allKinds": "Todos",
7790
+ "inventory.filters.status": "Situa\xE7\xE3o",
7791
+ "inventory.filters.allStatuses": "Todas",
6712
7792
  "inventory.stock.pendingRetry": "A confirma\xE7\xE3o ainda est\xE1 pendente. N\xE3o fa\xE7a outro lan\xE7amento. Tente novamente com os mesmos dados antes de sair desta tela.",
6713
7793
  "inventory.stock.refreshAfterCommit": "Movimento registrado. Atualize a tela para consultar o saldo.",
6714
7794
  "inventory.dashboard.activeItems": "Itens ativos",
@@ -6735,6 +7815,10 @@ var ptBR = {
6735
7815
  "inventory.history.newEntry": "Nova entrada",
6736
7816
  "inventory.history.newExit": "Sa\xEDda",
6737
7817
  "inventory.nav.recipes": "Receitas",
7818
+ "inventory.entity.stockLocation": "Local de Estoque",
7819
+ "inventory.entity.stockLocation.plural": "Locais de Estoque",
7820
+ "inventory.entity.productCategory": "Categoria",
7821
+ "inventory.entity.productCategory.plural": "Categorias",
6738
7822
  "registry.suppliers": "Fornecedores",
6739
7823
  "registry.suppliers.description": "De quem voc\xEA compra",
6740
7824
  "registry.product-categories": "Categorias",
@@ -6746,7 +7830,7 @@ var ptBR = {
6746
7830
  "inventory.history.searchPlaceholder": "Buscar movimenta\xE7\xF5es...",
6747
7831
  "inventory.history.subtitle": "Todas as movimenta\xE7\xF5es de estoque",
6748
7832
  "inventory.history.title": "Hist\xF3rico de Estoque",
6749
- "inventory.nav.dashboard": "Painel",
7833
+ "inventory.nav.dashboard": "Resumo",
6750
7834
  "inventory.nav.entry": "Entrada",
6751
7835
  "inventory.nav.exit": "Sa\xEDda",
6752
7836
  "inventory.nav.history": "Hist\xF3rico",
@@ -15360,7 +16444,8 @@ function resolveConfig(options) {
15360
16444
  currency: { ...DEFAULT_CURRENCY, ...options?.currency },
15361
16445
  productTypes: options?.productTypes ?? DEFAULT_PRODUCT_TYPES,
15362
16446
  locations: options?.locations ?? [],
15363
- moduleNav: options?.moduleNav
16447
+ moduleNav: options?.moduleNav,
16448
+ extraViews: options?.extraViews ?? []
15364
16449
  };
15365
16450
  }
15366
16451
  function createInventoryDataProvider() {