From 8bd862cd1961104cc1db114586566a817e29fd74 Mon Sep 17 00:00:00 2001 From: shaun-leach Date: Sat, 4 May 2024 23:43:00 -0700 Subject: [PATCH 01/10] 1st pass KiCAD BOM imporoter --- .../Binner.Common/IO/ExcelDataImporter.cs | 88 ++++++++++++++++++- 1 file changed, 87 insertions(+), 1 deletion(-) diff --git a/Binner/Library/Binner.Common/IO/ExcelDataImporter.cs b/Binner/Library/Binner.Common/IO/ExcelDataImporter.cs index 51173114..239612aa 100644 --- a/Binner/Library/Binner.Common/IO/ExcelDataImporter.cs +++ b/Binner/Library/Binner.Common/IO/ExcelDataImporter.cs @@ -1,5 +1,6 @@ using Binner.Global.Common; using Binner.Model; +using Microsoft.EntityFrameworkCore.Metadata.Internal; using NPOI.SS.UserModel; using System; using System.Collections.Generic; @@ -7,6 +8,7 @@ using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; +using TypeSupport.Extensions; namespace Binner.Common.IO { @@ -16,7 +18,7 @@ namespace Binner.Common.IO public class ExcelDataImporter : IDataImporter { // SupportedTables ordering matters when it comes to relational data! - private readonly string[] SupportedTables = new string[] { "Projects", "PartTypes", "Parts" }; + private readonly string[] SupportedTables = new string[] { "Projects", "PartTypes", "Parts", "BOM" }; private readonly IStorageProvider _storageProvider; private readonly TemporaryKeyTracker _temporaryKeyTracker = new TemporaryKeyTracker(); @@ -58,6 +60,33 @@ public async Task ImportAsync(string filename, Stream stream, IUse var result = new ImportResult(); foreach (var table in SupportedTables) result.RowsImportedByTable.Add(table, 0); + + long bomProjectId = 0; + if (filename.Contains("_bom")) + { + string projectName = filename.Remove(filename.LastIndexOf("_bom")); + Project project = await _storageProvider.GetProjectAsync(projectName, userContext); + if (project == null) + { + project = new Project(); + project.Name = projectName; + try + { + project = await _storageProvider.AddProjectAsync(project, userContext); + bomProjectId = project.ProjectId; + } + catch (Exception ex) + { + result.Errors.Add($"[Sheet '{projectName}'] Project with name '{projectName}' could not be added. Error: {ex.Message}"); + } + + } + else + { + bomProjectId = project.ProjectId; + } + + } // get the global part types, and the user's custom part types var partTypes = (await _storageProvider.GetPartTypesAsync(userContext)).ToList(); try @@ -74,8 +103,65 @@ public async Task ImportAsync(string filename, Stream stream, IUse for (var rowNumber = 1; rowNumber <= worksheet.LastRowNum; rowNumber++) { var rowData = worksheet.GetRow(rowNumber); + if (rowData == null) + continue; switch (table.ToLower()) { + case "bom": + { + // import BOM info + var isPartNumberValid = TryGet(rowData, header, "MPN", out var partNumber); + var isQuantityValid = TryGet(rowData, header, "Quantity per PCB", out var quantity); + var isReferenceValid = TryGet(rowData, header, "References", out var reference); + var isNoteValid = TryGet(rowData, header, "Value", out var note); + + if (!isPartNumberValid || !isQuantityValid || !isReferenceValid) + continue; + + ProjectPartAssignment assignment = new ProjectPartAssignment(); + assignment.ProjectId = bomProjectId; + assignment.Quantity = quantity; + assignment.Notes = note; + assignment.SchematicReferenceId = reference; + + var part = await _storageProvider.GetPartAsync(partNumber, userContext); + if (part != null) + { + assignment.PartName = part.PartNumber; + assignment.PartId = part.PartId; + } + else + { + part = new Part(); + part.PartNumber = partNumber; + part.Quantity = 0; + part.UserId = userContext?.UserId; + part.PartTypeId = (long)SystemDefaults.DefaultPartTypes.Other; + try + { + part = await _storageProvider.AddPartAsync(part, userContext); + } + catch (Exception ex) + { + // failed to add part + result.Errors.Add($"[Row {rowNumber}, Sheet '{table}'] Part with PartNumber '{partNumber}' could not be added. Error: {ex.Message}"); + } + + assignment.PartName = partNumber; + assignment.PartId = part.PartId; + } + + try + { + await _storageProvider.AddProjectPartAssignmentAsync(assignment, userContext); + } + catch (Exception ex) + { + result.Errors.Add($"[Row {rowNumber}, Sheet '{table}'] BOM entry '{partNumber}' could not be added. Error: {ex.Message}"); + } + + } + break; case "projects": { // import project info From 90d1f6055a6957a22c0ab96c4421911249f8a7bd Mon Sep 17 00:00:00 2001 From: shaun-leach Date: Wed, 19 Jun 2024 20:28:24 -0700 Subject: [PATCH 02/10] 1st pass real BOM import --- .gitignore | 6 ++ Binner/Binner.Web/ClientApp/src/pages/Boms.js | 91 +++++++++++++++++++ .../Binner.Common/IO/ExcelDataImporter.cs | 4 +- 3 files changed, 99 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 7e9dab35..25d2dd9a 100644 --- a/.gitignore +++ b/.gitignore @@ -539,3 +539,9 @@ $RECYCLE.BIN/ Binner/BinnerInstaller/BinnerSetup.exe Binner/Binner.Web/ClientApp/.vscode/settings.json *.bak + +# Slickedit +*.vpj +*.vtg +*.vpw +*.vpwhist diff --git a/Binner/Binner.Web/ClientApp/src/pages/Boms.js b/Binner/Binner.Web/ClientApp/src/pages/Boms.js index 79fc5a68..ffc96495 100644 --- a/Binner/Binner.Web/ClientApp/src/pages/Boms.js +++ b/Binner/Binner.Web/ClientApp/src/pages/Boms.js @@ -7,6 +7,8 @@ import _ from 'underscore'; import { toast } from "react-toastify"; import { fetchApi } from '../common/fetchApi'; import { ProjectColors } from "../common/Types"; +import { useDropzone } from "react-dropzone"; +import { humanFileSize } from "../common/files"; export function Boms (props) { const { t } = useTranslation(); @@ -22,6 +24,7 @@ export function Boms (props) { const [pageSize, setPageSize] = useState(parseInt(localStorage.getItem("bomsRecordsPerPage")) || 5); const [loading, setLoading] = useState(true); const [addVisible, setAddVisible] = useState(false); + const [importVisible, setImportVisible] = useState(false); const [project, setProject] = useState(defaultProject); const [projects, setProjects] = useState([]); const [changeTracker, setChangeTracker] = useState([]); @@ -32,6 +35,8 @@ export function Boms (props) { const [confirmDeleteIsOpen, setConfirmDeleteIsOpen] = useState(false); const [confirmPartDeleteContent, setConfirmProjectDeleteContent] = useState(null); const [confirmDeleteSelectedProject, setConfirmDeleteSelectedProject] = useState(null); + const [acceptedFile, setAcceptedFile] = useState(null); + const [error, setError] = useState(null); const [colors] = useState(_.map(ProjectColors, function (c) { return { @@ -50,6 +55,26 @@ export function Boms (props) { { key: 5, text: '100', value: 100 }, ]; + const { acceptedFiles, getRootProps, getInputProps } = useDropzone({ + maxFiles: 3, + onDrop: (acceptedFiles, rejectedFiles, e) => { + // do accept manually + const acceptedMimeTypes = ["application/vnd.ms-excel", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "application/sql", "text/csv"]; + let errorMsg = ""; + for (let i = 0; i < acceptedFiles.length; i++) { + if (!acceptedMimeTypes.includes(acceptedFiles[i].type)) { + errorMsg += `${t('page.exportData.fileNotSupported', "File '{{name}}' with mime type '{{type}}' is not an accepted image type!", { name: acceptedFiles[i].name, type: acceptedFiles[i].type})}\r\n`; + } + } + if (errorMsg.length > 0) { + setAcceptedFile(null); + toast.error(errorMsg); + } else { + setAcceptedFile(acceptedFiles[0]); + } + } + }); + const loadProjects = async (page, pageSize, reset = false) => { setLoading(true); let endOfData = false; @@ -115,6 +140,10 @@ export function Boms (props) { setAddVisible(!addVisible); }; + const handleShowImport = () => { + setImportVisible(!importVisible); + }; + const handleLoadBom = (e, p) => { e.preventDefault(); e.stopPropagation(); @@ -139,10 +168,39 @@ export function Boms (props) { // reset form setProject(defaultProject); setAddVisible(false); + setImportVisible(false); loadProjects(page, pageSize, true); } }; + const onImportProject = async () => { + if (acceptedFiles && acceptedFiles.length > 0) { + const formData = new FormData(); + formData.append("name", project.name); + formData.append("description", project.description); + for (let i = 0; i < acceptedFiles.length; i++) { + formData.append("files", acceptedFiles[i], acceptedFiles[i].name); + } + + const response = await fetchApi('api/project/import', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(formData) + }); + if (response.responseObject.status === 200) { + // reset form + setProject(defaultProject); + setAddVisible(false); + setImportVisible(false); + loadProjects(page, pageSize, true); + } else { + toast.error(t('importProjectFailed', 'Failed to import project!')); + } + } + }; + const handleSort = (clickedColumn) => () => { if (column !== clickedColumn) { setColumn(clickedColumn); @@ -281,6 +339,7 @@ export function Boms (props) {
+
{addVisible && @@ -297,6 +356,38 @@ export function Boms (props) { }
+
+ {importVisible && + +
+ + +
+ {t('page.exportData.uploadNote', "Drag a document to upload, or click to select files")} + +
{t('page.exportData.acceptedFileTypes', "Accepted file types: \"*.sql, *.xls, *.xlsx, *.csv\"")}
+
+ {error && ( +
+ {t('label.error', "Error")}: {error} +
+ )} + + + +
+ } +
ImportAsync(string filename, Stream stream, IUse { // import BOM info var isPartNumberValid = TryGet(rowData, header, "MPN", out var partNumber); - var isQuantityValid = TryGet(rowData, header, "Quantity per PCB", out var quantity); - var isReferenceValid = TryGet(rowData, header, "References", out var reference); + var isQuantityValid = TryGet(rowData, header, "Qty", out var quantity); + var isReferenceValid = TryGet(rowData, header, "Reference", out var reference); var isNoteValid = TryGet(rowData, header, "Value", out var note); if (!isPartNumberValid || !isQuantityValid || !isReferenceValid) From 76f24ec3f63f881ac4100c5d9579b150ed4550c1 Mon Sep 17 00:00:00 2001 From: shaun-leach Date: Thu, 20 Jun 2024 20:35:04 -0700 Subject: [PATCH 03/10] Add controller and service support for importing a project --- .../Controllers/ProjectController.cs | 14 +++++++++++++ .../Binner.Common/Services/IProjectService.cs | 7 +++++++ .../Binner.Common/Services/ProjectService.cs | 6 ++++++ .../Requests/ImportProjectRequest.cs | 20 +++++++++++++++++++ 4 files changed, 47 insertions(+) create mode 100644 Binner/Library/Binner.Model/Requests/ImportProjectRequest.cs diff --git a/Binner/Binner.Web/Controllers/ProjectController.cs b/Binner/Binner.Web/Controllers/ProjectController.cs index d14fd3c4..7f527945 100644 --- a/Binner/Binner.Web/Controllers/ProjectController.cs +++ b/Binner/Binner.Web/Controllers/ProjectController.cs @@ -87,6 +87,20 @@ public async Task CreateProjectAsync(CreateProjectRequest request return Ok(project); } + /// + /// Import a new project + /// + /// + /// + [HttpPost("import")] + public async Task ImportProjectAsync(ImportProjectRequest request) + { + var mappedProject = Mapper.Map(request); + mappedProject.DateCreatedUtc = DateTime.UtcNow; + var project = await _projectService.ImportProjectAsync(mappedProject); + return Ok(project); + } + /// /// Update an existing project /// diff --git a/Binner/Library/Binner.Common/Services/IProjectService.cs b/Binner/Library/Binner.Common/Services/IProjectService.cs index 35ef4b26..ab8e0f84 100644 --- a/Binner/Library/Binner.Common/Services/IProjectService.cs +++ b/Binner/Library/Binner.Common/Services/IProjectService.cs @@ -15,6 +15,13 @@ public interface IProjectService /// Task AddProjectAsync(Project project); + /// + /// Import a project + /// + /// + /// + Task ImportProjectAsync(Project project); + /// /// Update an existing project /// diff --git a/Binner/Library/Binner.Common/Services/ProjectService.cs b/Binner/Library/Binner.Common/Services/ProjectService.cs index f1b4e8fe..d8a152fe 100644 --- a/Binner/Library/Binner.Common/Services/ProjectService.cs +++ b/Binner/Library/Binner.Common/Services/ProjectService.cs @@ -33,6 +33,12 @@ public async Task AddProjectAsync(Project project) return await _storageProvider.AddProjectAsync(project, _requestContext.GetUserContext()); } + public async Task ImportProjectAsync(Project project) + { + return null; + //return await _storageProvider.AddProjectAsync(project, _requestContext.GetUserContext()); + } + public async Task DeleteProjectAsync(Project project) { var user = _requestContext.GetUserContext(); diff --git a/Binner/Library/Binner.Model/Requests/ImportProjectRequest.cs b/Binner/Library/Binner.Model/Requests/ImportProjectRequest.cs new file mode 100644 index 00000000..cbcc3ac0 --- /dev/null +++ b/Binner/Library/Binner.Model/Requests/ImportProjectRequest.cs @@ -0,0 +1,20 @@ +namespace Binner.Model.Requests +{ + public class ImportProjectRequest + { + /// + /// Name of project + /// + public string? Name { get; set; } + + /// + /// Description of project + /// + public string? Description { get; set; } + + /// + /// File to import from + /// + public string? File { get; set; } + } +} From 93662fe41cfc361d17279ee61c3a520f1d32a1ce Mon Sep 17 00:00:00 2001 From: shaun-leach Date: Sat, 22 Jun 2024 17:09:51 -0700 Subject: [PATCH 04/10] Get file and meta info into the controller --- Binner/Binner.Web/ClientApp/src/pages/Boms.js | 53 ++++++++++--------- .../Controllers/ProjectController.cs | 4 +- .../Requests/ImportProjectRequest.cs | 8 +-- 3 files changed, 35 insertions(+), 30 deletions(-) diff --git a/Binner/Binner.Web/ClientApp/src/pages/Boms.js b/Binner/Binner.Web/ClientApp/src/pages/Boms.js index ffc96495..78565a38 100644 --- a/Binner/Binner.Web/ClientApp/src/pages/Boms.js +++ b/Binner/Binner.Web/ClientApp/src/pages/Boms.js @@ -9,6 +9,8 @@ import { fetchApi } from '../common/fetchApi'; import { ProjectColors } from "../common/Types"; import { useDropzone } from "react-dropzone"; import { humanFileSize } from "../common/files"; +import axios from "axios"; +import { getAuthToken } from "../common/authentication"; export function Boms (props) { const { t } = useTranslation(); @@ -56,10 +58,10 @@ export function Boms (props) { ]; const { acceptedFiles, getRootProps, getInputProps } = useDropzone({ - maxFiles: 3, + maxFiles: 1, onDrop: (acceptedFiles, rejectedFiles, e) => { // do accept manually - const acceptedMimeTypes = ["application/vnd.ms-excel", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "application/sql", "text/csv"]; + const acceptedMimeTypes = ["application/vnd.ms-excel", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "text/csv"]; let errorMsg = ""; for (let i = 0; i < acceptedFiles.length; i++) { if (!acceptedMimeTypes.includes(acceptedFiles[i].type)) { @@ -174,32 +176,31 @@ export function Boms (props) { }; const onImportProject = async () => { - if (acceptedFiles && acceptedFiles.length > 0) { + if (acceptedFile != null) { const formData = new FormData(); - formData.append("name", project.name); - formData.append("description", project.description); - for (let i = 0; i < acceptedFiles.length; i++) { - formData.append("files", acceptedFiles[i], acceptedFiles[i].name); - } + formData.set("name", project.name); + formData.set("description", project.description); + formData.set("file", acceptedFile, acceptedFile.name); - const response = await fetchApi('api/project/import', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(formData) - }); - if (response.responseObject.status === 200) { - // reset form - setProject(defaultProject); - setAddVisible(false); - setImportVisible(false); - loadProjects(page, pageSize, true); - } else { - toast.error(t('importProjectFailed', 'Failed to import project!')); + fetchApi("api/authentication/identity").then((_) => { + axios + .request({ + method: "post", + url: `api/project/import`, + data: formData, + headers: { Authorization: `Bearer ${getAuthToken()}` } + }) + .then((response) => { + toast.dismiss(); + if (response.status === 200) { + toast.success(t("importProjectSuccess", "BOM Imported!")); + } else { + toast.error(t("importProjectFailed", `Failed to import BOM!`), { autoClose: 10000 }); + } + }); + }); } - } - }; + }; const handleSort = (clickedColumn) => () => { if (column !== clickedColumn) { @@ -368,7 +369,7 @@ export function Boms (props) { > {t('page.exportData.uploadNote', "Drag a document to upload, or click to select files")} -
{t('page.exportData.acceptedFileTypes', "Accepted file types: \"*.sql, *.xls, *.xlsx, *.csv\"")}
+
{t('page.exportData.acceptedFileTypes', "Accepted file types: \"*.xls, *.xlsx, *.csv\"")}
{error && (
diff --git a/Binner/Binner.Web/Controllers/ProjectController.cs b/Binner/Binner.Web/Controllers/ProjectController.cs index 7f527945..b6f4905e 100644 --- a/Binner/Binner.Web/Controllers/ProjectController.cs +++ b/Binner/Binner.Web/Controllers/ProjectController.cs @@ -5,6 +5,7 @@ using Binner.Model.Requests; using Binner.Model.Responses; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; @@ -93,7 +94,8 @@ public async Task CreateProjectAsync(CreateProjectRequest request /// /// [HttpPost("import")] - public async Task ImportProjectAsync(ImportProjectRequest request) + [Consumes("multipart/form-data")] + public async Task ImportProjectAsync([FromForm] ImportProjectRequest request) { var mappedProject = Mapper.Map(request); mappedProject.DateCreatedUtc = DateTime.UtcNow; diff --git a/Binner/Library/Binner.Model/Requests/ImportProjectRequest.cs b/Binner/Library/Binner.Model/Requests/ImportProjectRequest.cs index cbcc3ac0..542a039b 100644 --- a/Binner/Library/Binner.Model/Requests/ImportProjectRequest.cs +++ b/Binner/Library/Binner.Model/Requests/ImportProjectRequest.cs @@ -1,4 +1,6 @@ -namespace Binner.Model.Requests +using Microsoft.AspNetCore.Http; + +namespace Binner.Model.Requests { public class ImportProjectRequest { @@ -13,8 +15,8 @@ public class ImportProjectRequest public string? Description { get; set; } /// - /// File to import from + /// File to import /// - public string? File { get; set; } + public IFormFile? File { get; set; } } } From 7212b45b5e4202b6e8211d472a090a6c5331df52 Mon Sep 17 00:00:00 2001 From: shaun-leach Date: Sun, 23 Jun 2024 17:08:03 -0700 Subject: [PATCH 05/10] Actual import support --- .../Controllers/ProjectController.cs | 4 +- .../Binner.Common/IO/CsvBOMImporter.cs | 412 ++++++++++++++++++ .../Binner.Common/IO/ExcelBOMImporter.cs | 212 +++++++++ .../Binner.Common/IO/ExcelDataImporter.cs | 84 +--- .../Binner.Common/Services/IProjectService.cs | 2 +- .../Binner.Common/Services/ProjectService.cs | 52 ++- 6 files changed, 676 insertions(+), 90 deletions(-) create mode 100644 Binner/Library/Binner.Common/IO/CsvBOMImporter.cs create mode 100644 Binner/Library/Binner.Common/IO/ExcelBOMImporter.cs diff --git a/Binner/Binner.Web/Controllers/ProjectController.cs b/Binner/Binner.Web/Controllers/ProjectController.cs index b6f4905e..882ab6d5 100644 --- a/Binner/Binner.Web/Controllers/ProjectController.cs +++ b/Binner/Binner.Web/Controllers/ProjectController.cs @@ -97,9 +97,7 @@ public async Task CreateProjectAsync(CreateProjectRequest request [Consumes("multipart/form-data")] public async Task ImportProjectAsync([FromForm] ImportProjectRequest request) { - var mappedProject = Mapper.Map(request); - mappedProject.DateCreatedUtc = DateTime.UtcNow; - var project = await _projectService.ImportProjectAsync(mappedProject); + var project = await _projectService.ImportProjectAsync(request); return Ok(project); } diff --git a/Binner/Library/Binner.Common/IO/CsvBOMImporter.cs b/Binner/Library/Binner.Common/IO/CsvBOMImporter.cs new file mode 100644 index 00000000..f3b06aa7 --- /dev/null +++ b/Binner/Library/Binner.Common/IO/CsvBOMImporter.cs @@ -0,0 +1,412 @@ +using Binner.Global.Common; +using Binner.Model; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace Binner.Common.IO +{ + public class CsvBOMImporter + { + // SupportedTables ordering matters when it comes to relational data! + private readonly IStorageProvider _storageProvider; + private readonly TemporaryKeyTracker _temporaryKeyTracker = new TemporaryKeyTracker(); + + public CsvBOMImporter(IStorageProvider storageProvider) + { + _storageProvider = storageProvider; + } + + private string? GetValueFromHeader(string[] rowData, Header header, string name) + { + var headerIndex = header.GetHeaderIndex(name); + if (headerIndex >= 0) + return rowData[headerIndex]; + return null; + } + + public async Task ImportAsync(Project project, Stream stream, IUserContext? userContext) + { + var result = new ImportResult(); +/* foreach (var table in SupportedTables) + result.RowsImportedByTable.Add(table, 0); + // get the global part types, and the user's custom part types + var partTypes = (await _storageProvider.GetPartTypesAsync(userContext)).ToList(); + try + { + stream.Position = 0; + var reader = new StreamReader(stream); + var data = await reader.ReadToEndAsync(); + // remove line breaks + data = data.Replace("\r", ""); + + var tableName = Path.GetFileNameWithoutExtension(filename); + if (!SupportedTables.Contains(tableName, StringComparer.InvariantCultureIgnoreCase)) + { + result.Errors.Add($"Filename '{tableName}' not a supported table name! Filename must be one of the following: {string.Join(",", SupportedTables)} and may optionally contain the schema prefix 'dbo'."); + result.Success = false; + return result; + } + + var rows = SplitBoundaries(data, new char[] { '\n' }); + if (!rows.Any()) + { + result.Success = true; + result.Warnings.Add("No rows were found!"); + return result; + } + + // read csv header + Header? header = null; + var headerRow = rows.First(); + if (headerRow.StartsWith("#")) + { + header = new Header(headerRow); + } + else + { + result.Success = false; + result.Errors.Add("No CSV row header was found! A CSV row header is required as the first row of data to indicate the column ordering. Example: '#Name,Description,Location,Color,DateCreatedUtc,DateModifiedUtc'"); + return result; + } + var rowNumber = 0; + foreach (var row in rows) + { + // skip the header row + if (rowNumber == 0) + { + rowNumber++; + continue; + } + var rowData = SplitBoundaries(row, new char[] { ',' }, true); + if (rowData.Length != header.Headers.Count) + { + result.Warnings.Add($"[Row {rowNumber}] Row does not contain the same number of columns as the header, skipping..."); + continue; + } + switch (tableName.ToLower()) + { + case "projects": + { + // import project info + var isProjectIdValid = TryGet(rowData, header, "ProjectId", out var projectId); + var isColorValid = TryGet(rowData, header, "Color", out var color); + var isDateCreatedValid = TryGet(rowData, header, "DateCreatedUtc", out var dateCreatedUtc); + var isDateModifiedValid = TryGet(rowData, header, "DateModifiedUtc", out var dateModifiedUtc); + if (!isColorValid || !isDateCreatedValid || !isDateModifiedValid) + continue; + var name = GetQuoted(GetValueFromHeader(rowData, header, "Name"))?.Trim(); + + if (!string.IsNullOrEmpty(name) && await _storageProvider.GetProjectAsync(name, userContext) == null) + { + var project = new Project + { + Name = name, + Description = GetQuoted(GetValueFromHeader(rowData, header, "Description")), + Location = GetQuoted(GetValueFromHeader(rowData, header, "Location")), + Color = color, + DateCreatedUtc = dateCreatedUtc + }; + try + { + project = await _storageProvider.AddProjectAsync(project, userContext); + _temporaryKeyTracker.AddKeyMapping("Projects", "ProjectId", projectId, project.ProjectId); + result.TotalRowsImported++; + result.RowsImportedByTable["Projects"]++; + } + catch (Exception ex) + { + result.Errors.Add($"[Row {rowNumber}, Project with name '{name}' could not be added. Error: {ex.Message}"); + } + } + else + { + result.Warnings.Add($"[Row {rowNumber}] Project with name '{name}' already exists."); + } + } + break; + case "parttypes": + { + // import partTypes info + var isPartTypeIdValid = TryGet(rowData, header, "PartTypeId", out var partTypeId); + var isParentPartTypeIdValid = TryGet(rowData, header, "ParentPartTypeId", out var parentPartTypeId); + var isDateCreatedValid = TryGet(rowData, header, "DateCreatedUtc", out var dateCreatedUtc); + if (!isParentPartTypeIdValid || !isDateCreatedValid) + continue; + + var name = GetQuoted(GetValueFromHeader(rowData, header, "Name"))?.Trim(); + // part types need to have a unique name for the user and can not be part of global part types + if (!string.IsNullOrEmpty(name) && !partTypes.Any(x => x.Name?.Equals(name, StringComparison.InvariantCultureIgnoreCase) == true)) + { + if (parentPartTypeId == 0) + parentPartTypeId = null; + var partType = new PartType + { + ParentPartTypeId = parentPartTypeId != null ? _temporaryKeyTracker.GetMappedId("PartTypes", "PartTypeId", parentPartTypeId.Value) : null, + Name = name, + DateCreatedUtc = dateCreatedUtc + }; + partType = await _storageProvider.GetOrCreatePartTypeAsync(partType, userContext); + if (partType != null) + { + _temporaryKeyTracker.AddKeyMapping("PartTypes", "PartTypeId", partTypeId, + partType.PartTypeId); + result.TotalRowsImported++; + result.RowsImportedByTable["PartTypes"]++; + } + } + else + { + result.Warnings.Add($"[Row {rowNumber}] PartType with name '{name}' already exists."); + } + } + break; + case "parts": + { + // import parts info + var isPartIdValid = TryGet(rowData, header, "PartId", out var partId); + var isPartTypeIdValid = TryGet(rowData, header, "PartTypeId", out var partTypeId); + var isBinNumberValid = TryGet(rowData, header, "BinNumber", out var binNumber); + var isBinNumber2Valid = TryGet(rowData, header, "BinNumber2", out var binNumber2); + var isCostValid = TryGet(rowData, header, "Cost", out var cost); + var isDatasheetUrlValid = TryGet(rowData, header, "DatasheetUrl", out var datasheetUrl); + var isDescriptionValid = TryGet(rowData, header, "Description", out var description); + var isDigiKeyPartNumberValid = TryGet(rowData, header, "DigiKeyPartNumber", out var digiKeyPartNumber); + var isImageUrlValid = TryGet(rowData, header, "ImageUrl", out var imageUrl); + var isKeywordsValid = TryGet(rowData, header, "Keywords", out var keywords); + var isLocationValid = TryGet(rowData, header, "Location", out var location); + var isLowestCostSupplierValid = TryGet(rowData, header, "LowestCostSupplier", out var lowestCostSupplier); + var isLowestCostSupplierUrlValid = TryGet(rowData, header, "LowestCostSupplierUrl", out var lowestCostSupplierUrl); + var isLowStockThresholdValid = TryGet(rowData, header, "LowStockThreshold", out var lowStockThreshold); + var isManufacturerValid = TryGet(rowData, header, "Manufacturer", out var manufacturer); + var isManufacturerPartNumberValid = TryGet(rowData, header, "ManufacturerPartNumber", out var manufacturerPartNumber); + var isMountingTypeIdValid = TryGet(rowData, header, "MountingTypeId", out var mountingTypeId); + var isMouserPartNumberValid = TryGet(rowData, header, "MouserPartNumber", out var mouserPartNumber); + var isPackageTypeValid = TryGet(rowData, header, "PackageType", out var packageType); + var isPartNumberValid = TryGet(rowData, header, "PartNumber", out var partNumber); + var isProductUrlValid = TryGet(rowData, header, "ProductUrl", out var productUrl); + var isProjectIdValid = TryGet(rowData, header, "ProjectId", out var projectId); + var isQuantityValid = TryGet(rowData, header, "Quantity", out var quantity); + var isSwarmPartNumberManufacturerIdValid = TryGet(rowData, header, "SwarmPartNumberManufacturerId", out var swarmPartNumberManufacturerId); + var isDateCreatedValid = TryGet(rowData, header, "DateCreatedUtc", out var dateCreatedUtc); + + if (!isPartTypeIdValid || !isBinNumberValid || !isBinNumber2Valid || !isCostValid || !isDatasheetUrlValid + || !isDescriptionValid || !isDigiKeyPartNumberValid || !isImageUrlValid || !isKeywordsValid || !isLocationValid || !isLowestCostSupplierValid + || !isLowestCostSupplierUrlValid || !isLowStockThresholdValid || !isManufacturerValid || !isManufacturerPartNumberValid || !isMountingTypeIdValid || !isMouserPartNumberValid + || !isPackageTypeValid || !isPartNumberValid || !isProductUrlValid || !isProjectIdValid || !isQuantityValid || !isSwarmPartNumberManufacturerIdValid) + continue; + + if (!string.IsNullOrEmpty(partNumber) && await _storageProvider.GetPartAsync(partNumber, userContext) == null) + { + var part = new Part + { + PartTypeId = _temporaryKeyTracker.GetMappedId("PartTypes", "PartTypeId", partTypeId), + BinNumber = binNumber, + BinNumber2 = binNumber2, + Cost = cost, + DatasheetUrl = datasheetUrl, + Description = description, + DigiKeyPartNumber = digiKeyPartNumber, + ImageUrl = imageUrl, + Keywords = !string.IsNullOrEmpty(keywords) ? keywords.Split(new string[] { "," }, StringSplitOptions.RemoveEmptyEntries) : null, + Location = location, + LowestCostSupplier = lowestCostSupplier, + LowestCostSupplierUrl = lowestCostSupplierUrl, + LowStockThreshold = lowStockThreshold, + Manufacturer = manufacturer, + ManufacturerPartNumber = manufacturerPartNumber, + MountingTypeId = mountingTypeId, + MouserPartNumber = mouserPartNumber, + PackageType = packageType, + PartNumber = partNumber, + ProductUrl = productUrl, + ProjectId = projectId != null ? _temporaryKeyTracker.GetMappedId("Projects", "ProjectId", projectId.Value) : null, + Quantity = quantity, + //SwarmPartNumberManufacturerId = swarmPartNumberManufacturerId, + DateCreatedUtc = dateCreatedUtc + }; + // some data validation required + if (part.ProjectId == 0) part.ProjectId = null; + if (part.UserId == 0) part.UserId = userContext?.UserId; + if (part.PartTypeId == 0) part.PartTypeId = (long)SystemDefaults.DefaultPartTypes.Other; + try + { + part = await _storageProvider.AddPartAsync(part, userContext); + _temporaryKeyTracker.AddKeyMapping("Parts", "PartId", partId, part.PartId); + result.TotalRowsImported++; + result.RowsImportedByTable["Parts"]++; + } + catch (Exception ex) + { + // failed to add part + result.Errors.Add($"[Row {rowNumber}, Part with PartNumber '{partNumber}' could not be added. Error: {ex.Message}"); + } + } + else + { + result.Warnings.Add($"[Row {rowNumber}] Part with PartNumber '{partNumber}' already exists."); + } + } + break; + } + rowNumber++; + } + } + catch (Exception ex) + { + result.Errors.Add(ex.Message); + } +*/ + result.Success = !result.Errors.Any(); + return result; + } + + private bool TryGet(string?[] rowData, Header header, string name, out T? value) + { + value = default; + var type = typeof(T); + var columnIndex = header.GetHeaderIndex(name); + if (columnIndex < 0 || columnIndex >= rowData.Length) + { + value = default; + return true; + } + + if (Nullable.GetUnderlyingType(type) != null && rowData[columnIndex] == null) + return true; + var unquotedValue = GetQuoted(rowData[columnIndex]); + + if (type == typeof(string)) + { + if (!string.IsNullOrEmpty(unquotedValue)) + value = (T)(object)unquotedValue; + return true; + } + if (type == typeof(long) || type == typeof(long?)) + { + var isLongValid = long.TryParse(unquotedValue, out var longValue); + value = (T)(object)longValue; + return isLongValid; + } + if (type == typeof(int) || type == typeof(int?)) + { + var isIntValid = int.TryParse(unquotedValue, out var intValue); + value = (T)(object)intValue; + return isIntValid; + } + if (type == typeof(bool) || type == typeof(bool?)) + { + var isIntValid = bool.TryParse(unquotedValue, out var boolValue); + value = (T)(object)boolValue; + return isIntValid; + } + if (type == typeof(DateTime) || type == typeof(DateTime?)) + { + var isDateValid = DateTime.TryParse(unquotedValue, out var dateValue); + value = (T)(object)dateValue; + return isDateValid; + } + if (type == typeof(double) || type == typeof(double?)) + { + var isDoubleValid = double.TryParse(unquotedValue, out var doubleValue); + value = (T)(object)doubleValue; + return isDoubleValid; + } + return false; + } + + private string? GetQuoted(string? val) + { + if (val == null) + return null; + var match = Regex.Match(val, @"'[^']*'|\""[^\""]*\"""); + if (match.Success) + { + var value = match.Value.Substring(1, match.Value.Length - 2); + // decode line breaks + value = value.Replace("\\r", "\r").Replace("\\n", "\n").Replace("\\t", "\t"); + return value; + } + // decode line breaks + val = val.Replace("\\r", "\r").Replace("\\n", "\n").Replace("\\t", "\t"); + return val; + } + + private string[] SplitBoundaries(string data, char[] rowDelimiters, bool removeBoundary = false) + { + var rows = new List(); + var quotes = new List { '"', '\'' }; + var startPos = 0; + var insideQuotes = false; + var insideQuotesChar = '\0'; + for (var i = 0; i < data.Length; i++) + { + var c = data[i]; + if (quotes.Contains(c)) + { + if (!insideQuotes) + { + insideQuotes = true; + insideQuotesChar = c; + } + else if (c == insideQuotesChar) + { + insideQuotes = false; + } + } + if ((rowDelimiters.Any(x => x.Equals(c)) && !insideQuotes) || i == data.Length - 1) + { + var row = data.Substring(startPos, i - startPos + 1 - (removeBoundary && !(i == data.Length - 1) ? 1 : 0)); + if (!string.IsNullOrEmpty(row) && row.Length > (removeBoundary ? 0 : 1)) + rows.Add(row); + startPos = i + 1; + } + } + + return rows.ToArray(); + } + + public class Header + { + public List Headers { get; set; } = new List(); + + public Header(string headerRow) + { + var headers = headerRow.Split(new string[] { "," }, StringSplitOptions.TrimEntries); + for (var i = 0; i < headers.Length; i++) + { + var name = headers[i].Replace("'", "").Replace("\"", ""); + if (name.StartsWith("#")) + name = name.Substring(1); + Headers.Add(new HeaderIndex(name, i)); + } + } + + public int GetHeaderIndex(string name) + { + var header = Headers.FirstOrDefault(x => x.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase)); + if (header == null) + return -1; + return header.Index; + } + } + + public class HeaderIndex + { + public string Name { get; set; } + public int Index { get; set; } + + public HeaderIndex(string name, int index) + { + Name = name; + Index = index; + } + + public override string ToString() + => Name; + } + } +} diff --git a/Binner/Library/Binner.Common/IO/ExcelBOMImporter.cs b/Binner/Library/Binner.Common/IO/ExcelBOMImporter.cs new file mode 100644 index 00000000..ee2ff6c8 --- /dev/null +++ b/Binner/Library/Binner.Common/IO/ExcelBOMImporter.cs @@ -0,0 +1,212 @@ +using Binner.Global.Common; +using Binner.Model; +using Microsoft.EntityFrameworkCore.Metadata.Internal; +using NPOI.SS.UserModel; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace Binner.Common.IO +{ + /// + /// Imports data from Excel Open XML Format 2007+ (XLSX) + /// + public class ExcelBOMImporter + { + // SupportedTables ordering matters when it comes to relational data! + private readonly IStorageProvider _storageProvider; + + public ExcelBOMImporter(IStorageProvider storageProvider) + { + _storageProvider = storageProvider; + } + + public async Task ImportAsync(Project project, Stream stream, IUserContext? userContext) + { + var result = new ImportResult(); + try + { + stream.Position = 0; + var workbook = WorkbookFactory.Create(stream); + var worksheet = workbook.GetSheetAt(0); + if (worksheet != null) + { + // parse worksheet + var header = new Header(worksheet.GetRow(0)); + for (var rowNumber = 1; rowNumber <= worksheet.LastRowNum; rowNumber++) + { + var rowData = worksheet.GetRow(rowNumber); + if (rowData == null) + continue; + + // import BOM info + var isPartNumberValid = TryGet(rowData, header, "MPN", out var partNumber); + var isQuantityValid = TryGet(rowData, header, "Qty", out var quantity); + var isReferenceValid = TryGet(rowData, header, "Reference", out var reference); + var isNoteValid = TryGet(rowData, header, "Value", out var note); + + if (!isPartNumberValid || !isQuantityValid || !isReferenceValid) + continue; + + ProjectPartAssignment assignment = new ProjectPartAssignment(); + assignment.ProjectId = project.ProjectId; + assignment.Quantity = quantity; + assignment.Notes = note; + assignment.SchematicReferenceId = reference; + + var part = await _storageProvider.GetPartAsync(partNumber, userContext); + if (part != null) + { + assignment.PartName = part.PartNumber; + assignment.PartId = part.PartId; + } + else + { + part = new Part(); + part.PartNumber = partNumber; + part.Quantity = 0; + part.UserId = userContext?.UserId; + part.PartTypeId = (long)SystemDefaults.DefaultPartTypes.Other; + try + { + part = await _storageProvider.AddPartAsync(part, userContext); + } + catch (Exception ex) + { + // failed to add part + result.Errors.Add($"[Row {rowNumber}'] Part with PartNumber '{partNumber}' could not be added. Error: {ex.Message}"); + } + + assignment.PartName = partNumber; + assignment.PartId = part.PartId; + } + + try + { + await _storageProvider.AddProjectPartAssignmentAsync(assignment, userContext); + } + catch (Exception ex) + { + result.Errors.Add($"[Row {rowNumber}] BOM entry '{partNumber}' could not be added. Error: {ex.Message}"); + } + } + } + } + catch (Exception ex) + { + result.Errors.Add(ex.Message); + } + + result.Success = !result.Errors.Any(); + + return result; + } + + private bool TryGet(IRow rowData, Header header, string name, out T? value) + { + value = default; + var type = typeof(T); + var columnIndex = header.GetHeaderIndex(name); + var cellValue = columnIndex >= 0 ? rowData.GetCell(columnIndex) : null; + if (Nullable.GetUnderlyingType(type) != null && cellValue?.ToString() == null) + return true; + var unquotedValue = GetQuoted(cellValue?.ToString()); + + if (type == typeof(string)) + { + if (!string.IsNullOrEmpty(unquotedValue)) + value = (T)(object)unquotedValue; + return true; + } + if (type == typeof(long) || type == typeof(long?)) + { + var isLongValid = long.TryParse(unquotedValue, out var longValue); + value = (T)(object)longValue; + return isLongValid; + } + if (type == typeof(int) || type == typeof(int?)) + { + var isIntValid = int.TryParse(unquotedValue, out var intValue); + value = (T)(object)intValue; + return isIntValid; + } + if (type == typeof(bool) || type == typeof(bool?)) + { + var isIntValid = bool.TryParse(unquotedValue, out var boolValue); + value = (T)(object)boolValue; + return isIntValid; + } + if (type == typeof(DateTime) || type == typeof(DateTime?)) + { + var isDateValid = DateTime.TryParse(unquotedValue, out var dateValue); + value = (T)(object)dateValue; + return isDateValid; + } + if (type == typeof(double) || type == typeof(double?)) + { + var isDoubleValid = double.TryParse(unquotedValue, out var doubleValue); + value = (T)(object)doubleValue; + return isDoubleValid; + } + return false; + } + + private string? GetQuoted(string? val) + { + if (val == null) + return null; + var match = Regex.Match(val, @"'[^']*'|\""[^\""]*\"""); + if (match.Success) + { + var value = match.Value.Substring(1, match.Value.Length - 2); + // decode line breaks + value = value.Replace("\\r", "\r").Replace("\\n", "\n").Replace("\\t", "\t"); + return value; + } + // decode line breaks + val = val.Replace("\\r", "\r").Replace("\\n", "\n").Replace("\\t", "\t"); + return val; + } + + public class Header + { + public List Headers { get; set; } = new List(); + + public Header(IRow headerRow) + { + for (var i = 0; i < headerRow.LastCellNum; i++) + { + var headerName = headerRow.GetCell(i).StringCellValue; + var name = headerName.Replace("'", "").Replace("\"", ""); + Headers.Add(new HeaderIndex(name, i)); + } + } + + public int GetHeaderIndex(string name) + { + var header = Headers.FirstOrDefault(x => x.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase)); + if (header == null) + return -1; + return header.Index; + } + } + + public class HeaderIndex + { + public string Name { get; set; } + public int Index { get; set; } + + public HeaderIndex(string name, int index) + { + Name = name; + Index = index; + } + + public override string ToString() + => Name; + } + } +} diff --git a/Binner/Library/Binner.Common/IO/ExcelDataImporter.cs b/Binner/Library/Binner.Common/IO/ExcelDataImporter.cs index 9c9703b4..e386b480 100644 --- a/Binner/Library/Binner.Common/IO/ExcelDataImporter.cs +++ b/Binner/Library/Binner.Common/IO/ExcelDataImporter.cs @@ -8,7 +8,6 @@ using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; -using TypeSupport.Extensions; namespace Binner.Common.IO { @@ -18,7 +17,7 @@ namespace Binner.Common.IO public class ExcelDataImporter : IDataImporter { // SupportedTables ordering matters when it comes to relational data! - private readonly string[] SupportedTables = new string[] { "Projects", "PartTypes", "Parts", "BOM" }; + private readonly string[] SupportedTables = new string[] { "Projects", "PartTypes", "Parts" }; private readonly IStorageProvider _storageProvider; private readonly TemporaryKeyTracker _temporaryKeyTracker = new TemporaryKeyTracker(); @@ -61,32 +60,6 @@ public async Task ImportAsync(string filename, Stream stream, IUse foreach (var table in SupportedTables) result.RowsImportedByTable.Add(table, 0); - long bomProjectId = 0; - if (filename.Contains("_bom")) - { - string projectName = filename.Remove(filename.LastIndexOf("_bom")); - Project project = await _storageProvider.GetProjectAsync(projectName, userContext); - if (project == null) - { - project = new Project(); - project.Name = projectName; - try - { - project = await _storageProvider.AddProjectAsync(project, userContext); - bomProjectId = project.ProjectId; - } - catch (Exception ex) - { - result.Errors.Add($"[Sheet '{projectName}'] Project with name '{projectName}' could not be added. Error: {ex.Message}"); - } - - } - else - { - bomProjectId = project.ProjectId; - } - - } // get the global part types, and the user's custom part types var partTypes = (await _storageProvider.GetPartTypesAsync(userContext)).ToList(); try @@ -107,61 +80,6 @@ public async Task ImportAsync(string filename, Stream stream, IUse continue; switch (table.ToLower()) { - case "bom": - { - // import BOM info - var isPartNumberValid = TryGet(rowData, header, "MPN", out var partNumber); - var isQuantityValid = TryGet(rowData, header, "Qty", out var quantity); - var isReferenceValid = TryGet(rowData, header, "Reference", out var reference); - var isNoteValid = TryGet(rowData, header, "Value", out var note); - - if (!isPartNumberValid || !isQuantityValid || !isReferenceValid) - continue; - - ProjectPartAssignment assignment = new ProjectPartAssignment(); - assignment.ProjectId = bomProjectId; - assignment.Quantity = quantity; - assignment.Notes = note; - assignment.SchematicReferenceId = reference; - - var part = await _storageProvider.GetPartAsync(partNumber, userContext); - if (part != null) - { - assignment.PartName = part.PartNumber; - assignment.PartId = part.PartId; - } - else - { - part = new Part(); - part.PartNumber = partNumber; - part.Quantity = 0; - part.UserId = userContext?.UserId; - part.PartTypeId = (long)SystemDefaults.DefaultPartTypes.Other; - try - { - part = await _storageProvider.AddPartAsync(part, userContext); - } - catch (Exception ex) - { - // failed to add part - result.Errors.Add($"[Row {rowNumber}, Sheet '{table}'] Part with PartNumber '{partNumber}' could not be added. Error: {ex.Message}"); - } - - assignment.PartName = partNumber; - assignment.PartId = part.PartId; - } - - try - { - await _storageProvider.AddProjectPartAssignmentAsync(assignment, userContext); - } - catch (Exception ex) - { - result.Errors.Add($"[Row {rowNumber}, Sheet '{table}'] BOM entry '{partNumber}' could not be added. Error: {ex.Message}"); - } - - } - break; case "projects": { // import project info diff --git a/Binner/Library/Binner.Common/Services/IProjectService.cs b/Binner/Library/Binner.Common/Services/IProjectService.cs index ab8e0f84..80c6cf31 100644 --- a/Binner/Library/Binner.Common/Services/IProjectService.cs +++ b/Binner/Library/Binner.Common/Services/IProjectService.cs @@ -20,7 +20,7 @@ public interface IProjectService /// /// /// - Task ImportProjectAsync(Project project); + Task ImportProjectAsync(ImportProjectRequest request); /// /// Update an existing project diff --git a/Binner/Library/Binner.Common/Services/ProjectService.cs b/Binner/Library/Binner.Common/Services/ProjectService.cs index d8a152fe..09849d15 100644 --- a/Binner/Library/Binner.Common/Services/ProjectService.cs +++ b/Binner/Library/Binner.Common/Services/ProjectService.cs @@ -1,5 +1,6 @@ using AutoMapper; using Binner.Data; +using Binner.Common.IO; using Binner.Global.Common; using Binner.Model; using Binner.Model.Requests; @@ -7,6 +8,7 @@ using Microsoft.EntityFrameworkCore; using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Threading.Tasks; using Mapper = AnyMapper.Mapper; @@ -33,10 +35,54 @@ public async Task AddProjectAsync(Project project) return await _storageProvider.AddProjectAsync(project, _requestContext.GetUserContext()); } - public async Task ImportProjectAsync(Project project) + public async Task ImportProjectAsync(ImportProjectRequest request) { - return null; - //return await _storageProvider.AddProjectAsync(project, _requestContext.GetUserContext()); + var stream = new MemoryStream(); + await request.File.CopyToAsync(stream); + stream.Position = 0; + + var userContext = _requestContext.GetUserContext(); + string projectName = request.Name; + Project project = await _storageProvider.GetProjectAsync(projectName, userContext); + if (project == null) + { + project = new Project(); + project.Name = projectName; + project.Description = request.Description; + try + { + project = await _storageProvider.AddProjectAsync(project, userContext); + } + catch (Exception ex) + { + } + + } + + ImportResult result = null; + if (project != null) { + var extension = Path.GetExtension(request.File.FileName); + switch (extension.ToLower()) + { + case ".csv": + var csvImporter = new CsvBOMImporter(_storageProvider); + result = await csvImporter.ImportAsync(project, stream, userContext); + break; + case ".xls": + case ".xlsx": + case ".xlsm": + case ".xlsb": + var excelImporter = new ExcelBOMImporter(_storageProvider); + result = await excelImporter.ImportAsync(project, stream, userContext); + break; + } + + if (result != null && !result.Success) { + project = null; + } + } + + return project; } public async Task DeleteProjectAsync(Project project) From 0deb5f370fafacc36b3733387f280b48bddf09c6 Mon Sep 17 00:00:00 2001 From: shaun-leach Date: Sun, 23 Jun 2024 17:10:29 -0700 Subject: [PATCH 06/10] Remove unneeded using and whitespace --- Binner/Library/Binner.Common/IO/ExcelDataImporter.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/Binner/Library/Binner.Common/IO/ExcelDataImporter.cs b/Binner/Library/Binner.Common/IO/ExcelDataImporter.cs index e386b480..2de56cf7 100644 --- a/Binner/Library/Binner.Common/IO/ExcelDataImporter.cs +++ b/Binner/Library/Binner.Common/IO/ExcelDataImporter.cs @@ -1,6 +1,5 @@ using Binner.Global.Common; using Binner.Model; -using Microsoft.EntityFrameworkCore.Metadata.Internal; using NPOI.SS.UserModel; using System; using System.Collections.Generic; @@ -59,7 +58,6 @@ public async Task ImportAsync(string filename, Stream stream, IUse var result = new ImportResult(); foreach (var table in SupportedTables) result.RowsImportedByTable.Add(table, 0); - // get the global part types, and the user's custom part types var partTypes = (await _storageProvider.GetPartTypesAsync(userContext)).ToList(); try From 515296e093b5921424ea366999972bf3f70c8523 Mon Sep 17 00:00:00 2001 From: shaun-leach Date: Sun, 23 Jun 2024 17:17:15 -0700 Subject: [PATCH 07/10] Refresh projects page after importing a new BOM --- Binner/Binner.Web/ClientApp/src/pages/Boms.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Binner/Binner.Web/ClientApp/src/pages/Boms.js b/Binner/Binner.Web/ClientApp/src/pages/Boms.js index 78565a38..cd856b51 100644 --- a/Binner/Binner.Web/ClientApp/src/pages/Boms.js +++ b/Binner/Binner.Web/ClientApp/src/pages/Boms.js @@ -194,6 +194,10 @@ export function Boms (props) { toast.dismiss(); if (response.status === 200) { toast.success(t("importProjectSuccess", "BOM Imported!")); + setProject(defaultProject); + setAddVisible(false); + setImportVisible(false); + loadProjects(page, pageSize, true); } else { toast.error(t("importProjectFailed", `Failed to import BOM!`), { autoClose: 10000 }); } From 70d8a9c22b81006a872f884802ba2ea40b13a206 Mon Sep 17 00:00:00 2001 From: shaun-leach Date: Mon, 24 Jun 2024 20:44:36 -0700 Subject: [PATCH 08/10] CSV import support --- Binner/Binner.Web/ClientApp/src/pages/Boms.js | 2 +- .../Binner.Common/IO/CsvBOMImporter.cs | 223 ++++-------------- 2 files changed, 50 insertions(+), 175 deletions(-) diff --git a/Binner/Binner.Web/ClientApp/src/pages/Boms.js b/Binner/Binner.Web/ClientApp/src/pages/Boms.js index cd856b51..ce45514b 100644 --- a/Binner/Binner.Web/ClientApp/src/pages/Boms.js +++ b/Binner/Binner.Web/ClientApp/src/pages/Boms.js @@ -373,7 +373,7 @@ export function Boms (props) { > {t('page.exportData.uploadNote', "Drag a document to upload, or click to select files")} -
{t('page.exportData.acceptedFileTypes', "Accepted file types: \"*.xls, *.xlsx, *.csv\"")}
+
{t('page.projects.acceptedFileTypes', "Accepted file types: \"*.xls, *.xlsx, *.csv\"")}
{error && (
diff --git a/Binner/Library/Binner.Common/IO/CsvBOMImporter.cs b/Binner/Library/Binner.Common/IO/CsvBOMImporter.cs index f3b06aa7..52c1be20 100644 --- a/Binner/Library/Binner.Common/IO/CsvBOMImporter.cs +++ b/Binner/Library/Binner.Common/IO/CsvBOMImporter.cs @@ -31,10 +31,6 @@ public CsvBOMImporter(IStorageProvider storageProvider) public async Task ImportAsync(Project project, Stream stream, IUserContext? userContext) { var result = new ImportResult(); -/* foreach (var table in SupportedTables) - result.RowsImportedByTable.Add(table, 0); - // get the global part types, and the user's custom part types - var partTypes = (await _storageProvider.GetPartTypesAsync(userContext)).ToList(); try { stream.Position = 0; @@ -43,14 +39,6 @@ public async Task ImportAsync(Project project, Stream stream, IUse // remove line breaks data = data.Replace("\r", ""); - var tableName = Path.GetFileNameWithoutExtension(filename); - if (!SupportedTables.Contains(tableName, StringComparer.InvariantCultureIgnoreCase)) - { - result.Errors.Add($"Filename '{tableName}' not a supported table name! Filename must be one of the following: {string.Join(",", SupportedTables)} and may optionally contain the schema prefix 'dbo'."); - result.Success = false; - return result; - } - var rows = SplitBoundaries(data, new char[] { '\n' }); if (!rows.Any()) { @@ -62,7 +50,7 @@ public async Task ImportAsync(Project project, Stream stream, IUse // read csv header Header? header = null; var headerRow = rows.First(); - if (headerRow.StartsWith("#")) + if (headerRow.StartsWith("\"#\"")) { header = new Header(headerRow); } @@ -87,171 +75,58 @@ public async Task ImportAsync(Project project, Stream stream, IUse result.Warnings.Add($"[Row {rowNumber}] Row does not contain the same number of columns as the header, skipping..."); continue; } - switch (tableName.ToLower()) - { - case "projects": - { - // import project info - var isProjectIdValid = TryGet(rowData, header, "ProjectId", out var projectId); - var isColorValid = TryGet(rowData, header, "Color", out var color); - var isDateCreatedValid = TryGet(rowData, header, "DateCreatedUtc", out var dateCreatedUtc); - var isDateModifiedValid = TryGet(rowData, header, "DateModifiedUtc", out var dateModifiedUtc); - if (!isColorValid || !isDateCreatedValid || !isDateModifiedValid) - continue; - var name = GetQuoted(GetValueFromHeader(rowData, header, "Name"))?.Trim(); - if (!string.IsNullOrEmpty(name) && await _storageProvider.GetProjectAsync(name, userContext) == null) - { - var project = new Project - { - Name = name, - Description = GetQuoted(GetValueFromHeader(rowData, header, "Description")), - Location = GetQuoted(GetValueFromHeader(rowData, header, "Location")), - Color = color, - DateCreatedUtc = dateCreatedUtc - }; - try - { - project = await _storageProvider.AddProjectAsync(project, userContext); - _temporaryKeyTracker.AddKeyMapping("Projects", "ProjectId", projectId, project.ProjectId); - result.TotalRowsImported++; - result.RowsImportedByTable["Projects"]++; - } - catch (Exception ex) - { - result.Errors.Add($"[Row {rowNumber}, Project with name '{name}' could not be added. Error: {ex.Message}"); - } - } - else - { - result.Warnings.Add($"[Row {rowNumber}] Project with name '{name}' already exists."); - } - } - break; - case "parttypes": - { - // import partTypes info - var isPartTypeIdValid = TryGet(rowData, header, "PartTypeId", out var partTypeId); - var isParentPartTypeIdValid = TryGet(rowData, header, "ParentPartTypeId", out var parentPartTypeId); - var isDateCreatedValid = TryGet(rowData, header, "DateCreatedUtc", out var dateCreatedUtc); - if (!isParentPartTypeIdValid || !isDateCreatedValid) - continue; + // import BOM info + var isPartNumberValid = TryGet(rowData, header, "MPN", out var partNumber); + var isQuantityValid = TryGet(rowData, header, "Qty", out var quantity); + var isReferenceValid = TryGet(rowData, header, "Reference", out var reference); + var isNoteValid = TryGet(rowData, header, "Value", out var note); + + if (!isPartNumberValid || !isQuantityValid || !isReferenceValid) + continue; + + ProjectPartAssignment assignment = new ProjectPartAssignment(); + assignment.ProjectId = project.ProjectId; + assignment.Quantity = quantity; + assignment.Notes = note; + assignment.SchematicReferenceId = reference; - var name = GetQuoted(GetValueFromHeader(rowData, header, "Name"))?.Trim(); - // part types need to have a unique name for the user and can not be part of global part types - if (!string.IsNullOrEmpty(name) && !partTypes.Any(x => x.Name?.Equals(name, StringComparison.InvariantCultureIgnoreCase) == true)) - { - if (parentPartTypeId == 0) - parentPartTypeId = null; - var partType = new PartType - { - ParentPartTypeId = parentPartTypeId != null ? _temporaryKeyTracker.GetMappedId("PartTypes", "PartTypeId", parentPartTypeId.Value) : null, - Name = name, - DateCreatedUtc = dateCreatedUtc - }; - partType = await _storageProvider.GetOrCreatePartTypeAsync(partType, userContext); - if (partType != null) - { - _temporaryKeyTracker.AddKeyMapping("PartTypes", "PartTypeId", partTypeId, - partType.PartTypeId); - result.TotalRowsImported++; - result.RowsImportedByTable["PartTypes"]++; - } - } - else - { - result.Warnings.Add($"[Row {rowNumber}] PartType with name '{name}' already exists."); - } - } - break; - case "parts": - { - // import parts info - var isPartIdValid = TryGet(rowData, header, "PartId", out var partId); - var isPartTypeIdValid = TryGet(rowData, header, "PartTypeId", out var partTypeId); - var isBinNumberValid = TryGet(rowData, header, "BinNumber", out var binNumber); - var isBinNumber2Valid = TryGet(rowData, header, "BinNumber2", out var binNumber2); - var isCostValid = TryGet(rowData, header, "Cost", out var cost); - var isDatasheetUrlValid = TryGet(rowData, header, "DatasheetUrl", out var datasheetUrl); - var isDescriptionValid = TryGet(rowData, header, "Description", out var description); - var isDigiKeyPartNumberValid = TryGet(rowData, header, "DigiKeyPartNumber", out var digiKeyPartNumber); - var isImageUrlValid = TryGet(rowData, header, "ImageUrl", out var imageUrl); - var isKeywordsValid = TryGet(rowData, header, "Keywords", out var keywords); - var isLocationValid = TryGet(rowData, header, "Location", out var location); - var isLowestCostSupplierValid = TryGet(rowData, header, "LowestCostSupplier", out var lowestCostSupplier); - var isLowestCostSupplierUrlValid = TryGet(rowData, header, "LowestCostSupplierUrl", out var lowestCostSupplierUrl); - var isLowStockThresholdValid = TryGet(rowData, header, "LowStockThreshold", out var lowStockThreshold); - var isManufacturerValid = TryGet(rowData, header, "Manufacturer", out var manufacturer); - var isManufacturerPartNumberValid = TryGet(rowData, header, "ManufacturerPartNumber", out var manufacturerPartNumber); - var isMountingTypeIdValid = TryGet(rowData, header, "MountingTypeId", out var mountingTypeId); - var isMouserPartNumberValid = TryGet(rowData, header, "MouserPartNumber", out var mouserPartNumber); - var isPackageTypeValid = TryGet(rowData, header, "PackageType", out var packageType); - var isPartNumberValid = TryGet(rowData, header, "PartNumber", out var partNumber); - var isProductUrlValid = TryGet(rowData, header, "ProductUrl", out var productUrl); - var isProjectIdValid = TryGet(rowData, header, "ProjectId", out var projectId); - var isQuantityValid = TryGet(rowData, header, "Quantity", out var quantity); - var isSwarmPartNumberManufacturerIdValid = TryGet(rowData, header, "SwarmPartNumberManufacturerId", out var swarmPartNumberManufacturerId); - var isDateCreatedValid = TryGet(rowData, header, "DateCreatedUtc", out var dateCreatedUtc); + var part = await _storageProvider.GetPartAsync(partNumber, userContext); + if (part != null) + { + assignment.PartName = part.PartNumber; + assignment.PartId = part.PartId; + } + else + { + part = new Part(); + part.PartNumber = partNumber; + part.Quantity = 0; + part.UserId = userContext?.UserId; + part.PartTypeId = (long)SystemDefaults.DefaultPartTypes.Other; + try + { + part = await _storageProvider.AddPartAsync(part, userContext); + } + catch (Exception ex) + { + // failed to add part + result.Errors.Add($"[Row {rowNumber}'] Part with PartNumber '{partNumber}' could not be added. Error: {ex.Message}"); + } - if (!isPartTypeIdValid || !isBinNumberValid || !isBinNumber2Valid || !isCostValid || !isDatasheetUrlValid - || !isDescriptionValid || !isDigiKeyPartNumberValid || !isImageUrlValid || !isKeywordsValid || !isLocationValid || !isLowestCostSupplierValid - || !isLowestCostSupplierUrlValid || !isLowStockThresholdValid || !isManufacturerValid || !isManufacturerPartNumberValid || !isMountingTypeIdValid || !isMouserPartNumberValid - || !isPackageTypeValid || !isPartNumberValid || !isProductUrlValid || !isProjectIdValid || !isQuantityValid || !isSwarmPartNumberManufacturerIdValid) - continue; + assignment.PartName = partNumber; + assignment.PartId = part.PartId; + } - if (!string.IsNullOrEmpty(partNumber) && await _storageProvider.GetPartAsync(partNumber, userContext) == null) - { - var part = new Part - { - PartTypeId = _temporaryKeyTracker.GetMappedId("PartTypes", "PartTypeId", partTypeId), - BinNumber = binNumber, - BinNumber2 = binNumber2, - Cost = cost, - DatasheetUrl = datasheetUrl, - Description = description, - DigiKeyPartNumber = digiKeyPartNumber, - ImageUrl = imageUrl, - Keywords = !string.IsNullOrEmpty(keywords) ? keywords.Split(new string[] { "," }, StringSplitOptions.RemoveEmptyEntries) : null, - Location = location, - LowestCostSupplier = lowestCostSupplier, - LowestCostSupplierUrl = lowestCostSupplierUrl, - LowStockThreshold = lowStockThreshold, - Manufacturer = manufacturer, - ManufacturerPartNumber = manufacturerPartNumber, - MountingTypeId = mountingTypeId, - MouserPartNumber = mouserPartNumber, - PackageType = packageType, - PartNumber = partNumber, - ProductUrl = productUrl, - ProjectId = projectId != null ? _temporaryKeyTracker.GetMappedId("Projects", "ProjectId", projectId.Value) : null, - Quantity = quantity, - //SwarmPartNumberManufacturerId = swarmPartNumberManufacturerId, - DateCreatedUtc = dateCreatedUtc - }; - // some data validation required - if (part.ProjectId == 0) part.ProjectId = null; - if (part.UserId == 0) part.UserId = userContext?.UserId; - if (part.PartTypeId == 0) part.PartTypeId = (long)SystemDefaults.DefaultPartTypes.Other; - try - { - part = await _storageProvider.AddPartAsync(part, userContext); - _temporaryKeyTracker.AddKeyMapping("Parts", "PartId", partId, part.PartId); - result.TotalRowsImported++; - result.RowsImportedByTable["Parts"]++; - } - catch (Exception ex) - { - // failed to add part - result.Errors.Add($"[Row {rowNumber}, Part with PartNumber '{partNumber}' could not be added. Error: {ex.Message}"); - } - } - else - { - result.Warnings.Add($"[Row {rowNumber}] Part with PartNumber '{partNumber}' already exists."); - } - } - break; + try + { + await _storageProvider.AddProjectPartAssignmentAsync(assignment, userContext); } + catch (Exception ex) + { + result.Errors.Add($"[Row {rowNumber}] BOM entry '{partNumber}' could not be added. Error: {ex.Message}"); + } + rowNumber++; } } @@ -259,7 +134,7 @@ public async Task ImportAsync(Project project, Stream stream, IUse { result.Errors.Add(ex.Message); } -*/ + result.Success = !result.Errors.Any(); return result; } From 4a1f87adcf0e64aa38c0cdaf8befaeec9e34f5e1 Mon Sep 17 00:00:00 2001 From: shaun-leach Date: Thu, 27 Jun 2024 16:48:18 -0700 Subject: [PATCH 09/10] Properly clean up a failed import --- Binner/Library/Binner.Common/Services/ProjectService.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Binner/Library/Binner.Common/Services/ProjectService.cs b/Binner/Library/Binner.Common/Services/ProjectService.cs index 09849d15..cf329b8e 100644 --- a/Binner/Library/Binner.Common/Services/ProjectService.cs +++ b/Binner/Library/Binner.Common/Services/ProjectService.cs @@ -78,6 +78,7 @@ public async Task AddProjectAsync(Project project) } if (result != null && !result.Success) { + DeleteProjectAsync(project); project = null; } } From 8ecd0b2cbbabf1bf4fdf6b5ddc5315e00928b8b7 Mon Sep 17 00:00:00 2001 From: shaun-leach Date: Sat, 29 Jun 2024 16:58:46 -0700 Subject: [PATCH 10/10] Add translations. Clean up JS code --- .../ClientApp/public/locales/de/translation.json | 6 ++++++ .../ClientApp/public/locales/en/translation.json | 6 ++++++ .../ClientApp/public/locales/es/translation.json | 6 ++++++ .../ClientApp/public/locales/fr/translation.json | 6 ++++++ .../ClientApp/public/locales/it/translation.json | 6 ++++++ .../ClientApp/public/locales/zh/translation.json | 6 ++++++ Binner/Binner.Web/ClientApp/src/pages/Boms.js | 12 +++--------- 7 files changed, 39 insertions(+), 9 deletions(-) diff --git a/Binner/Binner.Web/ClientApp/public/locales/de/translation.json b/Binner/Binner.Web/ClientApp/public/locales/de/translation.json index 30304e22..35fe9b3f 100644 --- a/Binner/Binner.Web/ClientApp/public/locales/de/translation.json +++ b/Binner/Binner.Web/ClientApp/public/locales/de/translation.json @@ -99,6 +99,11 @@ "boms": { "header": { "description": "Die Stückliste ermöglicht es Ihnen, den Lagerbestand pro Projekt zu verwalten. Sie können die Mengen für jede hergestellte Leiterplatte reduzieren, und überprüfen welche Teile Sie in höherer Stückzahl kaufen müssen um die Kosten zu analysieren.

Wählen Sie das Projekt aus, für das Sie die Stückliste (BOM) verwalten möchten, oder erstellen Sie ein neues Projekt.
" + }, + "acceptedFileTypes": "Accepted file types: \"*.xls, *.xlsx, *.csv\"" + "import": { + "success": "BOM Imported!", + "failure": "Failed to import BOM!" } }, "partTypes": { @@ -636,6 +641,7 @@ }, "button": { "addBomProject": "Stücklistenprojekt hinzufügen", + "importBomProject": "Import BOM Project", "addPart": "Bauteil hinzufügen", "save": "Speichern", "download": "Download", diff --git a/Binner/Binner.Web/ClientApp/public/locales/en/translation.json b/Binner/Binner.Web/ClientApp/public/locales/en/translation.json index c511977d..0daec816 100644 --- a/Binner/Binner.Web/ClientApp/public/locales/en/translation.json +++ b/Binner/Binner.Web/ClientApp/public/locales/en/translation.json @@ -98,6 +98,11 @@ "boms": { "header": { "description": "Bill of Materials, or BOM allows you to manage inventory quantities per project. You can reduce quantities for each PCB you produce, check which parts you need to buy more of and analyze costs.

Choose or create the project to manage BOM for.
" + }, + "acceptedFileTypes": "Accepted file types: \"*.xls, *.xlsx, *.csv\"" + "import": { + "success": "BOM Imported!", + "failure": "Failed to import BOM!" } }, "partTypes": { @@ -638,6 +643,7 @@ }, "button": { "addBomProject": "Add BOM Project", + "importBomProject": "Import BOM Project", "addPart": "Add Part", "save": "Save", "download": "Download", diff --git a/Binner/Binner.Web/ClientApp/public/locales/es/translation.json b/Binner/Binner.Web/ClientApp/public/locales/es/translation.json index ceec8ee8..6e39df5f 100644 --- a/Binner/Binner.Web/ClientApp/public/locales/es/translation.json +++ b/Binner/Binner.Web/ClientApp/public/locales/es/translation.json @@ -89,6 +89,11 @@ "boms": { "header": { "description": "Bill of Materials, or BOM allows you to manage inventory quantities per project. You can reduce quantities for each PCB you produce, check which parts you need to buy more of and analyze costs.

Choose or create the project to manage BOM for.
" + }, + "acceptedFileTypes": "Accepted file types: \"*.xls, *.xlsx, *.csv\"" + "import": { + "success": "BOM Imported!", + "failure": "Failed to import BOM!" } }, "partTypes": { @@ -457,6 +462,7 @@ }, "button": { "addBomProject": "Add BOM Project", + "importBomProject": "Import BOM Project", "addPart": "Add Part", "save": "Save", "download": "Download", diff --git a/Binner/Binner.Web/ClientApp/public/locales/fr/translation.json b/Binner/Binner.Web/ClientApp/public/locales/fr/translation.json index ceec8ee8..6e39df5f 100644 --- a/Binner/Binner.Web/ClientApp/public/locales/fr/translation.json +++ b/Binner/Binner.Web/ClientApp/public/locales/fr/translation.json @@ -89,6 +89,11 @@ "boms": { "header": { "description": "Bill of Materials, or BOM allows you to manage inventory quantities per project. You can reduce quantities for each PCB you produce, check which parts you need to buy more of and analyze costs.

Choose or create the project to manage BOM for.
" + }, + "acceptedFileTypes": "Accepted file types: \"*.xls, *.xlsx, *.csv\"" + "import": { + "success": "BOM Imported!", + "failure": "Failed to import BOM!" } }, "partTypes": { @@ -457,6 +462,7 @@ }, "button": { "addBomProject": "Add BOM Project", + "importBomProject": "Import BOM Project", "addPart": "Add Part", "save": "Save", "download": "Download", diff --git a/Binner/Binner.Web/ClientApp/public/locales/it/translation.json b/Binner/Binner.Web/ClientApp/public/locales/it/translation.json index 036eb9fb..72e3a667 100644 --- a/Binner/Binner.Web/ClientApp/public/locales/it/translation.json +++ b/Binner/Binner.Web/ClientApp/public/locales/it/translation.json @@ -98,6 +98,11 @@ "boms": { "header": { "description": "La Distinta Base, (DBA), consente di gestire le quantità dei componenti per progetto. Si può ridurre le quantità di ogni PCB da produrre, verificare quali componenti occorre acquistare ancora e analizzare i costi.

Scegliere o creare il progetto di cui gestire la DBA.
" + }, + "acceptedFileTypes": "Accepted file types: \"*.xls, *.xlsx, *.csv\"" + "import": { + "success": "BOM Imported!", + "failure": "Failed to import BOM!" } }, "partTypes": { @@ -635,6 +640,7 @@ }, "button": { "addBomProject": "Aggiungi un Progetto DBA", + "importBomProject": "Import BOM Project", "addPart": "Aggiungi componente", "save": "Salva", "download": "Download", diff --git a/Binner/Binner.Web/ClientApp/public/locales/zh/translation.json b/Binner/Binner.Web/ClientApp/public/locales/zh/translation.json index 68d77010..2b87439c 100644 --- a/Binner/Binner.Web/ClientApp/public/locales/zh/translation.json +++ b/Binner/Binner.Web/ClientApp/public/locales/zh/translation.json @@ -90,6 +90,11 @@ "boms": { "header": { "description": "你可以使用物料清单工具,按照项目来管理零部件。你可以直接按照PCB板的套数扣除对应零件数量,或者检查还需要购买哪些零件以及分析成本。

请选择或创建需要管理BOM的项目。
" + }, + "acceptedFileTypes": "Accepted file types: \"*.xls, *.xlsx, *.csv\"" + "import": { + "success": "BOM Imported!", + "failure": "Failed to import BOM!" } }, "partTypes": { @@ -468,6 +473,7 @@ }, "button": { "addBomProject": "创建BOM工程文件", + "importBomProject": "Import BOM Project", "addPart": "添加部件", "save": "保存", "download": "下载", diff --git a/Binner/Binner.Web/ClientApp/src/pages/Boms.js b/Binner/Binner.Web/ClientApp/src/pages/Boms.js index ce45514b..e86fbf27 100644 --- a/Binner/Binner.Web/ClientApp/src/pages/Boms.js +++ b/Binner/Binner.Web/ClientApp/src/pages/Boms.js @@ -38,7 +38,6 @@ export function Boms (props) { const [confirmPartDeleteContent, setConfirmProjectDeleteContent] = useState(null); const [confirmDeleteSelectedProject, setConfirmDeleteSelectedProject] = useState(null); const [acceptedFile, setAcceptedFile] = useState(null); - const [error, setError] = useState(null); const [colors] = useState(_.map(ProjectColors, function (c) { return { @@ -193,13 +192,13 @@ export function Boms (props) { .then((response) => { toast.dismiss(); if (response.status === 200) { - toast.success(t("importProjectSuccess", "BOM Imported!")); + toast.success(t("page.boms.import.success", "BOM Imported!")); setProject(defaultProject); setAddVisible(false); setImportVisible(false); loadProjects(page, pageSize, true); } else { - toast.error(t("importProjectFailed", `Failed to import BOM!`), { autoClose: 10000 }); + toast.error(t("page.boms.import.failure", `Failed to import BOM!`), { autoClose: 10000 }); } }); }); @@ -373,13 +372,8 @@ export function Boms (props) { > {t('page.exportData.uploadNote', "Drag a document to upload, or click to select files")} -
{t('page.projects.acceptedFileTypes', "Accepted file types: \"*.xls, *.xlsx, *.csv\"")}
+
{t('page.boms.acceptedFileTypes', "Accepted file types: \"*.xls, *.xlsx, *.csv\"")}
- {error && ( -
- {t('label.error', "Error")}: {error} -
- )}