Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion backend/internal/data/repo/repo_entities.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`

Expand All @@ -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"`
Expand Down Expand Up @@ -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"`
Expand Down Expand Up @@ -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"`

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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).
Expand Down Expand Up @@ -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).
Comment on lines 1437 to 1441
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid unconditional ImportRef overwrite on update.

SetImportRef(data.UPC) always executes. If UPC is absent/empty from a caller, this clears existing data unexpectedly.

✅ Proposed fix
 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).
   SetArchived(data.Archived).
@@
   SetQuantity(data.Quantity).
   SetAssetID(int64(data.AssetID)).
   SetSyncChildEntityLocations(data.SyncChildEntityLocations)
+
+if strings.TrimSpace(data.UPC) != "" {
+  q.SetImportRef(strings.TrimSpace(data.UPC))
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/internal/data/repo/repo_entities.go` around lines 1437 - 1441, The
update query always calls SetImportRef(data.UPC) which will clear the stored
import reference when data.UPC is empty; modify the update builder in the
repository (the r.db.Entity.Update() chain that uses entity.ID(...),
entity.HasGroupWith(...), SetName, SetDescription, SetSerialNumber) to only call
SetImportRef(data.UPC) when data.UPC is present (e.g. data.UPC != "" or non-nil
depending on type); implement this by branching before building the query (add
the SetImportRef call conditionally) or by using an optional/conditional builder
pattern so existing ImportRef is preserved when UPC is not supplied.

SetModelNumber(data.ModelNumber).
SetManufacturer(data.Manufacturer).
Expand Down Expand Up @@ -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).
Expand Down
29 changes: 28 additions & 1 deletion frontend/components/Item/BarcodeModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,21 @@
<DialogTitle>{{ $t("components.item.product_import.title") }}</DialogTitle>
</DialogHeader>

<div
v-if="existingItems.length > 0"
class="flex flex-col gap-2 rounded-md border border-amber-500/40 bg-amber-500/10 p-4 text-amber-700 dark:text-amber-300"
role="alert"
>
<span class="text-sm font-medium">
Found {{ existingItems.length }} existing item(s) with UPC {{ barcode }}.
</span>
Comment on lines +14 to +15
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Duplicate count may be underreported due fixed page size.

The alert says “Found N existing item(s)”, but getAll is capped at 10 results. If there are more matches, the message is inaccurate.

🛠️ Proposed fix
-      const existingResult = await api.items.getAll({ q: barcode.trim(), page: 1, pageSize: 10 });
+      const existingResult = await api.items.getAll({ q: barcode.trim(), page: 1, pageSize: 100 });

If you keep paging capped, adjust the text to indicate it is a partial list (e.g., “showing first N matches”).

Also applies to: 228-231

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/components/Item/BarcodeModal.vue` around lines 14 - 15, The UI text
in BarcodeModal.vue is misleading because existingItems (from getAll) is capped
to 10, so update the message near the span that uses existingItems.length and
barcode to indicate it's a partial result (e.g., "Showing first {{
existingItems.length }} matches for UPC {{ barcode }}") or programmatically
append " (showing first N results)" when getAll returns a limited page;
reference the existingItems variable and the getAll call that fetches items to
ensure the displayed copy reflects the page cap wherever similar messages appear
(also update the same pattern at the other occurrence around lines 228-231).

<div class="flex flex-wrap gap-2">
<Button v-for="existing in existingItems" :key="existing.id" size="sm" variant="outline" @click="navigateToItem(existing.id)">
Open {{ existing.name }}
</Button>
Comment on lines +8 to +19
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Localize the new duplicate-alert copy.

The strings Found ... existing item(s)... and Open ... are hardcoded and won’t be translated.

🌐 Proposed fix
-        <span class="text-sm font-medium">
-          Found {{ existingItems.length }} existing item(s) with UPC {{ barcode }}.
-        </span>
+        <span class="text-sm font-medium">
+          {{ $t("components.item.product_import.duplicates_found", { count: existingItems.length, barcode }) }}
+        </span>
         <div class="flex flex-wrap gap-2">
           <Button v-for="existing in existingItems" :key="existing.id" size="sm" variant="outline" `@click`="navigateToItem(existing.id)">
-            Open {{ existing.name }}
+            {{ $t("components.item.product_import.open_item", { name: existing.name }) }}
           </Button>
         </div>

As per coding guidelines: "**/*.{ts,vue}: Check for hardcoded strings in UI components that should be translatable."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div
v-if="existingItems.length > 0"
class="flex flex-col gap-2 rounded-md border border-amber-500/40 bg-amber-500/10 p-4 text-amber-700 dark:text-amber-300"
role="alert"
>
<span class="text-sm font-medium">
Found {{ existingItems.length }} existing item(s) with UPC {{ barcode }}.
</span>
<div class="flex flex-wrap gap-2">
<Button v-for="existing in existingItems" :key="existing.id" size="sm" variant="outline" @click="navigateToItem(existing.id)">
Open {{ existing.name }}
</Button>
<div
v-if="existingItems.length > 0"
class="flex flex-col gap-2 rounded-md border border-amber-500/40 bg-amber-500/10 p-4 text-amber-700 dark:text-amber-300"
role="alert"
>
<span class="text-sm font-medium">
{{ $t("components.item.product_import.duplicates_found", { count: existingItems.length, barcode }) }}
</span>
<div class="flex flex-wrap gap-2">
<Button v-for="existing in existingItems" :key="existing.id" size="sm" variant="outline" `@click`="navigateToItem(existing.id)">
{{ $t("components.item.product_import.open_item", { name: existing.name }) }}
</Button>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/components/Item/BarcodeModal.vue` around lines 8 - 19, The two
hardcoded UI strings in the template should be converted to translatable
messages: replace the "Found {{ existingItems.length }} existing item(s) with
UPC {{ barcode }}." span and the Button label "Open {{ existing.name }}" with
i18n lookup keys (e.g. barcodeModal.foundExistingItems and
barcodeModal.openItem) and pass the dynamic values (existingItems.length,
barcode, existing.name) as interpolation params to the translation function;
update the template to call your project's i18n helper (e.g. $t or useI18n t())
for the span and the Button children and add the corresponding keys to the
locale files. Ensure you update nodes referencing existingItems, barcode,
existing.name and navigateToItem remains unchanged.

</div>
</div>

<div
v-if="errorMessage"
class="flex items-center gap-2 rounded-md border border-destructive bg-destructive/10 p-4 text-destructive"
Expand Down Expand Up @@ -112,7 +127,7 @@
import { useI18n } from "vue-i18n";
import { DialogID } from "@/components/ui/dialog-provider/utils";
import { Button } from "~/components/ui/button";
import type { BarcodeProduct } from "~~/lib/api/types/data-contracts";
import type { BarcodeProduct, EntitySummary } from "~~/lib/api/types/data-contracts";
import { useDialog } from "~/components/ui/dialog-provider";
import MdiAlertCircleOutline from "~icons/mdi/alert-circle-outline";
import MdiBarcode from "~icons/mdi/barcode";
Expand All @@ -129,6 +144,7 @@
const searching = ref(false);
const barcode = ref<string>("");
const products = ref<BarcodeProduct[] | null>(null);
const existingItems = ref<EntitySummary[]>([]);
const selectedRow = ref(-1);
const errorMessage = ref<string | null>(null);

Expand Down Expand Up @@ -159,6 +175,7 @@
selectedRow.value = -1;
searching.value = false;
errorMessage.value = null;
existingItems.value = [];

if (params?.barcode) {
// Reset if the barcode is different
Expand Down Expand Up @@ -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");
Expand All @@ -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;
Expand All @@ -226,6 +249,10 @@
}
}

function navigateToItem(itemId: string) {
navigateTo(`/item/${itemId}`);
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
function extractValue(data: Record<string, any>, value: string) {
const parts = value.split(".");
Expand Down
4 changes: 4 additions & 0 deletions frontend/components/Item/CreateModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,7 @@
parentId: null,
name: "",
quantity: 1,
upc: "",
description: "",
color: "",
tags: [] as string[],
Expand Down Expand Up @@ -602,6 +603,7 @@
if (params?.product) {
form.name = params.product.item.name;
form.description = params.product.item.description;
form.upc = params.product.barcode;

Comment on lines 603 to 607
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

UPC is dropped when creation goes through the template endpoint.

form.upc is set from barcode import, but when templateData.value is present, create() uses api.templates.createItem(...) without UPC. This can silently lose scanned UPC values.

💡 Proposed fix (preserve UPC immediately)
-    if (templateData.value) {
+    if (templateData.value && !form.upc) {
       const templateRequest = {
         name: form.name,
         description: form.description,
         parentId: form.location.id as string,
         tagIds: form.tags,
         quantity: form.quantity,
       };

       const result = await api.templates.createItem(templateData.value.id, templateRequest);
       error = result.error;
       data = result.data;
     } else {
       // Normal item creation without template
       const out: EntityCreate = {
         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,
       };

Also applies to: 646-666

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/components/Item/CreateModal.vue` around lines 603 - 607, When a
product is imported via barcode the UPC is assigned to form.upc from
params.product.barcode, but the create() branch that calls
api.templates.createItem(...) when templateData.value is present never forwards
form.upc so scanned UPCs are lost; update the create() logic to include the UPC
(use form.upc) in the payload passed to api.templates.createItem and any other
template-related creation calls (also check the same for the alternative branch
used at lines ~646-666), ensuring templateData.value creation uses the same
fields as the non-template path so the UPC is preserved.

if (params.product.imageURL) {
form.photos.push({
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -705,6 +708,7 @@

form.name = "";
form.quantity = 1;
form.upc = "";
form.description = "";
form.color = "";
form.photos = [];
Expand Down
4 changes: 4 additions & 0 deletions frontend/lib/api/types/data-contracts.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions frontend/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
6 changes: 6 additions & 0 deletions frontend/pages/item/[id]/index/edit.vue
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,12 @@
ref: "description",
maxLength: 1000,
},
{
type: "text",
label: "items.upc",
ref: "upc",
maxLength: 64,
},
{
type: "text",
label: "items.serial_number",
Expand Down
Loading