@fayz-ai/plugin-inventory 0.13.2 → 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';
4
- import { createSafeDataProvider, registerTranslations, countByTenant, useTranslation, getActiveUnitId, createPendingCommand, errorMessage, useDataChanged, getSupabaseClientOptional, getActiveTenantId } from '@fayz-ai/core';
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
+ 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
@@ -554,13 +554,22 @@ function buildProductEntity(t2, productTypes, currency, onStockChanged, provider
554
554
  relation: { table: T.stockLocations, labelField: "name" },
555
555
  placeholder: t2("inventory.productForm.optional"),
556
556
  showInTable: false
557
- }
557
+ },
558
+ // O rail de todo cadastro. `isActive` já era LIDO na lista (a linha
559
+ // aparece esmaecida) e não tinha campo: dava para ver que um produto
560
+ // estava inativo e não dava para reativá-lo.
561
+ ...recordRailFields({ tagsTable: "products", activeLabel: t2("common.active") })
558
562
  ];
559
563
  return {
560
564
  name: t2("inventory.stock.product"),
561
565
  namePlural: t2("inventory.nav.products"),
562
566
  icon: "Package",
563
567
  displayField: "name",
568
+ // MOSTRAR e TROCAR são coisas diferentes. `image` só existe quando há
569
+ // provider, porque é ele que sabe enviar o arquivo — mas sem `imageField` a
570
+ // página do produto ficava sem foto mesmo com a foto na linha, enquanto a
571
+ // lista a desenhava. O campo responde por desenhar; `image`, por substituir.
572
+ imageField: "imageUrl",
564
573
  image: provider?.uploadProductImage ? productImage(provider) : void 0,
565
574
  fields,
566
575
  fieldGroups: [
@@ -568,9 +577,15 @@ function buildProductEntity(t2, productTypes, currency, onStockChanged, provider
568
577
  { id: "classification", label: t2("inventory.productForm.classification"), description: t2("inventory.productForm.classificationDesc"), columns: 1 },
569
578
  { id: "pricing", label: t2("inventory.productForm.pricing"), columns: 3 },
570
579
  { id: "stock", label: t2("inventory.productForm.stockLevels"), description: t2("inventory.productForm.stockLevelsDesc"), columns: 2 },
571
- { id: "organization", label: t2("inventory.productForm.organization"), description: t2("inventory.productForm.organizationDesc"), columns: 2 }
580
+ { id: "organization", label: t2("inventory.productForm.organization"), description: t2("inventory.productForm.organizationDesc"), columns: 2 },
581
+ { ...RECORD_GROUP }
582
+ ],
583
+ // Duas facetas: o TIPO (o que a coisa é) e a SITUAÇÃO (se vale). A
584
+ // segunda abre em «Ativos», como toda lista de cadastro.
585
+ facets: [
586
+ { field: "productType", allLabel: t2("crud.list.allFacet") },
587
+ activeFacet({ active: t2("common.active"), inactive: t2("common.inactive"), all: t2("crud.list.allFacet") })
572
588
  ],
573
- facets: [{ field: "productType", allLabel: t2("crud.list.allFacet") }],
574
589
  detailTabs: [
575
590
  {
576
591
  id: "stock",
@@ -590,21 +605,53 @@ function ProductListView({ onNew, onOpen }) {
590
605
  const { currency, productTypes } = useInventoryConfig();
591
606
  const products2 = useInventoryStore((s) => s.products);
592
607
  const productsTotal = useInventoryStore((s) => s.productsTotal);
593
- useInventoryStore((s) => s.productsLoading);
608
+ const productsLoading = useInventoryStore((s) => s.productsLoading);
594
609
  const fetchProducts = useInventoryStore((s) => s.fetchProducts);
595
610
  const [search, setSearch] = useState("");
596
611
  const [typeFilter, setTypeFilter] = useState();
597
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]);
598
625
  const [page, setPage] = useState(1);
599
626
  const [loadedOnce, setLoadedOnce] = useState(false);
600
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);
601
647
  const query = useMemo(() => ({
602
- productType: typeFilter,
648
+ productType: emVigor.productType,
649
+ categoryId: emVigor.categoryId || void 0,
603
650
  search: search || void 0,
604
- isActive: activeFilter === void 0 ? void 0 : activeFilter === "true",
651
+ isActive: emVigor.isActive === void 0 ? void 0 : emVigor.isActive === "true",
605
652
  page,
606
653
  pageSize: PAGE_SIZE
607
- }), [typeFilter, search, activeFilter, page]);
654
+ }), [emVigor, search, page]);
608
655
  useEffect(() => {
609
656
  let active = true;
610
657
  Promise.resolve(fetchProducts(query)).finally(() => {
@@ -615,22 +662,6 @@ function ProductListView({ onNew, onOpen }) {
615
662
  };
616
663
  }, [query]);
617
664
  const entity = useMemo(() => buildProductEntity(t2, productTypes, currency), [t2, productTypes, currency]);
618
- const facets = useMemo(() => [
619
- {
620
- field: "productType",
621
- allLabel: t2("crud.list.allFacet"),
622
- options: productTypes.map((p) => ({ value: p.value, label: p.label }))
623
- },
624
- {
625
- field: "isActive",
626
- label: t2("inventory.productList.filterStatus"),
627
- allLabel: t2("inventory.productList.filterAllStatuses"),
628
- options: [
629
- { value: "true", label: t2("inventory.productList.filterActive") },
630
- { value: "false", label: t2("inventory.productList.filterInactive") }
631
- ]
632
- }
633
- ], [productTypes, t2]);
634
665
  const runBulk = useCallback(async (rows, isActive) => {
635
666
  const targets = rows.filter((p) => (p.isActive ?? true) !== isActive);
636
667
  if (targets.length === 0) {
@@ -674,46 +705,122 @@ function ProductListView({ onNew, onOpen }) {
674
705
  }
675
706
  ];
676
707
  }, [can, bulkBusy, runBulk, t2]);
677
- return /* @__PURE__ */ jsx(
678
- CrudListView,
708
+ const filtros = /* @__PURE__ */ jsxs(
709
+ FilterPanel,
679
710
  {
680
- entityDef: entity,
681
- items: loadedOnce ? products2 : null,
682
- total: productsTotal,
683
- search,
684
- onSearchChange: (value) => {
685
- setPage(1);
686
- setSearch(value);
687
- },
688
- searchPlaceholder: t2("inventory.productList.searchPlaceholder"),
689
- facets,
690
- activeFilters: { productType: typeFilter, isActive: activeFilter },
691
- onFacetChange: (field, value) => {
692
- setPage(1);
693
- if (field === "isActive") setActiveFilter(value);
694
- else setTypeFilter(value);
695
- },
696
- page,
697
- pageSize: PAGE_SIZE,
698
- onPageChange: setPage,
699
- bulkActions,
700
- onNew,
701
- aiCreate: {
702
- hint: t2("inventory.productList.askPlaceholder"),
703
- description: t2("inventory.productList.aiHint"),
704
- intro: {
705
- role: "operations",
706
- persona: t2("inventory.productList.askPersona"),
707
- pitch: t2("inventory.productList.askPitch"),
708
- description: t2("inventory.productList.askDescription"),
709
- suggestions: [0, 1, 2].map((i) => ({ label: t2(`inventory.productList.askSuggestion.${i}`) }))
710
- }
711
- },
712
- addLabel: t2("inventory.productList.newProduct"),
713
- onRowClick: (row) => onOpen?.(row.id),
714
- 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
+ ]
715
786
  }
716
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
+ ] });
717
824
  }
718
825
  function ProductCrudForm({ editId, onSaved }) {
719
826
  const t2 = useTranslation();
@@ -1423,10 +1530,49 @@ function MovementHistoryView({ onViewDetail, onNew, onNewExit } = {}) {
1423
1530
  const movements2 = useInventoryStore((s) => s.movements);
1424
1531
  const movementsLoading = useInventoryStore((s) => s.movementsLoading);
1425
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]);
1426
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
+ };
1427
1568
  useEffect(() => {
1428
- fetchMovements({ search: search || void 0 });
1429
- }, [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]);
1430
1576
  const columns = useMemo(() => [
1431
1577
  {
1432
1578
  accessorKey: "movementDate",
@@ -1514,6 +1660,69 @@ function MovementHistoryView({ onViewDetail, onNew, onNewExit } = {}) {
1514
1660
  ], [currency, t2]);
1515
1661
  return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
1516
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
+ ),
1517
1726
  /* @__PURE__ */ jsx(
1518
1727
  ListView,
1519
1728
  {
@@ -1538,6 +1747,290 @@ function MovementHistoryView({ onViewDetail, onNew, onNewExit } = {}) {
1538
1747
  )
1539
1748
  ] });
1540
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"];
1541
2034
  var STATUS_TONE = {
1542
2035
  open: "bg-muted text-muted-foreground",
1543
2036
  counting: "bg-info-soft text-info-soft-foreground",
@@ -1594,6 +2087,35 @@ function StockCountsView({ onNew, onOpen }) {
1594
2087
  const [sessions, setSessions] = useState([]);
1595
2088
  const [loadedOnce, setLoadedOnce] = useState(false);
1596
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
+ };
1597
2119
  const load = useCallback(async () => {
1598
2120
  setSessions(await provider.getCountSessions());
1599
2121
  setLoadedOnce(true);
@@ -1603,29 +2125,114 @@ function StockCountsView({ onNew, onOpen }) {
1603
2125
  }, [load]);
1604
2126
  const rows = useMemo(() => {
1605
2127
  const term = search.trim().toLowerCase();
1606
- 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) => ({
1607
2129
  ...session,
1608
2130
  label: session.reference || `${session.stockLocationName ?? ""} \xB7 ${session.openedAt.slice(0, 10)}`.trim()
1609
2131
  })).filter((row) => !term || row.label.toLowerCase().includes(term));
1610
- }, [sessions, search]);
2132
+ }, [sessions, search, locationId, categoryId, statusFilter, dateFrom, dateTo]);
1611
2133
  const entity = useMemo(() => buildCountEntity(t2), [t2]);
1612
- return /* @__PURE__ */ jsx(
1613
- CrudListView,
2134
+ const filtros = /* @__PURE__ */ jsxs(
2135
+ FilterPanel,
1614
2136
  {
1615
- entityDef: entity,
1616
- items: loadedOnce ? rows : null,
1617
- total: rows.length,
1618
- search,
1619
- onSearchChange: setSearch,
1620
- searchPlaceholder: t2("inventory.counts.searchPlaceholder"),
1621
- onNew,
1622
- addLabel: t2("inventory.counts.newCount"),
1623
- onRowClick: (row) => onOpen?.(row.id),
1624
- emptyMessage: t2("inventory.counts.empty"),
1625
- onRefresh: () => load(),
1626
- 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
+ ]
1627
2214
  }
1628
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
+ ] });
1629
2236
  }
1630
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";
1631
2238
  function StockCountFormView({ onOpened, onCancel }) {
@@ -1800,6 +2407,17 @@ function StockCountSessionView({ sessionId, onBack }) {
1800
2407
  void load();
1801
2408
  }, [load]);
1802
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]);
1803
2421
  const closed = session?.status === "closed" || session?.status === "cancelled";
1804
2422
  const hideVariance = (session?.blind ?? false) && !closed;
1805
2423
  async function commit(item, raw) {
@@ -1911,8 +2529,49 @@ function StockCountSessionView({ sessionId, onBack }) {
1911
2529
  /* @__PURE__ */ jsx(Button, { size: "sm", onClick: requestClose, children: t2("inventory.counts.close") })
1912
2530
  ] })
1913
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
+ ] }),
1914
2573
  /* @__PURE__ */ jsxs("div", { className: "rounded-card border bg-card shadow-sm divide-y", children: [
1915
- items.map((item, index) => {
2574
+ itensVisiveis.map((item, index) => {
1916
2575
  const variance = lineVariance(item);
1917
2576
  return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 px-4 py-3", children: [
1918
2577
  /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
@@ -1947,7 +2606,7 @@ function StockCountSessionView({ sessionId, onBack }) {
1947
2606
  )
1948
2607
  ] }, item.id);
1949
2608
  }),
1950
- 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") })
1951
2610
  ] }),
1952
2611
  /* @__PURE__ */ jsx(
1953
2612
  ConfirmDialog,
@@ -3372,6 +4031,102 @@ function UsageAuditView() {
3372
4031
  )
3373
4032
  ] });
3374
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
+ }
3375
4130
  var INVENTORY_MODULE_DEFAULTS = {
3376
4131
  recipes: true
3377
4132
  };
@@ -3464,54 +4219,106 @@ function InventoryOnboarding({ onComplete }) {
3464
4219
  }
3465
4220
  function buildNav(config, view, navigate, t2) {
3466
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.
3467
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.
3468
4231
  ...config.modules.products ? [{
3469
4232
  id: "products",
3470
4233
  label: t2("inventory.nav.products"),
3471
4234
  icon: "Package",
3472
- active: view.startsWith("products"),
4235
+ active: view.startsWith("products") || view === "product-categories" || view === "bulk-products",
3473
4236
  children: [
3474
4237
  { id: "products-new", kind: "action", label: t2("inventory.nav.new"), active: view === "products-new", onClick: () => navigate("products-new") },
3475
- { 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") }
3476
4241
  ]
3477
4242
  }] : [],
3478
- // Movimentações é UMA tela: o que entrou e o que saiu. A pílula "Histórico"
3479
- // ao lado da aba repetia o nome da própria aba (e o título da página já diz
3480
- // "Histórico de Estoque"), então a aba passa a SER o destino — filha única
3481
- // 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.
3482
4247
  {
3483
- id: "stock",
3484
- label: t2("inventory.nav.stock"),
3485
- 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"),
3486
4255
  children: [
3487
- { id: "stock-entry", kind: "action", label: t2("inventory.nav.entry"), active: view === "stock-entry", onClick: () => navigate("stock-entry") },
3488
- { id: "stock-exit", kind: "action", label: t2("inventory.nav.exit"), active: view === "stock-exit", onClick: () => navigate("stock-exit") },
3489
- { 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") }
3490
4258
  ]
3491
4259
  },
3492
- // Contagem e Auditoria de Uso são ABAS, não uma rubrica com pílula dentro.
4260
+ // MOVIMENTAÇÕES É UMA LINHA SÓ, e ela leva direto para a lista.
3493
4261
  //
3494
- // Conferir o que o sistema diz contra o que existe na prateleira são duas
3495
- // TELAS, cada uma com a sua lista e o seu fechamento — e agrupá-las sob
3496
- // "Inventário" punha o segundo nível num controle segmentado à direita da
3497
- // faixa, que é onde ninguém procura uma tela. A faixa é o lugar de uma
3498
- // tela; a pílula é o lugar de um recorte da mesma tela.
3499
- //
3500
- // "Nova contagem" saiu junto: era uma AÇÃO dentro de navegação, e a lista
3501
- // 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.
3502
4266
  {
3503
- id: "stock-counts",
3504
- label: t2("inventory.nav.counts"),
3505
- icon: "ClipboardCheck",
3506
- active: view === "stock-counts" || view.startsWith("stock-counts"),
3507
- 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")
3508
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.
3509
4279
  {
3510
4280
  id: "stock-usage-audit",
3511
4281
  label: t2("inventory.nav.usageAudit"),
3512
4282
  icon: "ScanSearch",
3513
4283
  active: view === "stock-usage-audit",
3514
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
+ ]
3515
4322
  }
3516
4323
  ];
3517
4324
  if (config.modules.recipes) {
@@ -3526,6 +4333,26 @@ function buildNav(config, view, navigate, t2) {
3526
4333
  ]
3527
4334
  });
3528
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
+ }
3529
4356
  return items;
3530
4357
  }
3531
4358
  function InventoryPage({ config: appConfig, provider, store, registries }) {
@@ -3638,6 +4465,7 @@ function InventoryPage({ config: appConfig, provider, store, registries }) {
3638
4465
  } },
3639
4466
  // One route id for both: `/inventory/stock/counts/<uuid>` parses back as
3640
4467
  // `stock-counts:<id>`, not `stock-counts-detail:<id>`.
4468
+ { id: "stock-panel", render: () => /* @__PURE__ */ jsx(StockPanelView, {}) },
3641
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}`) }) },
3642
4470
  { id: "stock-usage-audit", render: () => /* @__PURE__ */ jsx(UsageAuditView, {}) },
3643
4471
  { id: "stock-counts-new", render: () => /* @__PURE__ */ jsx(StockCountFormView, { onOpened: (sid) => navigate(`stock-counts:${sid}`), onCancel: () => navigate("stock-counts") }) },
@@ -3645,6 +4473,40 @@ function InventoryPage({ config: appConfig, provider, store, registries }) {
3645
4473
  { id: "recipes-list", render: () => /* @__PURE__ */ jsx(RecipesView, { onNew: () => navigate("recipes-new"), onView: (id) => navigate(`recipes-detail:${id}`), onNewForProduct: (pid) => navigate(`recipes-new:${pid}`) }) },
3646
4474
  { id: "recipes-new", render: ({ id }) => /* @__PURE__ */ jsx(RecipeFormView, { productId: id, onSaved: (rid) => rid ? navigate(`recipes-detail:${rid}`) : navigate("recipes-list") }) },
3647
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, {}) },
3648
4510
  { id: "dashboard", render: () => /* @__PURE__ */ jsx(DashboardView, {}) }
3649
4511
  ], "dashboard");
3650
4512
  return /* @__PURE__ */ jsx(InventoryContextProvider, { config, provider, store, children: /* @__PURE__ */ jsx(
@@ -3866,6 +4728,14 @@ function createInventoryDashboardWidgets(ctx2) {
3866
4728
  ];
3867
4729
  }
3868
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
+
3869
4739
  // src/data/mock.ts
3870
4740
  var nextId2 = 1;
3871
4741
  function uid() {
@@ -4119,6 +4989,53 @@ function createMockInventoryProvider() {
4119
4989
  async getPositions(productId) {
4120
4990
  return store.positions.filter((p) => p.productId === productId);
4121
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
+ },
4122
5039
  // --- Stock Locations ---
4123
5040
  async getLocations() {
4124
5041
  return store.locations.filter((l) => l.isActive);
@@ -5133,6 +6050,77 @@ function createSupabaseInventoryProvider() {
5133
6050
  }
5134
6051
  return positions;
5135
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
+ },
5136
6124
  // --- Stock Locations ---
5137
6125
  async getLocations() {
5138
6126
  const { pub } = getClients();
@@ -5818,8 +6806,6 @@ function createInventoryStore(provider) {
5818
6806
  }
5819
6807
  }));
5820
6808
  }
5821
-
5822
- // src/data/registries.ts
5823
6809
  var supplierEntity = {
5824
6810
  name: "Supplier",
5825
6811
  namePlural: "Suppliers",
@@ -5851,37 +6837,53 @@ var measurementUnitEntity = {
5851
6837
  namePlural: "Measurement Units",
5852
6838
  icon: "Ruler",
5853
6839
  displayField: "name",
6840
+ formColumns: 2,
6841
+ facets: [activeFacet({ active: "Ativos", inactive: "Inativos", all: "Todos" })],
6842
+ fieldGroups: [{ id: "identity", label: "Identifica\xE7\xE3o", columns: 2 }, { ...RECORD_GROUP }],
5854
6843
  defaultSort: "name",
5855
6844
  fields: [
5856
- { key: "name", label: "Name", type: "text", required: true, showInTable: true },
5857
- { key: "abbreviation", label: "Abbreviation", type: "text", required: true, showInTable: true },
5858
- { key: "isActive", label: "Active", type: "boolean", showInTable: true, defaultValue: true }
6845
+ { key: "name", label: "Name", type: "text", required: true, showInTable: true, group: "identity" },
6846
+ { key: "abbreviation", label: "Abbreviation", type: "text", required: true, showInTable: true, group: "identity" },
6847
+ ...recordRailFields({ tagsTable: T.measurementUnits, activeLabel: "Ativo", unit: false })
5859
6848
  ],
5860
6849
  data: { table: T.measurementUnits, tenantScoped: true }
5861
6850
  };
5862
6851
  var productCategoryEntity = {
5863
6852
  name: "Product Category",
5864
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",
5865
6859
  icon: "Tag",
5866
6860
  displayField: "name",
6861
+ formColumns: 2,
6862
+ facets: [activeFacet({ active: "Ativos", inactive: "Inativos", all: "Todos" })],
6863
+ fieldGroups: [{ id: "identity", label: "Identifica\xE7\xE3o", columns: 2 }, { ...RECORD_GROUP }],
5867
6864
  defaultSort: "name",
5868
6865
  fields: [
5869
- { key: "name", label: "Name", type: "text", required: true, showInTable: true },
5870
- { key: "parentId", label: "Parent", type: "text", showInTable: false },
5871
- { key: "isActive", label: "Active", type: "boolean", showInTable: true, defaultValue: true }
6866
+ { key: "name", label: "Name", type: "text", required: true, showInTable: true, group: "identity" },
6867
+ { key: "parentId", label: "Parent", type: "text", showInTable: false, group: "identity" },
6868
+ ...recordRailFields({ tagsTable: T.productCategories, activeLabel: "Ativo", unit: false })
5872
6869
  ],
5873
6870
  data: { table: T.productCategories, tenantScoped: true }
5874
6871
  };
5875
6872
  var stockLocationEntity = {
5876
- name: "Stock Location",
5877
- namePlural: "Stock Locations",
6873
+ name: "Local de Estoque",
6874
+ namePlural: "Locais de Estoque",
6875
+ nameKey: "inventory.entity.stockLocation",
6876
+ namePluralKey: "inventory.entity.stockLocation.plural",
5878
6877
  icon: "Warehouse",
5879
6878
  displayField: "name",
6879
+ formColumns: 2,
6880
+ facets: [activeFacet({ active: "Ativos", inactive: "Inativos", all: "Todos" })],
6881
+ fieldGroups: [{ id: "identity", label: "Identifica\xE7\xE3o", columns: 2 }, { ...RECORD_GROUP }],
5880
6882
  defaultSort: "name",
5881
6883
  fields: [
5882
- { key: "name", label: "Name", type: "text", required: true, showInTable: true },
5883
- { key: "description", label: "Description", type: "textarea", showInTable: true },
5884
- { key: "isActive", label: "Active", type: "boolean", showInTable: true, defaultValue: true }
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" },
6886
+ ...recordRailFields({ tagsTable: T.stockLocations, activeLabel: "Ativo", unit: true })
5885
6887
  ],
5886
6888
  data: { table: T.stockLocations, tenantScoped: true }
5887
6889
  };
@@ -5905,7 +6907,7 @@ var inventoryRegistries = [
5905
6907
  id: "stock-locations",
5906
6908
  entity: stockLocationEntity,
5907
6909
  icon: "Warehouse",
5908
- description: "Physical storage locations for inventory",
6910
+ description: "Onde a mercadoria fica guardada",
5909
6911
  display: "table"
5910
6912
  },
5911
6913
  // --- Seeded defaults, still editable by the tenant ---
@@ -6211,6 +7213,54 @@ function buildInventoryOnboarding() {
6211
7213
 
6212
7214
  // src/locales/en.ts
6213
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",
6214
7264
  "inventory.stock.pendingRetry": "Confirmation is pending. Do not create another entry. Retry the same details before leaving this screen.",
6215
7265
  "inventory.stock.refreshAfterCommit": "Movement recorded. Refresh the screen to see the balance.",
6216
7266
  "inventory.dashboard.activeItems": "Active items",
@@ -6237,6 +7287,10 @@ var en = {
6237
7287
  "inventory.history.newEntry": "New entry",
6238
7288
  "inventory.history.newExit": "Exit",
6239
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",
6240
7294
  "registry.suppliers": "Suppliers",
6241
7295
  "registry.suppliers.description": "Who you buy from",
6242
7296
  "registry.product-categories": "Categories",
@@ -6248,7 +7302,7 @@ var en = {
6248
7302
  "inventory.history.searchPlaceholder": "Search movements...",
6249
7303
  "inventory.history.subtitle": "All stock movements",
6250
7304
  "inventory.history.title": "Stock History",
6251
- "inventory.nav.dashboard": "Dashboard",
7305
+ "inventory.nav.dashboard": "Summary",
6252
7306
  "inventory.nav.entry": "Entry",
6253
7307
  "inventory.nav.exit": "Exit",
6254
7308
  "inventory.nav.history": "History",
@@ -6687,6 +7741,54 @@ var en = {
6687
7741
 
6688
7742
  // src/locales/pt-BR.ts
6689
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",
6690
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.",
6691
7793
  "inventory.stock.refreshAfterCommit": "Movimento registrado. Atualize a tela para consultar o saldo.",
6692
7794
  "inventory.dashboard.activeItems": "Itens ativos",
@@ -6713,6 +7815,10 @@ var ptBR = {
6713
7815
  "inventory.history.newEntry": "Nova entrada",
6714
7816
  "inventory.history.newExit": "Sa\xEDda",
6715
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",
6716
7822
  "registry.suppliers": "Fornecedores",
6717
7823
  "registry.suppliers.description": "De quem voc\xEA compra",
6718
7824
  "registry.product-categories": "Categorias",
@@ -6724,7 +7830,7 @@ var ptBR = {
6724
7830
  "inventory.history.searchPlaceholder": "Buscar movimenta\xE7\xF5es...",
6725
7831
  "inventory.history.subtitle": "Todas as movimenta\xE7\xF5es de estoque",
6726
7832
  "inventory.history.title": "Hist\xF3rico de Estoque",
6727
- "inventory.nav.dashboard": "Painel",
7833
+ "inventory.nav.dashboard": "Resumo",
6728
7834
  "inventory.nav.entry": "Entrada",
6729
7835
  "inventory.nav.exit": "Sa\xEDda",
6730
7836
  "inventory.nav.history": "Hist\xF3rico",
@@ -15338,7 +16444,8 @@ function resolveConfig(options) {
15338
16444
  currency: { ...DEFAULT_CURRENCY, ...options?.currency },
15339
16445
  productTypes: options?.productTypes ?? DEFAULT_PRODUCT_TYPES,
15340
16446
  locations: options?.locations ?? [],
15341
- moduleNav: options?.moduleNav
16447
+ moduleNav: options?.moduleNav,
16448
+ extraViews: options?.extraViews ?? []
15342
16449
  };
15343
16450
  }
15344
16451
  function createInventoryDataProvider() {