diff --git a/src/frontend/src/components/items/ScanButton.tsx b/src/frontend/src/components/items/ScanButton.tsx index 65c30caef6..c3fe6d14b5 100644 --- a/src/frontend/src/components/items/ScanButton.tsx +++ b/src/frontend/src/components/items/ScanButton.tsx @@ -3,6 +3,9 @@ import { ActionIcon } from '@mantine/core'; import { openContextModal } from '@mantine/modals'; import { IconQrcode } from '@tabler/icons-react'; +/** + * A button which opens the QR code scanner modal + */ export function ScanButton() { return ( - + + - + + + + diff --git a/src/frontend/src/components/nav/SearchDrawer.tsx b/src/frontend/src/components/nav/SearchDrawer.tsx new file mode 100644 index 0000000000..1fee52d466 --- /dev/null +++ b/src/frontend/src/components/nav/SearchDrawer.tsx @@ -0,0 +1,438 @@ +import { Trans, t } from '@lingui/macro'; +import { + ActionIcon, + Alert, + Center, + Checkbox, + Divider, + Drawer, + Group, + Menu, + Paper, + Space, + Stack, + Text, + TextInput +} from '@mantine/core'; +import { Loader } from '@mantine/core'; +import { useDebouncedValue } from '@mantine/hooks'; +import { + IconAlertCircle, + IconBackspace, + IconRefresh, + IconSearch, + IconSettings, + IconX +} from '@tabler/icons-react'; +import { useQuery } from '@tanstack/react-query'; +import { useEffect, useState } from 'react'; + +import { api } from '../../App'; + +// Define type for handling individual search queries +type SearchQuery = { + name: string; + title: string; + enabled: boolean; + parameters: any; + results?: any; + render: (result: any) => JSX.Element; +}; + +// Placeholder function for permissions checks (will be replaced with a proper implementation) +function permissionCheck(permission: string) { + return true; +} + +// Placeholder function for settings checks (will be replaced with a proper implementation) +function settingsCheck(setting: string) { + return true; +} + +// Placeholder function for rendering an individual search result +// In the future, this will be defined individually for each result type +function renderResult(result: any) { + return Result here - ID = {`${result.pk}`}; +} + +/* + * Build a list of search queries based on user permissions + */ +function buildSearchQueries(): SearchQuery[] { + return [ + { + name: 'part', + title: t`Parts`, + parameters: {}, + render: renderResult, + enabled: + permissionCheck('part.view') && + settingsCheck('SEARCH_PREVIEW_SHOW_PARTS') + }, + { + name: 'supplierpart', + title: t`Supplier Parts`, + parameters: { + part_detail: true, + supplier_detail: true, + manufacturer_detail: true + }, + render: renderResult, + enabled: + permissionCheck('part.view') && + permissionCheck('purchase_order.view') && + settingsCheck('SEARCH_PREVIEW_SHOW_SUPPLIER_PARTS') + }, + { + name: 'manufacturerpart', + title: t`Manufacturer Parts`, + parameters: { + part_detail: true, + supplier_detail: true, + manufacturer_detail: true + }, + render: renderResult, + enabled: + permissionCheck('part.view') && + permissionCheck('purchase_order.view') && + settingsCheck('SEARCH_PREVIEW_SHOW_MANUFACTURER_PARTS') + }, + { + name: 'partcategory', + title: t`Part Categories`, + parameters: {}, + render: renderResult, + enabled: + permissionCheck('part_category.view') && + settingsCheck('SEARCH_PREVIEW_SHOW_CATEGORIES') + }, + { + name: 'stockitem', + title: t`Stock Items`, + parameters: { + part_detail: true, + location_detail: true + }, + render: renderResult, + enabled: + permissionCheck('stock.view') && + settingsCheck('SEARCH_PREVIEW_SHOW_STOCK') + }, + { + name: 'stocklocation', + title: t`Stock Locations`, + parameters: {}, + render: renderResult, + enabled: + permissionCheck('stock_location.view') && + settingsCheck('SEARCH_PREVIEW_SHOW_LOCATIONS') + }, + { + name: 'build', + title: t`Build Orders`, + parameters: { + part_detail: true + }, + render: renderResult, + enabled: + permissionCheck('build.view') && + settingsCheck('SEARCH_PREVIEW_SHOW_BUILD_ORDERS') + }, + { + name: 'company', + title: t`Companies`, + parameters: {}, + render: renderResult, + enabled: + (permissionCheck('sales_order.view') || + permissionCheck('purchase_order.view')) && + settingsCheck('SEARCH_PREVIEW_SHOW_COMPANIES') + }, + { + name: 'purchaseorder', + title: t`Purchase Orders`, + parameters: { + supplier_detail: true + }, + render: renderResult, + enabled: + permissionCheck('purchase_order.view') && + settingsCheck(`SEARCH_PREVIEW_SHOW_PURCHASE_ORDERS`) + }, + { + name: 'salesorder', + title: t`Sales Orders`, + parameters: { + customer_detail: true + }, + render: renderResult, + enabled: + permissionCheck('sales_order.view') && + settingsCheck(`SEARCH_PREVIEW_SHOW_SALES_ORDERS`) + }, + { + name: 'returnorder', + title: t`Return Orders`, + parameters: { + customer_detail: true + }, + render: renderResult, + enabled: + permissionCheck('return_order.view') && + settingsCheck(`SEARCH_PREVIEW_SHOW_RETURN_ORDERS`) + } + ]; +} + +/* + * Render the results for a single search query + */ +function QueryResultGroup({ + query, + onRemove +}: { + query: SearchQuery; + onRemove: (query: string) => void; +}) { + if (query.results.count == 0) { + return null; + } + + return ( + + + + + {query.title} + + {' '} + - {query.results.count} results + + + + onRemove(query.name)} + > + + + + + + {query.results.results.map((result: any) => query.render(result))} + + + + + ); +} + +/** + * Construct a drawer which provides quick-search functionality + * @param + */ +export function SearchDrawer({ + opened, + onClose +}: { + opened: boolean; + onClose: () => void; +}) { + const [value, setValue] = useState(''); + const [searchText] = useDebouncedValue(value, 500); + + const [searchRegex, setSearchRegex] = useState(false); + const [searchWhole, setSearchWhole] = useState(false); + + // Construct a list of search queries based on user permissions + const searchQueries: SearchQuery[] = buildSearchQueries().filter( + (q) => q.enabled + ); + + // Re-fetch data whenever the search term is updated + useEffect(() => { + // TODO: Implement search functionality + refetch(); + }, [searchText]); + + // Function for performing the actual search query + const performSearch = async () => { + // Return empty result set if no search text + if (!searchText) { + return []; + } + + let params: any = { + offset: 0, + limit: 10, // TODO: Make this configurable (based on settings) + search: searchText, + search_regex: searchRegex, + search_whole: searchWhole + }; + + // Add in custom query parameters + searchQueries.forEach((query) => { + params[query.name] = query.parameters; + }); + + return api + .post(`/search/`, params) + .then(function (response) { + return response.data; + }) + .catch(function (error) { + console.error(error); + return []; + }); + }; + + // Search query manager + const { data, isError, isFetching, isLoading, refetch } = useQuery( + ['search', searchText, searchRegex, searchWhole], + performSearch, + { + refetchOnWindowFocus: false + } + ); + + // A list of queries which return valid results + const [queryResults, setQueryResults] = useState([]); + + // Update query results whenever the search results change + useEffect(() => { + if (data) { + let queries = searchQueries.filter((query) => query.name in data); + + for (let key in data) { + let query = queries.find((q) => q.name == key); + if (query) { + query.results = data[key]; + } + } + + // Filter for results with non-zero count + queries = queries.filter((query) => query.results.count > 0); + + setQueryResults(queries); + } else { + setQueryResults([]); + } + }, [data]); + + // Callback to remove a set of results from the list + function removeResults(query: string) { + setQueryResults(queryResults.filter((q) => q.name != query)); + } + + function closeDrawer() { + setValue(''); + onClose(); + } + + return ( + + setValue(event.currentTarget.value)} + icon={} + rightSection={ + value && ( + setValue('')} /> + ) + } + styles={{ root: { width: '100%' } }} + /> + refetch()} + > + + + + + + + + + + {t`Search Options`} + + + setSearchRegex(event.currentTarget.checked) + } + radius="sm" + /> + + + + setSearchWhole(event.currentTarget.checked) + } + radius="sm" + /> + + + + + } + > + {isFetching && ( +
+ +
+ )} + {!isFetching && !isError && ( + + {queryResults.map((query) => ( + removeResults(query)} + /> + ))} + + )} + {isError && ( + } + > + An error occurred during search query + + )} + {searchText && !isFetching && !isError && queryResults.length == 0 && ( + } + > + No results available for search query + + )} +
+ ); +}