浏览代码

fix inventory adj button

undefined
CANCERYS\kw093 1周前
父节点
当前提交
524bf42690
共有 3 个文件被更改,包括 22 次插入198 次删除
  1. +1
    -1
      src/app/api/inventory/actions.ts
  2. +4
    -4
      src/components/InventorySearch/InventoryLotLineTable.tsx
  3. +17
    -193
      src/components/InventorySearch/InventorySearch.tsx

+ 1
- 1
src/app/api/inventory/actions.ts 查看文件

@@ -172,7 +172,7 @@ async function fetchInventoriesLatestImpl(data: SearchInventory) {
export const fetchInventories = cache(fetchInventoriesImpl); export const fetchInventories = cache(fetchInventoriesImpl);


/** /**
* FP-MTMS Version Checklist | Functions Ref. No. 18 | v1.0.1 | 2026-09-07
* FP-MTMS Version Checklist | Functions Ref. No. 18 | v1.0.3 | 2026-09-07
* Inventory search page: latest inventory row per item (no baseUnit/uomId filter). * Inventory search page: latest inventory row per item (no baseUnit/uomId filter).
*/ */
export const fetchInventoriesLatest = cache(fetchInventoriesLatestImpl); export const fetchInventoriesLatest = cache(fetchInventoriesLatestImpl);


+ 4
- 4
src/components/InventorySearch/InventoryLotLineTable.tsx 查看文件

@@ -34,7 +34,7 @@ import dayjs from "dayjs";
import CheckIcon from "@mui/icons-material/Check"; import CheckIcon from "@mui/icons-material/Check";
import { submitStockAdjustment, StockAdjustmentLineRequest } from "@/app/api/stockAdjustment/actions"; import { submitStockAdjustment, StockAdjustmentLineRequest } from "@/app/api/stockAdjustment/actions";
import { useSession } from "next-auth/react"; import { useSession } from "next-auth/react";
import { AUTH } from "@/authorities";
import { AUTH, hasAbility } from "@/authorities";


type AdjustmentEntry = InventoryLotLineResult & { type AdjustmentEntry = InventoryLotLineResult & {
adjustedQty: number; adjustedQty: number;
@@ -59,7 +59,7 @@ interface Props {
onStockAdjustmentSuccess?: () => void | Promise<void>; onStockAdjustmentSuccess?: () => void | Promise<void>;
} }


/** FP-MTMS Version Checklist | Functions Ref. No. 3 | v1.0.5 | 2026-08-05 */
/** FP-MTMS Version Checklist | Functions Ref. No. 3 | v1.0.6 | 2026-09-07 */
const InventoryLotLineTable: React.FC<Props> = ({ const InventoryLotLineTable: React.FC<Props> = ({
inventoryLotLines, pagingController, setPagingController, totalCount, inventory, inventoryLotLines, pagingController, setPagingController, totalCount, inventory,
filterLotNo, filterLotNo,
@@ -68,8 +68,8 @@ const InventoryLotLineTable: React.FC<Props> = ({
}) => { }) => {
const { t } = useTranslation(["inventory"]); const { t } = useTranslation(["inventory"]);
const { data: session } = useSession(); const { data: session } = useSession();
const abilities = session?.user?.abilities ?? [];
const canStockAdjust = abilities.includes(AUTH.INVENTORY_ADJUST);
const abilities = session?.abilities ?? session?.user?.abilities ?? [];
const canStockAdjust = hasAbility(abilities, AUTH.INVENTORY_ADJUST);
const PRINT_PRINTER_ID_KEY = 'inventoryLotLinePrintPrinterId'; const PRINT_PRINTER_ID_KEY = 'inventoryLotLinePrintPrinterId';
const { setIsUploading } = useUploadContext(); const { setIsUploading } = useUploadContext();
const [stockTransferModalOpen, setStockTransferModalOpen] = useState(false); const [stockTransferModalOpen, setStockTransferModalOpen] = useState(false);


+ 17
- 193
src/components/InventorySearch/InventorySearch.tsx 查看文件

@@ -16,23 +16,10 @@ import {
fetchInventoryLotLines, fetchInventoryLotLines,
} from '@/app/api/inventory/actions'; } from '@/app/api/inventory/actions';
import { PrinterCombo } from '@/app/api/settings/printer'; import { PrinterCombo } from '@/app/api/settings/printer';
import { ItemCombo, fetchItemsByPage } from '@/app/api/settings/item/actions';
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
TextField,
Box,
CircularProgress,
Table,
TableBody,
TableCell,
TableHead,
TableRow,
Radio,
} from '@mui/material';
import { fetchItemsByPage } from '@/app/api/settings/item/actions';
import { useSession } from 'next-auth/react';
import { AUTH, hasAbility } from '@/authorities';
import { Button, Box } from '@mui/material';


interface Props { interface Props {
inventories: InventoryResult[]; inventories: InventoryResult[];
@@ -66,8 +53,6 @@ type ItemLookupRow = {
purchaseUnit?: string; purchaseUnit?: string;
}; };


type OpeningItemRow = ItemCombo & { code: string; name: string; type?: string };

const extractItemRecords = (res: unknown): ItemLookupRow[] => { const extractItemRecords = (res: unknown): ItemLookupRow[] => {
if (!res) return []; if (!res) return [];
if (Array.isArray(res)) return res as ItemLookupRow[]; if (Array.isArray(res)) return res as ItemLookupRow[];
@@ -77,11 +62,13 @@ const extractItemRecords = (res: unknown): ItemLookupRow[] => {
return []; return [];
}; };


/** FP-MTMS Version Checklist | Functions Ref. No. 18 | v1.0.1 | 2026-09-07 */
/** FP-MTMS Version Checklist | Functions Ref. No. 18 | v1.0.3 | 2026-09-07 */
const InventorySearch: React.FC<Props> = ({ inventories, printerCombo }) => { const InventorySearch: React.FC<Props> = ({ inventories, printerCombo }) => {
const { t } = useTranslation(['inventory', 'common', 'item']); const { t } = useTranslation(['inventory', 'common', 'item']);
const { data: session } = useSession();
const abilities = session?.abilities ?? session?.user?.abilities ?? [];
const canStockAdjust = hasAbility(abilities, AUTH.INVENTORY_ADJUST);
const searchInFlightRef = useRef(false); const searchInFlightRef = useRef(false);
const openingSearchInFlightRef = useRef(false);


const buildSyntheticInventory = useCallback((item: ItemLookupRow): InventoryResult => { const buildSyntheticInventory = useCallback((item: ItemLookupRow): InventoryResult => {
const uom = item.uomDesc || item.uom || item.purchaseUnit || ''; const uom = item.uomDesc || item.uom || item.purchaseUnit || '';
@@ -153,13 +140,6 @@ const InventorySearch: React.FC<Props> = ({ inventories, printerCombo }) => {
const [lotNoFilter, setLotNoFilter] = useState(''); const [lotNoFilter, setLotNoFilter] = useState('');
const [scannedItemId, setScannedItemId] = useState<number | null>(null); const [scannedItemId, setScannedItemId] = useState<number | null>(null);


// Opening inventory (pure opening stock for items without existing inventory)
const [openingItems, setOpeningItems] = useState<OpeningItemRow[]>([]);
const [openingModalOpen, setOpeningModalOpen] = useState(false);
const [openingSelectedItem, setOpeningSelectedItem] = useState<OpeningItemRow | null>(null);
const [openingLoading, setOpeningLoading] = useState(false);
const [openingSearchText, setOpeningSearchText] = useState('');

const defaultInputs = useMemo( const defaultInputs = useMemo(
() => ({ () => ({
itemId: '', itemId: '',
@@ -354,7 +334,7 @@ const InventorySearch: React.FC<Props> = ({ inventories, printerCombo }) => {
setInventoryLotLinesPagingController(() => defaultPagingController); setInventoryLotLinesPagingController(() => defaultPagingController);


// No inventory rows: look up item master so 0-qty rows can be selected for stock adjustment. // No inventory rows: look up item master so 0-qty rows can be selected for stock adjustment.
if (invRes?.records?.length === 0) {
if (canStockAdjust && invRes?.records?.length === 0) {
try { try {
const items = await lookupItemsByCodeOrName(query.itemCode, query.itemName); const items = await lookupItemsByCodeOrName(query.itemCode, query.itemName);
const typeFilter = query.itemType?.trim(); const typeFilter = query.itemType?.trim();
@@ -385,6 +365,7 @@ const InventorySearch: React.FC<Props> = ({ inventories, printerCombo }) => {
refetchInventoryLotLineData, refetchInventoryLotLineData,
lookupItemsByCodeOrName, lookupItemsByCodeOrName,
applyItemsAsSyntheticInventories, applyItemsAsSyntheticInventories,
canStockAdjust,
], ],
); );


@@ -430,8 +411,12 @@ const InventorySearch: React.FC<Props> = ({ inventories, printerCombo }) => {
onInventoryRowClick(target); onInventoryRowClick(target);
} else { } else {
refetchInventoryLotLineData(null, 'search', defaultPagingController); refetchInventoryLotLineData(null, 'search', defaultPagingController);
const items = await lookupItemsByCodeOrName(res?.itemCode);
if (!applyItemsAsSyntheticInventories(items)) {
if (canStockAdjust) {
const items = await lookupItemsByCodeOrName(res?.itemCode);
if (!applyItemsAsSyntheticInventories(items)) {
setSelectedInventory(null);
}
} else {
setSelectedInventory(null); setSelectedInventory(null);
} }
} }
@@ -454,73 +439,10 @@ const InventorySearch: React.FC<Props> = ({ inventories, printerCombo }) => {
refetchInventoryLotLineData, refetchInventoryLotLineData,
lookupItemsByCodeOrName, lookupItemsByCodeOrName,
applyItemsAsSyntheticInventories, applyItemsAsSyntheticInventories,
canStockAdjust,
scanUiMode, scanUiMode,
]); ]);


//console.log('', 'color: #666', inventoriesPagingController);

const handleOpenOpeningInventoryModal = useCallback(() => {
setOpeningSelectedItem(null);
setOpeningItems([]);
setOpeningSearchText('');
setOpeningModalOpen(true);
}, []);

const handleOpeningSearch = useCallback(async () => {
const trimmed = openingSearchText.trim();
if (!trimmed) {
setOpeningItems([]);
return;
}
if (openingSearchInFlightRef.current) return;
openingSearchInFlightRef.current = true;
setOpeningLoading(true);
try {
const items = trimmed.includes(' ')
? await lookupItemsByCodeOrName(undefined, trimmed)
: await lookupItemsByCodeOrName(trimmed);
const combos: OpeningItemRow[] = items.map((item) => {
const uom = item.uomDesc || item.uom || item.purchaseUnit || '';
return {
id: item.id,
label: `${item.code} - ${item.name}`,
uomId: 0,
uom,
uomDesc: uom,
code: item.code,
name: item.name,
type: item.type,
};
});
setOpeningItems(combos);
} catch (e) {
console.error('Failed to search items for opening inventory:', e);
setOpeningItems([]);
} finally {
setOpeningLoading(false);
openingSearchInFlightRef.current = false;
}
}, [openingSearchText, lookupItemsByCodeOrName]);

const handleConfirmOpeningInventory = useCallback(() => {
if (!openingSelectedItem) {
setOpeningModalOpen(false);
return;
}

applyItemsAsSyntheticInventories([
{
id: Number(openingSelectedItem.id),
code: openingSelectedItem.code,
name: openingSelectedItem.name,
type: openingSelectedItem.type,
uom: openingSelectedItem.uom,
uomDesc: openingSelectedItem.uomDesc,
},
]);
setOpeningModalOpen(false);
}, [openingSelectedItem, applyItemsAsSyntheticInventories]);

return ( return (
<> <>
<SearchBox <SearchBox
@@ -545,14 +467,6 @@ const InventorySearch: React.FC<Props> = ({ inventories, printerCombo }) => {
</Button> </Button>
</> </>
)} )}

<Button
variant="outlined"
color="secondary"
onClick={handleOpenOpeningInventoryModal}
>
{t('Add entry for items without inventory')}
</Button>
</Box> </Box>
} }
/> />
@@ -604,96 +518,6 @@ const InventorySearch: React.FC<Props> = ({ inventories, printerCombo }) => {
} }
}} }}
/> />

<Dialog
open={openingModalOpen}
onClose={() => setOpeningModalOpen(false)}
fullWidth
maxWidth="md"
>
<DialogTitle>{t('Add entry for items without inventory')}</DialogTitle>
<DialogContent sx={{ pt: 2 }}>
<Box sx={{ display: 'flex', gap: 1, mb: 2 }}>
<TextField
label={t('Item')}
fullWidth
value={openingSearchText}
onChange={(e) => setOpeningSearchText(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
handleOpeningSearch();
}
}}
sx={{ flex: 2 }}
/>
<Button
variant="contained"
onClick={handleOpeningSearch}
disabled={openingLoading}
sx={{ flex: 1 }}
>
{openingLoading ? <CircularProgress size={20} /> : t('common:Search')}
</Button>
</Box>

{openingItems.length === 0 && !openingLoading ? (
<Box sx={{ py: 1, color: 'text.secondary', fontSize: 14 }}>
{openingSearchText
? t('No data')
: t('Enter item code or name to search')}
</Box>
) : (
<Table size="small">
<TableHead>
<TableRow>
<TableCell />
<TableCell>{t('Code')}</TableCell>
<TableCell>{t('Name')}</TableCell>
<TableCell>{t('UoM')}</TableCell>
<TableCell align="right">{t('Current Stock')}</TableCell>
</TableRow>
</TableHead>
<TableBody>
{openingItems.map((it) => {
const selected = openingSelectedItem?.id === it.id;
return (
<TableRow
key={it.id}
hover
selected={selected}
onClick={() => setOpeningSelectedItem(it)}
sx={{ cursor: 'pointer' }}
>
<TableCell padding="checkbox">
<Radio checked={selected} />
</TableCell>
<TableCell>{it.code}</TableCell>
<TableCell>{it.name}</TableCell>
<TableCell>{it.uomDesc || it.uom}</TableCell>
<TableCell align="right">
{it.currentStockBalance != null ? it.currentStockBalance : '-'}
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
)}
</DialogContent>
<DialogActions>
<Button onClick={() => setOpeningModalOpen(false)}>
{t('common:Cancel')}
</Button>
<Button
variant="contained"
onClick={handleConfirmOpeningInventory}
disabled={!openingSelectedItem}
>
{t('common:Confirm')}
</Button>
</DialogActions>
</Dialog>
</> </>
); );
}; };


正在加载...
取消
保存