FPSMS-frontend
Nie możesz wybrać więcej, niż 25 tematów Tematy muszą się zaczynać od litery lub cyfry, mogą zawierać myślniki ('-') i mogą mieć do 35 znaków.

InventorySearch.tsx 22 KiB

1 rok temu
5 miesięcy temu
1 rok temu
1 rok temu
1 rok temu
5 miesięcy temu
4 miesięcy temu
4 miesięcy temu
1 rok temu
10 miesięcy temu
10 miesięcy temu
1 rok temu
1 rok temu
10 miesięcy temu
10 miesięcy temu
10 miesięcy temu
10 miesięcy temu
10 miesięcy temu
10 miesięcy temu
10 miesięcy temu
10 miesięcy temu
10 miesięcy temu
10 miesięcy temu
1 rok temu
10 miesięcy temu
1 rok temu
10 miesięcy temu
10 miesięcy temu
10 miesięcy temu
10 miesięcy temu
10 miesięcy temu
5 miesięcy temu
5 miesięcy temu
5 miesięcy temu
4 miesięcy temu
1 rok temu
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699
  1. 'use client';
  2. import { InventoryLotLineResult, InventoryResult } from '@/app/api/inventory';
  3. import { useTranslation } from 'react-i18next';
  4. import SearchBox, { Criterion } from '../SearchBox';
  5. import { useCallback, useEffect, useMemo, useState } from 'react';
  6. import { uniq, uniqBy } from 'lodash';
  7. import InventoryTable from './InventoryTable';
  8. import { defaultPagingController } from '../SearchResults/SearchResults';
  9. import InventoryLotLineTable from './InventoryLotLineTable';
  10. import { useQrCodeScannerContext } from '@/components/QrCodeScannerProvider/QrCodeScannerProvider';
  11. import {
  12. analyzeQrCode,
  13. SearchInventory,
  14. SearchInventoryLotLine,
  15. fetchInventoriesLatest,
  16. fetchInventoryLotLines,
  17. } from '@/app/api/inventory/actions';
  18. import { PrinterCombo } from '@/app/api/settings/printer';
  19. import { ItemCombo, fetchItemsWithDetails, ItemWithDetails } from '@/app/api/settings/item/actions';
  20. import {
  21. Button,
  22. Dialog,
  23. DialogActions,
  24. DialogContent,
  25. DialogTitle,
  26. TextField,
  27. Box,
  28. CircularProgress,
  29. Table,
  30. TableBody,
  31. TableCell,
  32. TableHead,
  33. TableRow,
  34. Radio,
  35. } from '@mui/material';
  36. interface Props {
  37. inventories: InventoryResult[];
  38. printerCombo?: PrinterCombo[];
  39. }
  40. type SearchQuery = Partial<
  41. Omit<
  42. InventoryResult,
  43. | "id"
  44. | "qty"
  45. | "uomCode"
  46. | "uomUdfudesc"
  47. | "germPerSmallestUnit"
  48. | "qtyPerSmallestUnit"
  49. | "itemSmallestUnit"
  50. | "price"
  51. | "description"
  52. | "category"
  53. >
  54. >;
  55. type SearchParamNames = keyof SearchQuery;
  56. /** FP-MTMS Version Checklist | Functions Ref. No. 18 | v1.0.0 | 2026-07-17 */
  57. const InventorySearch: React.FC<Props> = ({ inventories, printerCombo }) => {
  58. const { t } = useTranslation(['inventory', 'common', 'item']);
  59. const buildSyntheticInventory = useCallback(
  60. (item: ItemWithDetails): InventoryResult => ({
  61. id: 0,
  62. itemId: item.id,
  63. itemCode: item.code,
  64. itemName: item.name,
  65. itemType: 'Material',
  66. onHandQty: 0,
  67. onHoldQty: 0,
  68. unavailableQty: 0,
  69. availableQty: 0,
  70. uomCode: item.uom,
  71. uomUdfudesc: item.uomDesc,
  72. uomShortDesc: item.uom,
  73. qtyPerSmallestUnit: 1,
  74. baseUom: item.uom,
  75. price: 0,
  76. currencyName: '',
  77. status: 'active',
  78. latestMarketUnitPrice: undefined,
  79. latestMupUpdatedDate: undefined,
  80. }),
  81. [],
  82. );
  83. const getFirstItemRecord = useCallback((res: any): ItemWithDetails | null => {
  84. if (!res) return null;
  85. if (Array.isArray(res)) return (res[0] as ItemWithDetails) ?? null;
  86. if (Array.isArray(res?.records)) return (res.records[0] as ItemWithDetails) ?? null;
  87. return null;
  88. }, []);
  89. // Inventory
  90. const [filteredInventories, setFilteredInventories] = useState<InventoryResult[]>([]);
  91. const [inventoriesPagingController, setInventoriesPagingController] = useState(defaultPagingController)
  92. const [inventoriesTotalCount, setInventoriesTotalCount] = useState(0)
  93. const [selectedInventory, setSelectedInventory] = useState<InventoryResult | null>(null)
  94. // Inventory Lot Line
  95. const [filteredInventoryLotLines, setFilteredInventoryLotLines] = useState<InventoryLotLineResult[]>([]);
  96. const [inventoryLotLinesPagingController, setInventoryLotLinesPagingController] = useState(defaultPagingController)
  97. const [inventoryLotLinesTotalCount, setInventoryLotLinesTotalCount] = useState(0)
  98. // Scan-mode UI (hardware QR scanner via QrCodeScannerProvider)
  99. const qrScanner = useQrCodeScannerContext();
  100. const [scanUiMode, setScanUiMode] = useState<'idle' | 'scanning'>('idle');
  101. const [scanHoverCancel, setScanHoverCancel] = useState(false);
  102. // Resolved lot no for filtering
  103. const [lotNoFilter, setLotNoFilter] = useState('');
  104. const [scannedItemId, setScannedItemId] = useState<number | null>(null);
  105. // Opening inventory (pure opening stock for items without existing inventory)
  106. const [openingItems, setOpeningItems] = useState<ItemCombo[]>([]);
  107. const [openingModalOpen, setOpeningModalOpen] = useState(false);
  108. const [openingSelectedItem, setOpeningSelectedItem] = useState<ItemCombo | null>(null);
  109. const [openingLoading, setOpeningLoading] = useState(false);
  110. const [openingSearchText, setOpeningSearchText] = useState('');
  111. const defaultInputs = useMemo(
  112. () => ({
  113. itemId: '',
  114. itemCode: '',
  115. itemName: '',
  116. itemType: '',
  117. onHandQty: '',
  118. onHoldQty: '',
  119. unavailableQty: '',
  120. availableQty: '',
  121. currencyName: '',
  122. status: '',
  123. baseUom: '',
  124. uomShortDesc: '',
  125. latestMarketUnitPrice: '',
  126. latestMupUpdatedDate: '',
  127. }),
  128. [],
  129. );
  130. const [inputs, setInputs] = useState<Record<SearchParamNames, string>>(defaultInputs);
  131. const searchCriteria: Criterion<SearchParamNames>[] = useMemo(
  132. () => [
  133. { label: t('Code'), paramName: 'itemCode', type: 'text' },
  134. { label: t('Name'), paramName: 'itemName', type: 'text' },
  135. {
  136. label: t('Type'),
  137. paramName: 'itemType',
  138. type: 'select-labelled',
  139. options: uniq(inventories.map((i) => i.itemType)).map((type) => ({
  140. value: type,
  141. label: t(type),
  142. })),
  143. },
  144. // {
  145. // label: t("Status"),
  146. // paramName: "status",
  147. // type: "select",
  148. // options: uniq(inventories.map((i) => i.status)),
  149. // },
  150. ],
  151. [t, inventories],
  152. );
  153. // Inventory
  154. const refetchInventoryData = useCallback(
  155. async (
  156. query: Record<SearchParamNames, string>,
  157. actionType: 'reset' | 'search' | 'paging' | 'init',
  158. pagingController: typeof defaultPagingController,
  159. lotNo: string,
  160. ) => {
  161. console.log('%c Action Type 1.', 'color:red', actionType);
  162. // Avoid loading data again
  163. if (actionType === 'paging' && pagingController === defaultPagingController) {
  164. return;
  165. }
  166. console.log('%c Action Type 2.', 'color:blue', actionType);
  167. const params: SearchInventory = {
  168. code: query?.itemCode ?? '',
  169. name: query?.itemName ?? '',
  170. type: query?.itemType.toLowerCase() === 'all' ? '' : query?.itemType ?? '',
  171. lotNo: lotNo?.trim() ? lotNo.trim() : undefined,
  172. pageNum: pagingController.pageNum - 1,
  173. pageSize: pagingController.pageSize,
  174. };
  175. const response = await fetchInventoriesLatest(params);
  176. if (response) {
  177. setInventoriesTotalCount(response.total);
  178. switch (actionType) {
  179. case 'init':
  180. case 'reset':
  181. case 'search':
  182. setFilteredInventories(() => response.records);
  183. break;
  184. case 'paging':
  185. setFilteredInventories((fi) =>
  186. uniqBy([...fi, ...response.records], 'itemId'),
  187. );
  188. }
  189. }
  190. return response;
  191. },
  192. [],
  193. );
  194. useEffect(() => {
  195. refetchInventoryData(defaultInputs, 'init', defaultPagingController, '');
  196. }, [defaultInputs, refetchInventoryData]);
  197. useEffect(() => {
  198. // if (!isEqual(inventoriesPagingController, defaultPagingController)) {
  199. refetchInventoryData(inputs, 'paging', inventoriesPagingController, lotNoFilter)
  200. // }
  201. }, [inventoriesPagingController, inputs, lotNoFilter, refetchInventoryData])
  202. // Inventory Lot Line
  203. const refetchInventoryLotLineData = useCallback(
  204. async (
  205. itemId: number | null,
  206. actionType: 'reset' | 'search' | 'paging',
  207. pagingController: typeof defaultPagingController,
  208. ) => {
  209. if (!itemId) {
  210. setSelectedInventory(null);
  211. setInventoryLotLinesTotalCount(0);
  212. setFilteredInventoryLotLines([]);
  213. return;
  214. }
  215. // Avoid loading data again
  216. if (actionType === 'paging' && pagingController === defaultPagingController) {
  217. return;
  218. }
  219. const params: SearchInventoryLotLine = {
  220. itemId,
  221. pageNum: pagingController.pageNum - 1,
  222. pageSize: pagingController.pageSize,
  223. };
  224. const response = await fetchInventoryLotLines(params);
  225. if (response) {
  226. setInventoryLotLinesTotalCount(response.total);
  227. switch (actionType) {
  228. case 'reset':
  229. case 'search':
  230. setFilteredInventoryLotLines(() => response.records);
  231. break;
  232. case 'paging':
  233. setFilteredInventoryLotLines((fi) => uniqBy([...fi, ...response.records], 'id'));
  234. }
  235. }
  236. },
  237. [],
  238. );
  239. useEffect(() => {
  240. // if (!isEqual(inventoryLotLinesPagingController, defaultPagingController)) {
  241. refetchInventoryLotLineData(selectedInventory?.itemId ?? null, 'paging', inventoryLotLinesPagingController)
  242. // }
  243. }, [inventoryLotLinesPagingController])
  244. // Reset
  245. const onReset = useCallback(() => {
  246. refetchInventoryData(defaultInputs, 'reset', defaultPagingController, '');
  247. refetchInventoryLotLineData(null, 'reset', defaultPagingController);
  248. // setFilteredInventories(inventories);
  249. setLotNoFilter('');
  250. setScannedItemId(null);
  251. setScanUiMode('idle');
  252. setScanHoverCancel(false);
  253. qrScanner.stopScan();
  254. qrScanner.resetScan();
  255. setInputs(() => defaultInputs)
  256. setInventoriesPagingController(() => defaultPagingController)
  257. setInventoryLotLinesPagingController(() => defaultPagingController)
  258. }, [defaultInputs, qrScanner, refetchInventoryData, refetchInventoryLotLineData]);
  259. // Click Row
  260. const onInventoryRowClick = useCallback(
  261. (item: InventoryResult) => {
  262. refetchInventoryLotLineData(item.itemId, 'search', defaultPagingController);
  263. setSelectedInventory(item);
  264. setInventoryLotLinesPagingController(() => defaultPagingController);
  265. },
  266. [refetchInventoryLotLineData],
  267. );
  268. // On Search
  269. const onSearch = useCallback(
  270. async (query: Record<SearchParamNames, string>) => {
  271. setLotNoFilter('');
  272. setScannedItemId(null);
  273. setScanUiMode('idle');
  274. setScanHoverCancel(false);
  275. qrScanner.stopScan();
  276. qrScanner.resetScan();
  277. const invRes = await refetchInventoryData(query, 'search', defaultPagingController, '');
  278. await refetchInventoryLotLineData(null, 'search', defaultPagingController);
  279. setInputs(() => query);
  280. setInventoriesPagingController(() => defaultPagingController);
  281. setInventoryLotLinesPagingController(() => defaultPagingController);
  282. // If there are no inventory rows, render a synthetic inventory so the "Stock Adjustment" chip can be used.
  283. if (invRes?.records?.length === 0) {
  284. try {
  285. const code = query.itemCode?.trim?.();
  286. const name = query.itemName?.trim?.();
  287. const lookupParams = code ? { code } : name ? { name } : null;
  288. if (lookupParams) {
  289. const itemRes = await fetchItemsWithDetails(lookupParams);
  290. const firstItem = getFirstItemRecord(itemRes);
  291. if (firstItem) {
  292. setSelectedInventory(buildSyntheticInventory(firstItem));
  293. setFilteredInventoryLotLines([]);
  294. setInventoryLotLinesPagingController(() => defaultPagingController);
  295. }
  296. }
  297. } catch (e) {
  298. console.error('Failed to build synthetic inventory:', e);
  299. }
  300. }
  301. },
  302. [
  303. qrScanner,
  304. refetchInventoryData,
  305. refetchInventoryLotLineData,
  306. buildSyntheticInventory,
  307. getFirstItemRecord,
  308. ],
  309. );
  310. const startLotScan = useCallback(() => {
  311. setScanHoverCancel(false);
  312. setScanUiMode('scanning');
  313. qrScanner.resetScan();
  314. qrScanner.startScan();
  315. }, [qrScanner]);
  316. const cancelLotScan = useCallback(() => {
  317. qrScanner.stopScan();
  318. qrScanner.resetScan();
  319. setScanUiMode('idle');
  320. setScanHoverCancel(false);
  321. }, [qrScanner]);
  322. useEffect(() => {
  323. if (scanUiMode !== 'scanning') return;
  324. const itemId = qrScanner.result?.itemId;
  325. const stockInLineId = qrScanner.result?.stockInLineId;
  326. if (!itemId || !stockInLineId) return;
  327. (async () => {
  328. try {
  329. const res = await analyzeQrCode({
  330. itemId: Number(itemId),
  331. stockInLineId: Number(stockInLineId),
  332. });
  333. const resolvedLotNo = res?.scanned?.lotNo?.trim?.() ? res.scanned.lotNo.trim() : '';
  334. if (!resolvedLotNo) return;
  335. setLotNoFilter(resolvedLotNo);
  336. setScannedItemId(res?.itemId ?? Number(itemId));
  337. const invRes = await refetchInventoryData(inputs, 'search', defaultPagingController, resolvedLotNo);
  338. const records = invRes?.records ?? [];
  339. const target = records.find((r) => r.itemId === (res?.itemId ?? Number(itemId))) ?? null;
  340. if (target) {
  341. onInventoryRowClick(target);
  342. } else {
  343. refetchInventoryLotLineData(null, 'search', defaultPagingController);
  344. // No inventory rows for this scanned item => show synthetic inventory with the existing chip workflow.
  345. const itemRes = await fetchItemsWithDetails({ code: res?.itemCode });
  346. const firstItem = getFirstItemRecord(itemRes);
  347. if (firstItem) {
  348. setSelectedInventory(buildSyntheticInventory(firstItem));
  349. setFilteredInventoryLotLines([]);
  350. setInventoryLotLinesPagingController(() => defaultPagingController);
  351. } else {
  352. setSelectedInventory(null);
  353. }
  354. }
  355. setInventoriesPagingController(() => defaultPagingController);
  356. setInventoryLotLinesPagingController(() => defaultPagingController);
  357. } catch (e) {
  358. console.error('Failed to analyze QR code:', e);
  359. } finally {
  360. // Always go back to initial state after a scan attempt
  361. cancelLotScan();
  362. }
  363. })();
  364. }, [
  365. cancelLotScan,
  366. inputs,
  367. onInventoryRowClick,
  368. qrScanner.result,
  369. refetchInventoryData,
  370. refetchInventoryLotLineData,
  371. buildSyntheticInventory,
  372. getFirstItemRecord,
  373. scanUiMode,
  374. ]);
  375. console.log('', 'color: #666', inventoriesPagingController);
  376. const handleOpenOpeningInventoryModal = useCallback(() => {
  377. setOpeningSelectedItem(null);
  378. setOpeningItems([]);
  379. setOpeningSearchText('');
  380. setOpeningModalOpen(true);
  381. }, []);
  382. const handleOpeningSearch = useCallback(async () => {
  383. const trimmed = openingSearchText.trim();
  384. if (!trimmed) {
  385. setOpeningItems([]);
  386. return;
  387. }
  388. setOpeningLoading(true);
  389. try {
  390. const searchParams: Record<string, any> = {
  391. pageSize: 50,
  392. pageNum: 1,
  393. };
  394. // Heuristic: if input contains space, treat as name; otherwise treat as code.
  395. if (trimmed.includes(' ')) {
  396. searchParams.name = trimmed;
  397. } else {
  398. searchParams.code = trimmed;
  399. }
  400. const response = await fetchItemsWithDetails(searchParams);
  401. let records: any[] = [];
  402. if (response && typeof response === 'object') {
  403. const anyRes = response as any;
  404. if (Array.isArray(anyRes.records)) {
  405. records = anyRes.records;
  406. } else if (Array.isArray(anyRes)) {
  407. records = anyRes;
  408. }
  409. }
  410. const combos: ItemCombo[] = records.map((item: any) => ({
  411. id: item.id,
  412. label: `${item.code} - ${item.name}`,
  413. uomId: item.uomId,
  414. uom: item.uom,
  415. uomDesc: item.uomDesc,
  416. group: item.group,
  417. currentStockBalance: item.currentStockBalance,
  418. }));
  419. setOpeningItems(combos);
  420. } catch (e) {
  421. console.error('Failed to search items for opening inventory:', e);
  422. setOpeningItems([]);
  423. } finally {
  424. setOpeningLoading(false);
  425. }
  426. }, [openingSearchText]);
  427. const handleConfirmOpeningInventory = useCallback(() => {
  428. if (!openingSelectedItem) {
  429. setOpeningModalOpen(false);
  430. return;
  431. }
  432. const rawLabel = openingSelectedItem.label ?? '';
  433. const [codePart, ...nameParts] = rawLabel.split(' - ');
  434. const itemCode = codePart?.trim() || rawLabel;
  435. const itemName = nameParts.join(' - ').trim() || itemCode;
  436. const syntheticInventory: InventoryResult = {
  437. id: 0,
  438. itemId: Number(openingSelectedItem.id),
  439. itemCode,
  440. itemName,
  441. itemType: 'Material',
  442. onHandQty: 0,
  443. onHoldQty: 0,
  444. unavailableQty: 0,
  445. availableQty: 0,
  446. uomCode: openingSelectedItem.uom,
  447. uomUdfudesc: openingSelectedItem.uomDesc,
  448. uomShortDesc: openingSelectedItem.uom,
  449. qtyPerSmallestUnit: 1,
  450. baseUom: openingSelectedItem.uom,
  451. price: 0,
  452. currencyName: '',
  453. status: 'active',
  454. latestMarketUnitPrice: undefined,
  455. latestMupUpdatedDate: undefined,
  456. };
  457. // Use this synthetic inventory to drive the stock adjustment UI
  458. setSelectedInventory(syntheticInventory);
  459. setFilteredInventoryLotLines([]);
  460. setInventoryLotLinesPagingController(() => defaultPagingController);
  461. setOpeningModalOpen(false);
  462. }, [openingSelectedItem]);
  463. return (
  464. <>
  465. <SearchBox
  466. criteria={searchCriteria}
  467. onSearch={(query) => {
  468. onSearch(query);
  469. }}
  470. onReset={onReset}
  471. extraActions={
  472. <Box sx={{ display: 'flex', gap: 1, alignItems: 'center', flexWrap: 'wrap' }}>
  473. {scanUiMode === 'idle' ? (
  474. <Button variant="contained" onClick={startLotScan}>
  475. {t('Search lot by QR code')}
  476. </Button>
  477. ) : (
  478. <>
  479. <Button variant="contained" disabled sx={{ bgcolor: 'grey.400', color: 'grey.800' }}>
  480. {t('Please scan...')}
  481. </Button>
  482. <Button variant="contained" color="error" onClick={cancelLotScan}>
  483. {t('Stop QR Scan')}
  484. </Button>
  485. </>
  486. )}
  487. <Button
  488. variant="outlined"
  489. color="secondary"
  490. onClick={handleOpenOpeningInventoryModal}
  491. sx={{ display: 'none' }}
  492. >
  493. {t('Add entry for items without inventory')}
  494. </Button>
  495. </Box>
  496. }
  497. />
  498. <InventoryTable
  499. inventories={filteredInventories}
  500. pagingController={inventoriesPagingController}
  501. setPagingController={setInventoriesPagingController}
  502. totalCount={inventoriesTotalCount}
  503. onRowClick={onInventoryRowClick}
  504. />
  505. <InventoryLotLineTable
  506. inventoryLotLines={filteredInventoryLotLines}
  507. pagingController={inventoryLotLinesPagingController}
  508. setPagingController={setInventoryLotLinesPagingController}
  509. totalCount={inventoryLotLinesTotalCount}
  510. inventory={selectedInventory}
  511. filterLotNo={lotNoFilter}
  512. printerCombo={printerCombo ?? []}
  513. onStockTransferSuccess={() =>
  514. refetchInventoryLotLineData(
  515. selectedInventory?.itemId ?? null,
  516. 'search',
  517. inventoryLotLinesPagingController,
  518. )
  519. }
  520. onStockAdjustmentSuccess={async () => {
  521. const itemId = selectedInventory?.itemId ?? null;
  522. // Refresh both blocks:
  523. // - middle: InventoryTable (inventories list)
  524. // - bottom: InventoryLotLineTable (lot lines for selected item)
  525. const invRes = await refetchInventoryData(
  526. inputs,
  527. 'search',
  528. inventoriesPagingController,
  529. lotNoFilter,
  530. );
  531. await refetchInventoryLotLineData(
  532. itemId,
  533. 'search',
  534. inventoryLotLinesPagingController,
  535. );
  536. // If inventory becomes available again after OPEN/ADJ, sync selected row.
  537. if (itemId != null && invRes?.records?.length) {
  538. const target = invRes.records.find((r) => r.itemId === itemId);
  539. if (target) setSelectedInventory(target);
  540. }
  541. }}
  542. />
  543. <Dialog
  544. open={openingModalOpen}
  545. onClose={() => setOpeningModalOpen(false)}
  546. fullWidth
  547. maxWidth="md"
  548. >
  549. <DialogTitle>{t('Add entry for items without inventory')}</DialogTitle>
  550. <DialogContent sx={{ pt: 2 }}>
  551. <Box sx={{ display: 'flex', gap: 1, mb: 2 }}>
  552. <TextField
  553. label={t('Item')}
  554. fullWidth
  555. value={openingSearchText}
  556. onChange={(e) => setOpeningSearchText(e.target.value)}
  557. onKeyDown={(e) => {
  558. if (e.key === 'Enter') {
  559. e.preventDefault();
  560. handleOpeningSearch();
  561. }
  562. }}
  563. sx={{ flex: 2 }}
  564. />
  565. <Button
  566. variant="contained"
  567. onClick={handleOpeningSearch}
  568. disabled={openingLoading}
  569. sx={{ flex: 1 }}
  570. >
  571. {openingLoading ? <CircularProgress size={20} /> : t('common:Search')}
  572. </Button>
  573. </Box>
  574. {openingItems.length === 0 && !openingLoading ? (
  575. <Box sx={{ py: 1, color: 'text.secondary', fontSize: 14 }}>
  576. {openingSearchText
  577. ? t('No data')
  578. : t('Enter item code or name to search')}
  579. </Box>
  580. ) : (
  581. <Table size="small">
  582. <TableHead>
  583. <TableRow>
  584. <TableCell />
  585. <TableCell>{t('Code')}</TableCell>
  586. <TableCell>{t('Name')}</TableCell>
  587. <TableCell>{t('UoM')}</TableCell>
  588. <TableCell align="right">{t('Current Stock')}</TableCell>
  589. </TableRow>
  590. </TableHead>
  591. <TableBody>
  592. {openingItems.map((it) => {
  593. const [code, ...nameParts] = (it.label ?? '').split(' - ');
  594. const name = nameParts.join(' - ');
  595. const selected = openingSelectedItem?.id === it.id;
  596. return (
  597. <TableRow
  598. key={it.id}
  599. hover
  600. selected={selected}
  601. onClick={() => setOpeningSelectedItem(it)}
  602. sx={{ cursor: 'pointer' }}
  603. >
  604. <TableCell padding="checkbox">
  605. <Radio checked={selected} />
  606. </TableCell>
  607. <TableCell>{code}</TableCell>
  608. <TableCell>{name}</TableCell>
  609. <TableCell>{it.uomDesc || it.uom}</TableCell>
  610. <TableCell align="right">
  611. {it.currentStockBalance != null ? it.currentStockBalance : '-'}
  612. </TableCell>
  613. </TableRow>
  614. );
  615. })}
  616. </TableBody>
  617. </Table>
  618. )}
  619. </DialogContent>
  620. <DialogActions>
  621. <Button onClick={() => setOpeningModalOpen(false)}>
  622. {t('common:Cancel')}
  623. </Button>
  624. <Button
  625. variant="contained"
  626. onClick={handleConfirmOpeningInventory}
  627. disabled={!openingSelectedItem}
  628. >
  629. {t('common:Confirm')}
  630. </Button>
  631. </DialogActions>
  632. </Dialog>
  633. </>
  634. );
  635. };
  636. export default InventorySearch;