diff --git a/backend/internal/data/repo/repo_entities.go b/backend/internal/data/repo/repo_entities.go index b9f88973c..404bf448e 100644 --- a/backend/internal/data/repo/repo_entities.go +++ b/backend/internal/data/repo/repo_entities.go @@ -93,6 +93,7 @@ type ( Name string `json:"name" validate:"required,min=1,max=255"` Quantity float64 `json:"quantity"` Description string `json:"description" validate:"max=1000"` + UPC string `json:"upc" validate:"max=64"` AssetID AssetID `json:"-"` EntityTypeID uuid.UUID `json:"entityTypeId"` @@ -116,6 +117,7 @@ type ( TagIDs []uuid.UUID `json:"tagIds"` // Identifications + UPC string `json:"upc" validate:"max=64"` SerialNumber string `json:"serialNumber"` ModelNumber string `json:"modelNumber"` Manufacturer string `json:"manufacturer"` @@ -156,6 +158,7 @@ type ( AssetID AssetID `json:"assetId,string"` Name string `json:"name"` Description string `json:"description"` + UPC string `json:"upc"` Quantity float64 `json:"quantity"` Insured bool `json:"insured"` Archived bool `json:"archived"` @@ -187,6 +190,7 @@ type ( SyncChildEntityLocations bool `json:"syncChildEntityLocations"` SerialNumber string `json:"serialNumber"` + UPC string `json:"upc"` ModelNumber string `json:"modelNumber"` Manufacturer string `json:"manufacturer"` @@ -258,6 +262,7 @@ func mapEntitySummary(e *ent.Entity) EntitySummary { AssetID: AssetID(e.AssetID), Name: e.Name, Description: e.Description, + UPC: e.ImportRef, ImportRef: e.ImportRef, Quantity: e.Quantity, CreatedAt: e.CreatedAt, @@ -336,6 +341,7 @@ func mapEntityOut(e *ent.Entity) EntityOut { SyncChildEntityLocations: e.SyncChildEntityLocations, // Identification + UPC: e.ImportRef, SerialNumber: e.SerialNumber, ModelNumber: e.ModelNumber, Manufacturer: e.Manufacturer, @@ -587,6 +593,7 @@ func (r *EntityRepository) QueryByGroup(ctx context.Context, gid uuid.UUID, q En entity.Or( entity.NameContainsFold(q.Search), entity.DescriptionContainsFold(q.Search), + entity.ImportRefContainsFold(q.Search), entity.SerialNumberContainsFold(q.Search), entity.ModelNumberContainsFold(q.Search), entity.ManufacturerContainsFold(q.Search), @@ -993,8 +1000,13 @@ func (r *EntityRepository) Create(ctx context.Context, gid uuid.UUID, data Entit return EntityOut{}, err } + importRef := data.ImportRef + if data.UPC != "" { + importRef = data.UPC + } + q := r.db.Entity.Create(). - SetImportRef(data.ImportRef). + SetImportRef(importRef). SetName(data.Name). SetQuantity(data.Quantity). SetDescription(data.Description). @@ -1425,6 +1437,7 @@ func (r *EntityRepository) UpdateByGroup(ctx context.Context, gid uuid.UUID, dat q := r.db.Entity.Update().Where(entity.ID(data.ID), entity.HasGroupWith(group.ID(gid))). SetName(data.Name). SetDescription(data.Description). + SetImportRef(data.UPC). SetSerialNumber(data.SerialNumber). SetModelNumber(data.ModelNumber). SetManufacturer(data.Manufacturer). @@ -2113,6 +2126,7 @@ func (r *EntityRepository) Duplicate(ctx context.Context, gid, id uuid.UUID, opt SetQuantity(originalEntity.Quantity). SetGroupID(gid). SetAssetID(int64(nextAssetID)). + SetImportRef(originalEntity.ImportRef). SetSerialNumber(originalEntity.SerialNumber). SetModelNumber(originalEntity.ModelNumber). SetManufacturer(originalEntity.Manufacturer). diff --git a/frontend/components/Item/BarcodeModal.vue b/frontend/components/Item/BarcodeModal.vue index 86bc3cfeb..151c7e289 100644 --- a/frontend/components/Item/BarcodeModal.vue +++ b/frontend/components/Item/BarcodeModal.vue @@ -5,6 +5,21 @@ {{ $t("components.item.product_import.title") }} + +
(""); const products = ref(null); + const existingItems = ref([]); const selectedRow = ref(-1); const errorMessage = ref(null); @@ -159,6 +175,7 @@ selectedRow.value = -1; searching.value = false; errorMessage.value = null; + existingItems.value = []; if (params?.barcode) { // Reset if the barcode is different @@ -196,6 +213,7 @@ async function retrieveProductInfo(barcode: string) { errorMessage.value = null; + existingItems.value = []; if (!barcode || barcode.trim().length === 0 || !/^[0-9]+$/.test(barcode)) { errorMessage.value = t("components.item.product_import.error_invalid_barcode"); @@ -207,6 +225,11 @@ searching.value = true; try { + const existingResult = await api.items.getAll({ q: barcode.trim(), page: 1, pageSize: 10 }); + if (!existingResult.error && existingResult.data?.items) { + existingItems.value = existingResult.data.items.filter(item => item.upc === barcode.trim()); + } + const result = await api.products.searchFromBarcode(barcode.trim()); if (result.error) { errorMessage.value = t("errors.api_failure") + result.error; @@ -226,6 +249,10 @@ } } + function navigateToItem(itemId: string) { + navigateTo(`/item/${itemId}`); + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any function extractValue(data: Record, value: string) { const parts = value.split("."); diff --git a/frontend/components/Item/CreateModal.vue b/frontend/components/Item/CreateModal.vue index 8779b8784..e2a94749a 100644 --- a/frontend/components/Item/CreateModal.vue +++ b/frontend/components/Item/CreateModal.vue @@ -419,6 +419,7 @@ parentId: null, name: "", quantity: 1, + upc: "", description: "", color: "", tags: [] as string[], @@ -602,6 +603,7 @@ if (params?.product) { form.name = params.product.item.name; form.description = params.product.item.description; + form.upc = params.product.barcode; if (params.product.imageURL) { form.photos.push({ @@ -660,6 +662,7 @@ parentId: form.parentId || form.location.id as string, name: form.name, quantity: form.quantity, + upc: form.upc, description: form.description, tagIds: form.tags, entityTypeId: selectedEntityType.value?.id, @@ -705,6 +708,7 @@ form.name = ""; form.quantity = 1; + form.upc = ""; form.description = ""; form.color = ""; form.photos = []; diff --git a/frontend/lib/api/types/data-contracts.ts b/frontend/lib/api/types/data-contracts.ts index 1a869f988..386f8facd 100644 --- a/frontend/lib/api/types/data-contracts.ts +++ b/frontend/lib/api/types/data-contracts.ts @@ -594,6 +594,7 @@ export interface EntityCreate { name: string; parentId?: string | null; quantity: number; + upc: string; /** Edges */ tagIds: string[]; } @@ -651,6 +652,7 @@ export interface EntityOut { soldNotes: string; soldPrice: number; soldTo: string; + upc: string; syncChildEntityLocations: boolean; tags: TagSummary[]; thumbnailId?: string | null; @@ -691,6 +693,7 @@ export interface EntitySummary { parent?: EntitySummary | null; purchasePrice: number; quantity: number; + upc: string; /** Sale details */ soldDate: Date | string; tags: TagSummary[]; @@ -858,6 +861,7 @@ export interface EntityUpdate { purchaseFrom: string; purchasePrice?: number | null; quantity: number; + upc: string; /** Identifications */ serialNumber: string; /** Sold */ diff --git a/frontend/locales/en.json b/frontend/locales/en.json index c0d18c6b8..1b5681cbf 100644 --- a/frontend/locales/en.json +++ b/frontend/locales/en.json @@ -569,6 +569,7 @@ "select_field": "Select a field", "select_value": "Select a value", "serial_number": "Serial Number", + "upc": "UPC", "show_advanced_view_options": "Show Advanced View Options", "show_empty": "Show Empty", "sold_at": "Sold At", diff --git a/frontend/pages/item/[id]/index/edit.vue b/frontend/pages/item/[id]/index/edit.vue index 123fa0aab..9aa9e82fb 100644 --- a/frontend/pages/item/[id]/index/edit.vue +++ b/frontend/pages/item/[id]/index/edit.vue @@ -188,6 +188,12 @@ ref: "description", maxLength: 1000, }, + { + type: "text", + label: "items.upc", + ref: "upc", + maxLength: 64, + }, { type: "text", label: "items.serial_number",