diff --git a/Binner/Binner.Web/ClientApp/public/locales/en/translation.json b/Binner/Binner.Web/ClientApp/public/locales/en/translation.json index adcfce10..7b195dcf 100644 --- a/Binner/Binner.Web/ClientApp/public/locales/en/translation.json +++ b/Binner/Binner.Web/ClientApp/public/locales/en/translation.json @@ -506,6 +506,7 @@ "footprintName": "Footprint Name", "extensionValue1": "Extension Value 1", "extensionValue2": "Extension Value 2", + "partId": "Part Id", "ok": "Ok", "popup": { "lowStock": "Quantities below this value will indicate the part is low on stock.", @@ -803,7 +804,8 @@ "symbolName": "KiCad Symbol Name", "footprintName": "KiCad Footprint Name", "extensionValue1": "Extension Value 1", - "extensionValue2": "Extension Value 2" + "extensionValue2": "Extension Value 2", + "partId": "Part Id" }, "message": { "noPartsAdded": "No parts added.", diff --git a/Binner/Binner.Web/ClientApp/src/components/PartsGrid2.css b/Binner/Binner.Web/ClientApp/src/components/PartsGrid2.css index 242a8609..693ff7f1 100644 --- a/Binner/Binner.Web/ClientApp/src/components/PartsGrid2.css +++ b/Binner/Binner.Web/ClientApp/src/components/PartsGrid2.css @@ -100,4 +100,8 @@ .MuiSwitch-root:hover { scale: 0.9 !important; +} + +.Mui-TableHeadCell-Content-Labels .Mui-TableHeadCell-Content-Wrapper { + padding-right: 2px; } \ No newline at end of file diff --git a/Binner/Binner.Web/ClientApp/src/components/PartsGrid2Memoized.js b/Binner/Binner.Web/ClientApp/src/components/PartsGrid2Memoized.js index 05ef3ca7..c51ad6dd 100644 --- a/Binner/Binner.Web/ClientApp/src/components/PartsGrid2Memoized.js +++ b/Binner/Binner.Web/ClientApp/src/components/PartsGrid2Memoized.js @@ -182,6 +182,8 @@ export default function PartsGrid2Memoized(props) { switch (columnName) { case "partNumber": return 180; + case "partId": + return 180; case "quantity": return 140; case "lowStockThreshold": @@ -268,6 +270,8 @@ export default function PartsGrid2Memoized(props) { switch(columnName){ case 'partNumber': return {...def, Cell: ({row}) => ( {row.original.partNumber})}; + case 'partId': + return { ...def, Cell: ({ row }) => ( {row.original.partId}) }; case 'manufacturerPartNumber': return {...def, Cell: ({row}) => ( {row.original[columnName]})}; case 'description': @@ -489,7 +493,7 @@ PartsGrid2Memoized.propTypes = { PartsGrid2Memoized.defaultProps = { loading: true, - columns: "partNumber,quantity,lowStockThreshold,manufacturerPartNumber,description,partType,packageType,mountingType,location,binNumber,binNumber2,cost,digikeyPartNumber,mouserPartNumber,arrowPartNumber,datasheetUrl,print,delete,symbolName,footprintName,extensionValue1,extensionValue2", + columns: "partNumber,partId,quantity,lowStockThreshold,manufacturerPartNumber,description,partType,packageType,mountingType,location,binNumber,binNumber2,cost,digikeyPartNumber,mouserPartNumber,arrowPartNumber,datasheetUrl,print,delete,symbolName,footprintName,extensionValue1,extensionValue2", defaultVisibleColumns: "partNumber,quantity,manufacturerPartNumber,description,partType,location,binNumber,binNumber2,cost,datasheetUrl,print,delete", page: 1, totalPages: 1, diff --git a/Binner/Binner.Web/ClientApp/src/pages/Inventory.js b/Binner/Binner.Web/ClientApp/src/pages/Inventory.js index a555348a..584b1807 100644 --- a/Binner/Binner.Web/ClientApp/src/pages/Inventory.js +++ b/Binner/Binner.Web/ClientApp/src/pages/Inventory.js @@ -134,7 +134,16 @@ export function Inventory(props) { partTypesRef.current = partTypes; useEffect(() => { - const partNumberStr = props.params.partNumber; + const partNumberRaw = props.params.partNumber; + let partNumberStr = partNumberRaw?.trim(); + let partId = 0; + if (partNumberRaw?.includes(':')) { + const parts = partNumberRaw.split(':'); + if (parts.length >= 1) + partNumberStr = parts[0].trim(); + if (parts.length >= 2) + partId = parseInt(parts[1].trim()); + } const newIsEditing = partNumberStr?.length > 0; setIsEditing(newIsEditing); const fetchData = async () => { @@ -143,7 +152,7 @@ export function Inventory(props) { await fetchPartTypes(); await fetchRecentRows(); if (partNumberStr) { - var loadedPart = await fetchPart(partNumberStr); + var loadedPart = await fetchPart(partNumberStr, partId); if (newIsEditing) setInputPartNumber(partNumberStr); setInputPartNumber(partNumberStr); await fetchPartMetadataAndInventory(partNumberStr, loadedPart || part); @@ -528,12 +537,16 @@ export function Inventory(props) { setPart(part); }; - const fetchPart = async (partNumber) => { + const fetchPart = async (partNumber, partId) => { Inventory.partAbortController.abort(); Inventory.partAbortController = new AbortController(); setLoadingPart(true); try { - const response = await fetchApi(`api/part?partNumber=${encodeURIComponent(partNumber.trim())}`, { + let query = `partNumber=${encodeURIComponent(partNumber.trim())}`; + const validPartId = typeof partId === "number" ? partId : partId && parseInt(partId.trim()); + if (validPartId > 0) + query += `&partId=${partId}`; + const response = await fetchApi(`api/part?${query}`, { signal: Inventory.partAbortController.signal }); const { data } = response; @@ -851,7 +864,7 @@ export function Inventory(props) { e.stopPropagation(); if (systemSettings.printer.printMode === 0) { // direct print - await fetchApi(`api/part/print?partNumber=${encodeURIComponent(part.partNumber.trim())}&generateImageOnly=false`, { method: "POST" }); + await fetchApi(`api/part/print?partNumber=${encodeURIComponent(part.partNumber.trim())}&partId=${part.partId}&generateImageOnly=false`, { method: "POST" }); } else { window.print(); } @@ -881,8 +894,11 @@ export function Inventory(props) { const handleRecentPartClick = async (e, part) => { setPart(part); - props.history(`/inventory/${encodeURIComponent(part.partNumber)}`); - await fetchPart(part.partNumber); + if (part.partId) + props.history(`/inventory/${encodeURIComponent(part.partNumber)}:${part.partId}`); + else + props.history(`/inventory/${encodeURIComponent(part.partNumber)}`); + await fetchPart(part.partNumber, part.partId); }; const handleSaveScannedParts = async (e, scannedParts) => { @@ -1477,7 +1493,7 @@ export function Inventory(props) { {isEditing ? part.partNumber : t('page.inventory.addtitle', "Add Inventory")} - {part.partNumber && } + {part.partNumber && }
{!isEditing && diff --git a/Binner/Binner.Web/ClientApp/src/pages/LowInventory.js b/Binner/Binner.Web/ClientApp/src/pages/LowInventory.js index 082743ea..17185c55 100644 --- a/Binner/Binner.Web/ClientApp/src/pages/LowInventory.js +++ b/Binner/Binner.Web/ClientApp/src/pages/LowInventory.js @@ -86,7 +86,10 @@ export function LowInventory (props) { }; const handlePartClick = (e, part) => { - props.history(`/inventory/${encodeURIComponent(part.partNumber)}`); + if (part.partId) + props.history(`/inventory/${encodeURIComponent(part.partNumber)}:${part.partId}`); + else + props.history(`/inventory/${encodeURIComponent(part.partNumber)}`); }; const removeFilter = (e, filterName, filterValue) => { diff --git a/Binner/Binner.Web/ClientApp/src/pages/Search.js b/Binner/Binner.Web/ClientApp/src/pages/Search.js index 3a6ac8bc..e5815bb3 100644 --- a/Binner/Binner.Web/ClientApp/src/pages/Search.js +++ b/Binner/Binner.Web/ClientApp/src/pages/Search.js @@ -178,7 +178,10 @@ export function Search(props) { }, [filterBy, filterByValue, keyword]); const handlePartClick = (e, part) => { - props.history(`/inventory/${encodeURIComponent(part.partNumber)}`); + if (part.partId) + props.history(`/inventory/${encodeURIComponent(part.partNumber)}:${part.partId}`); + else + props.history(`/inventory/${encodeURIComponent(part.partNumber)}`); }; const handleNextPage = async (e, page) => { diff --git a/Binner/Binner.Web/Controllers/PartController.cs b/Binner/Binner.Web/Controllers/PartController.cs index 3b5be88b..c790d2b5 100644 --- a/Binner/Binner.Web/Controllers/PartController.cs +++ b/Binner/Binner.Web/Controllers/PartController.cs @@ -67,7 +67,7 @@ public async Task GetAsync([FromQuery] GetPartRequest request) { if (string.IsNullOrEmpty(request.PartNumber)) return BadRequest($"No part number specified!"); - var response = await _partService.GetPartWithStoredFilesAsync(request.PartNumber); + var response = await _partService.GetPartWithStoredFilesAsync(request); if (response.Part == null) return NotFound(); var partResponse = Mapper.Map(response.Part); partResponse.StoredFiles = response.StoredFiles; @@ -335,7 +335,7 @@ public async Task SearchAsync([FromQuery] string keywords, [FromQ if (exactMatch) { // search by exact part name match - var part = await _partService.GetPartAsync(keywords); + var part = await _partService.GetPartAsync(new GetPartRequest { PartNumber = keywords }); if (part == null) return NotFound(); var partTypes = await _partService.GetPartTypesAsync(); @@ -599,7 +599,7 @@ public async Task PrintPartAsync([FromQuery] PrintPartRequest req try { if (string.IsNullOrEmpty(request.PartNumber)) return BadRequest("No part number specified."); - var part = await _partService.GetPartAsync(request.PartNumber); + var part = await _partService.GetPartAsync(new GetPartRequest { PartNumber = request.PartNumber, PartId = request.PartId }); if (part == null) return NotFound(); if (await _printService.HasPartLabelTemplateAsync()) @@ -651,7 +651,7 @@ public async Task PreviewPrintPartAsync([FromQuery] PrintPartRequ System.Threading.Thread.CurrentPrincipal = new TokenPrincipal(userContext, request.Token); if (string.IsNullOrEmpty(request.PartNumber)) return BadRequest("No part number specified."); - var part = await _partService.GetPartAsync(request.PartNumber); + var part = await _partService.GetPartAsync(new GetPartRequest { PartNumber = request.PartNumber, PartId = request.PartId }); var stream = new MemoryStream(); if (await _printService.HasPartLabelTemplateAsync()) diff --git a/Binner/Library/Binner.Common/Services/IPartService.cs b/Binner/Library/Binner.Common/Services/IPartService.cs index 3490c64f..e573b7b7 100644 --- a/Binner/Library/Binner.Common/Services/IPartService.cs +++ b/Binner/Library/Binner.Common/Services/IPartService.cs @@ -48,16 +48,16 @@ public interface IPartService /// /// Get a part by part number /// - /// + /// /// - Task GetPartAsync(string partNumber); + Task GetPartAsync(GetPartRequest request); /// /// Get a part with its associated stored files /// - /// + /// /// - Task<(Part? Part, ICollection StoredFiles)> GetPartWithStoredFilesAsync(string partNumber); + Task<(Part? Part, ICollection StoredFiles)> GetPartWithStoredFilesAsync(GetPartRequest request); /// /// Get all parts diff --git a/Binner/Library/Binner.Common/Services/PartService.cs b/Binner/Library/Binner.Common/Services/PartService.cs index 708ba5a7..06a9699d 100644 --- a/Binner/Library/Binner.Common/Services/PartService.cs +++ b/Binner/Library/Binner.Common/Services/PartService.cs @@ -89,15 +89,18 @@ public async Task> GetLowStockAsync(PaginatedRequest req return await _storageProvider.GetLowStockAsync(request, _requestContext.GetUserContext()); } - public async Task GetPartAsync(string partNumber) + public async Task GetPartAsync(GetPartRequest request) { - return await _storageProvider.GetPartAsync(partNumber, _requestContext.GetUserContext()); + if (request.PartId > 0) + return await _storageProvider.GetPartAsync(request.PartId, _requestContext.GetUserContext()); + else + return await _storageProvider.GetPartAsync(request.PartNumber ?? string.Empty, _requestContext.GetUserContext()); } - public async Task<(Part? Part, ICollection StoredFiles)> GetPartWithStoredFilesAsync(string partNumber) + public async Task<(Part? Part, ICollection StoredFiles)> GetPartWithStoredFilesAsync(GetPartRequest request) { var userContext = _requestContext.GetUserContext(); - var partEntity = await _storageProvider.GetPartAsync(partNumber, userContext); + var partEntity = await GetPartAsync(request); var storedFiles = new List(); if (partEntity != null) { @@ -1082,7 +1085,7 @@ private CommonPart MouserOrderLineToCommonPart(OrderHistoryLine? orderLine, stri var datasheetUrls = new List>(); // fetch part if it's in inventory - var inventoryPart = await GetPartAsync(partNumber); + var inventoryPart = await GetPartAsync(new GetPartRequest { PartNumber = partNumber }); if (inventoryPart != null && !string.IsNullOrEmpty(inventoryPart.DatasheetUrl)) datasheetUrls.Add(new NameValuePair(inventoryPart.ManufacturerPartNumber ?? string.Empty, new DatasheetSource($"https://{_configuration.ResourceSource}/{MissingDatasheetCoverName}", inventoryPart.DatasheetUrl, inventoryPart.ManufacturerPartNumber ?? string.Empty, inventoryPart.Description ?? string.Empty, inventoryPart.Manufacturer ?? string.Empty))); if (inventoryPart != null) diff --git a/Binner/Library/Binner.Model/Requests/GetPartRequest.cs b/Binner/Library/Binner.Model/Requests/GetPartRequest.cs index 8081abe0..75bc1fd8 100644 --- a/Binner/Library/Binner.Model/Requests/GetPartRequest.cs +++ b/Binner/Library/Binner.Model/Requests/GetPartRequest.cs @@ -6,5 +6,10 @@ public class GetPartRequest /// The main part number /// public string? PartNumber { get; set; } + + /// + /// Optional part id + /// + public long PartId { get; set; } } } diff --git a/Binner/Library/Binner.Model/Requests/PrintPartRequest.cs b/Binner/Library/Binner.Model/Requests/PrintPartRequest.cs index 9ba7e97c..629dda6e 100644 --- a/Binner/Library/Binner.Model/Requests/PrintPartRequest.cs +++ b/Binner/Library/Binner.Model/Requests/PrintPartRequest.cs @@ -6,7 +6,12 @@ public class PrintPartRequest : IImagesToken /// The main part number /// public string? PartNumber { get; set; } - + + /// + /// Optional part id + /// + public long PartId { get; set; } + /// /// True to generate image only ///