@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/context.d.ts +12 -0
- package/dist/context.d.ts.map +1 -1
- package/dist/data/mock.d.ts.map +1 -1
- package/dist/data/registries.d.ts.map +1 -1
- package/dist/data/supabase.d.ts.map +1 -1
- package/dist/data/types.d.ts +3 -1
- package/dist/data/types.d.ts.map +1 -1
- package/dist/index.d.ts +33 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1199 -114
- package/dist/index.js.map +1 -1
- package/dist/lib/stock-health.d.ts +14 -0
- package/dist/lib/stock-health.d.ts.map +1 -0
- package/dist/locales/en.d.ts.map +1 -1
- package/dist/locales/pt-BR.d.ts.map +1 -1
- package/dist/types.d.ts +36 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/views/GapScreens.d.ts +14 -0
- package/dist/views/GapScreens.d.ts.map +1 -0
- package/dist/views/InventoryPage.d.ts.map +1 -1
- package/dist/views/MovementHistoryView.d.ts.map +1 -1
- package/dist/views/ProductListView.d.ts.map +1 -1
- package/dist/views/StockCountSessionView.d.ts.map +1 -1
- package/dist/views/StockCountsView.d.ts.map +1 -1
- package/dist/views/StockPanelView.d.ts +3 -0
- package/dist/views/StockPanelView.d.ts.map +1 -0
- package/package.json +3 -3
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,
|
|
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,
|
|
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:
|
|
648
|
+
productType: emVigor.productType,
|
|
649
|
+
categoryId: emVigor.categoryId || void 0,
|
|
618
650
|
search: search || void 0,
|
|
619
|
-
isActive:
|
|
651
|
+
isActive: emVigor.isActive === void 0 ? void 0 : emVigor.isActive === "true",
|
|
620
652
|
page,
|
|
621
653
|
pageSize: PAGE_SIZE
|
|
622
|
-
}), [
|
|
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
|
-
|
|
693
|
-
|
|
708
|
+
const filtros = /* @__PURE__ */ jsxs(
|
|
709
|
+
FilterPanel,
|
|
694
710
|
{
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
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({
|
|
1444
|
-
|
|
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
|
-
|
|
1628
|
-
|
|
2134
|
+
const filtros = /* @__PURE__ */ jsxs(
|
|
2135
|
+
FilterPanel,
|
|
1629
2136
|
{
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
//
|
|
3494
|
-
//
|
|
3495
|
-
//
|
|
3496
|
-
//
|
|
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
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
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-
|
|
3503
|
-
{ id: "stock-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
3516
|
-
//
|
|
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-
|
|
3519
|
-
label: t2("inventory.nav.
|
|
3520
|
-
icon: "
|
|
3521
|
-
active: view === "stock-
|
|
3522
|
-
onClick: () => navigate("stock-
|
|
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: "
|
|
5896
|
-
namePlural: "
|
|
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: "
|
|
5905
|
-
{ key: "description", label: "
|
|
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: "
|
|
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": "
|
|
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": "
|
|
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() {
|