diff --git a/.scaffold/go.sum b/.scaffold/go.sum index d6c9e8a7..40d03dd1 100644 --- a/.scaffold/go.sum +++ b/.scaffold/go.sum @@ -8,7 +8,7 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/sysadminsmedia/homebox/backend v0.0.0-20251228052820-4557df86eddb h1:nRu1qr3gceoIIDJolCRnd/Eo5VLAMoH9CYnyKCCVBuA= -github.com/sysadminsmedia/homebox/backend v0.0.0-20251228052820-4557df86eddb/go.mod h1:9zHHw5TNttw5Kn4Wks+SxwXmJPz6PgGNbnB4BtF1Z4c= +github.com/sysadminsmedia/homebox/backend v0.0.0-20251228163253-2bd6ff580a7f h1:+5m3FRu/Ja3kH2XgFn0GYCWOKIe4O+7PbLawLXvb4gA= +github.com/sysadminsmedia/homebox/backend v0.0.0-20251228163253-2bd6ff580a7f/go.mod h1:9zHHw5TNttw5Kn4Wks+SxwXmJPz6PgGNbnB4BtF1Z4c= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/backend/internal/data/repo/repo_items.go b/backend/internal/data/repo/repo_items.go index 49943dc1..3d2390fc 100644 --- a/backend/internal/data/repo/repo_items.go +++ b/backend/internal/data/repo/repo_items.go @@ -810,6 +810,22 @@ func (e *ItemsRepository) DeleteByGroup(ctx context.Context, gid, id uuid.UUID) } func (e *ItemsRepository) WipeInventory(ctx context.Context, gid uuid.UUID, wipeLabels bool, wipeLocations bool, wipeMaintenance bool) (int, error) { + deleted := 0 + + // Wipe maintenance records if requested + // IMPORTANT: Must delete maintenance records BEFORE items since they are linked to items + if wipeMaintenance { + maintenanceCount, err := e.db.MaintenanceEntry.Delete(). + Where(maintenanceentry.HasItemWith(item.HasGroupWith(group.ID(gid)))). + Exec(ctx) + if err != nil { + log.Err(err).Msg("failed to delete maintenance entries during wipe inventory") + } else { + log.Info().Int("count", maintenanceCount).Msg("deleted maintenance entries during wipe inventory") + deleted += maintenanceCount + } + } + // Get all items for the group items, err := e.db.Item.Query(). Where(item.HasGroupWith(group.ID(gid))). @@ -819,7 +835,6 @@ func (e *ItemsRepository) WipeInventory(ctx context.Context, gid uuid.UUID, wipe return 0, err } - deleted := 0 // Delete each item with its attachments // Note: We manually delete attachments and items instead of calling DeleteByGroup // to continue processing remaining items even if some deletions fail @@ -872,20 +887,6 @@ func (e *ItemsRepository) WipeInventory(ctx context.Context, gid uuid.UUID, wipe } } - // Wipe maintenance records if requested - if wipeMaintenance { - // Maintenance entries are linked to items, so we query by items in the group - maintenanceCount, err := e.db.MaintenanceEntry.Delete(). - Where(maintenanceentry.HasItemWith(item.HasGroupWith(group.ID(gid)))). - Exec(ctx) - if err != nil { - log.Err(err).Msg("failed to delete maintenance entries during wipe inventory") - } else { - log.Info().Int("count", maintenanceCount).Msg("deleted maintenance entries during wipe inventory") - deleted += maintenanceCount - } - } - e.publishMutationEvent(gid) return deleted, nil } diff --git a/backend/internal/data/repo/repo_items_test.go b/backend/internal/data/repo/repo_items_test.go index b2f29440..59f45fc2 100644 --- a/backend/internal/data/repo/repo_items_test.go +++ b/backend/internal/data/repo/repo_items_test.go @@ -398,4 +398,161 @@ func TestItemsRepository_DeleteByGroupWithAttachments(t *testing.T) { require.Error(t, err) } +func TestItemsRepository_WipeInventory(t *testing.T) { + // Create test data: items, labels, locations, and maintenance entries + + // Create locations + loc1, err := tRepos.Locations.Create(context.Background(), tGroup.ID, LocationCreate{ + Name: "Test Location 1", + Description: "Test location for wipe test", + }) + require.NoError(t, err) + + loc2, err := tRepos.Locations.Create(context.Background(), tGroup.ID, LocationCreate{ + Name: "Test Location 2", + Description: "Another test location", + }) + require.NoError(t, err) + + // Create labels + label1, err := tRepos.Labels.Create(context.Background(), tGroup.ID, LabelCreate{ + Name: "Test Label 1", + Description: "Test label for wipe test", + }) + require.NoError(t, err) + + label2, err := tRepos.Labels.Create(context.Background(), tGroup.ID, LabelCreate{ + Name: "Test Label 2", + Description: "Another test label", + }) + require.NoError(t, err) + + // Create items + item1, err := tRepos.Items.Create(context.Background(), tGroup.ID, ItemCreate{ + Name: "Test Item 1", + Description: "Test item for wipe test", + LocationID: loc1.ID, + LabelIDs: []uuid.UUID{label1.ID}, + }) + require.NoError(t, err) + + item2, err := tRepos.Items.Create(context.Background(), tGroup.ID, ItemCreate{ + Name: "Test Item 2", + Description: "Another test item", + LocationID: loc2.ID, + LabelIDs: []uuid.UUID{label2.ID}, + }) + require.NoError(t, err) + + // Create maintenance entries for items + _, err = tRepos.MaintEntry.Create(context.Background(), item1.ID, MaintenanceEntryCreate{ + CompletedDate: types.DateFromTime(time.Now()), + Name: "Test Maintenance 1", + Description: "Test maintenance entry", + Cost: 100.0, + }) + require.NoError(t, err) + + _, err = tRepos.MaintEntry.Create(context.Background(), item2.ID, MaintenanceEntryCreate{ + CompletedDate: types.DateFromTime(time.Now()), + Name: "Test Maintenance 2", + Description: "Another test maintenance entry", + Cost: 200.0, + }) + require.NoError(t, err) + + // Test 1: Wipe inventory with all options enabled + t.Run("wipe all including labels, locations, and maintenance", func(t *testing.T) { + deleted, err := tRepos.Items.WipeInventory(context.Background(), tGroup.ID, true, true, true) + require.NoError(t, err) + assert.Greater(t, deleted, 0, "Should have deleted at least some entities") + + // Verify items are deleted + _, err = tRepos.Items.GetOneByGroup(context.Background(), tGroup.ID, item1.ID) + require.Error(t, err, "Item 1 should be deleted") + + _, err = tRepos.Items.GetOneByGroup(context.Background(), tGroup.ID, item2.ID) + require.Error(t, err, "Item 2 should be deleted") + + // Verify maintenance entries are deleted (query by item ID, should return empty) + maint1List, err := tRepos.MaintEntry.GetMaintenanceByItemID(context.Background(), tGroup.ID, item1.ID, MaintenanceFilters{}) + require.NoError(t, err) + assert.Empty(t, maint1List, "Maintenance entry 1 should be deleted") + + maint2List, err := tRepos.MaintEntry.GetMaintenanceByItemID(context.Background(), tGroup.ID, item2.ID, MaintenanceFilters{}) + require.NoError(t, err) + assert.Empty(t, maint2List, "Maintenance entry 2 should be deleted") + + // Verify labels are deleted + _, err = tRepos.Labels.GetOneByGroup(context.Background(), tGroup.ID, label1.ID) + require.Error(t, err, "Label 1 should be deleted") + + _, err = tRepos.Labels.GetOneByGroup(context.Background(), tGroup.ID, label2.ID) + require.Error(t, err, "Label 2 should be deleted") + + // Verify locations are deleted + _, err = tRepos.Locations.Get(context.Background(), loc1.ID) + require.Error(t, err, "Location 1 should be deleted") + + _, err = tRepos.Locations.Get(context.Background(), loc2.ID) + require.Error(t, err, "Location 2 should be deleted") + }) +} + +func TestItemsRepository_WipeInventory_OnlyItems(t *testing.T) { + // Create test data + loc, err := tRepos.Locations.Create(context.Background(), tGroup.ID, LocationCreate{ + Name: "Test Location", + Description: "Test location for wipe test", + }) + require.NoError(t, err) + + label, err := tRepos.Labels.Create(context.Background(), tGroup.ID, LabelCreate{ + Name: "Test Label", + Description: "Test label for wipe test", + }) + require.NoError(t, err) + + item, err := tRepos.Items.Create(context.Background(), tGroup.ID, ItemCreate{ + Name: "Test Item", + Description: "Test item for wipe test", + LocationID: loc.ID, + LabelIDs: []uuid.UUID{label.ID}, + }) + require.NoError(t, err) + + _, err = tRepos.MaintEntry.Create(context.Background(), item.ID, MaintenanceEntryCreate{ + CompletedDate: types.DateFromTime(time.Now()), + Name: "Test Maintenance", + Description: "Test maintenance entry", + Cost: 100.0, + }) + require.NoError(t, err) + + // Test: Wipe inventory with only items (no labels, locations, or maintenance) + deleted, err := tRepos.Items.WipeInventory(context.Background(), tGroup.ID, false, false, false) + require.NoError(t, err) + assert.Greater(t, deleted, 0, "Should have deleted at least the item") + + // Verify item is deleted + _, err = tRepos.Items.GetOneByGroup(context.Background(), tGroup.ID, item.ID) + require.Error(t, err, "Item should be deleted") + + // Verify maintenance entry is deleted due to cascade + maintList, err := tRepos.MaintEntry.GetMaintenanceByItemID(context.Background(), tGroup.ID, item.ID, MaintenanceFilters{}) + require.NoError(t, err) + assert.Empty(t, maintList, "Maintenance entry should be cascade deleted with item") + + // Verify label still exists + _, err = tRepos.Labels.GetOneByGroup(context.Background(), tGroup.ID, label.ID) + require.NoError(t, err, "Label should still exist") + + // Verify location still exists + _, err = tRepos.Locations.Get(context.Background(), loc.ID) + require.NoError(t, err, "Location should still exist") + + // Cleanup + _ = tRepos.Labels.DeleteByGroup(context.Background(), tGroup.ID, label.ID) + _ = tRepos.Locations.delete(context.Background(), loc.ID) +} diff --git a/backend/internal/data/repo/repo_wipe_integration_test.go b/backend/internal/data/repo/repo_wipe_integration_test.go new file mode 100644 index 00000000..30a0fb95 --- /dev/null +++ b/backend/internal/data/repo/repo_wipe_integration_test.go @@ -0,0 +1,194 @@ +package repo + +import ( + "context" + "testing" + "time" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/sysadminsmedia/homebox/backend/internal/data/types" +) + +// TestWipeInventory_Integration tests the complete wipe inventory flow +func TestWipeInventory_Integration(t *testing.T) { + // Create test data: locations, labels, items with maintenance + + // 1. Create locations + loc1, err := tRepos.Locations.Create(context.Background(), tGroup.ID, LocationCreate{ + Name: "Test Garage", + Description: "Garage location", + }) + require.NoError(t, err) + + loc2, err := tRepos.Locations.Create(context.Background(), tGroup.ID, LocationCreate{ + Name: "Test Basement", + Description: "Basement location", + }) + require.NoError(t, err) + + // 2. Create labels + label1, err := tRepos.Labels.Create(context.Background(), tGroup.ID, LabelCreate{ + Name: "Test Electronics", + Description: "Electronics label", + }) + require.NoError(t, err) + + label2, err := tRepos.Labels.Create(context.Background(), tGroup.ID, LabelCreate{ + Name: "Test Tools", + Description: "Tools label", + }) + require.NoError(t, err) + + // 3. Create items + item1, err := tRepos.Items.Create(context.Background(), tGroup.ID, ItemCreate{ + Name: "Test Laptop", + Description: "Work laptop", + LocationID: loc1.ID, + LabelIDs: []uuid.UUID{label1.ID}, + }) + require.NoError(t, err) + + item2, err := tRepos.Items.Create(context.Background(), tGroup.ID, ItemCreate{ + Name: "Test Drill", + Description: "Power drill", + LocationID: loc2.ID, + LabelIDs: []uuid.UUID{label2.ID}, + }) + require.NoError(t, err) + + item3, err := tRepos.Items.Create(context.Background(), tGroup.ID, ItemCreate{ + Name: "Test Monitor", + Description: "Computer monitor", + LocationID: loc1.ID, + LabelIDs: []uuid.UUID{label1.ID}, + }) + require.NoError(t, err) + + // 4. Create maintenance entries + _, err = tRepos.MaintEntry.Create(context.Background(), item1.ID, MaintenanceEntryCreate{ + CompletedDate: types.DateFromTime(time.Now()), + Name: "Laptop cleaning", + Description: "Cleaned keyboard and screen", + Cost: 0, + }) + require.NoError(t, err) + + _, err = tRepos.MaintEntry.Create(context.Background(), item2.ID, MaintenanceEntryCreate{ + CompletedDate: types.DateFromTime(time.Now()), + Name: "Drill maintenance", + Description: "Oiled motor", + Cost: 5.00, + }) + require.NoError(t, err) + + _, err = tRepos.MaintEntry.Create(context.Background(), item3.ID, MaintenanceEntryCreate{ + CompletedDate: types.DateFromTime(time.Now()), + Name: "Monitor calibration", + Description: "Color calibration", + Cost: 0, + }) + require.NoError(t, err) + + // 5. Verify items exist + allItems, err := tRepos.Items.GetAll(context.Background(), tGroup.ID) + require.NoError(t, err) + assert.GreaterOrEqual(t, len(allItems), 3, "Should have at least 3 items") + + // 6. Verify maintenance entries exist + maint1List, err := tRepos.MaintEntry.GetMaintenanceByItemID(context.Background(), tGroup.ID, item1.ID, MaintenanceFilters{}) + require.NoError(t, err) + assert.NotEmpty(t, maint1List, "Item 1 should have maintenance records") + + maint2List, err := tRepos.MaintEntry.GetMaintenanceByItemID(context.Background(), tGroup.ID, item2.ID, MaintenanceFilters{}) + require.NoError(t, err) + assert.NotEmpty(t, maint2List, "Item 2 should have maintenance records") + + // 7. Test wipe inventory with all options enabled + deleted, err := tRepos.Items.WipeInventory(context.Background(), tGroup.ID, true, true, true) + require.NoError(t, err) + assert.Greater(t, deleted, 0, "Should have deleted entities") + + // 8. Verify all items are deleted + allItemsAfter, err := tRepos.Items.GetAll(context.Background(), tGroup.ID) + require.NoError(t, err) + assert.Equal(t, 0, len(allItemsAfter), "All items should be deleted") + + // 9. Verify maintenance entries are deleted + maint1After, err := tRepos.MaintEntry.GetMaintenanceByItemID(context.Background(), tGroup.ID, item1.ID, MaintenanceFilters{}) + require.NoError(t, err) + assert.Empty(t, maint1After, "Item 1 maintenance records should be deleted") + + // 10. Verify labels are deleted + _, err = tRepos.Labels.GetOneByGroup(context.Background(), tGroup.ID, label1.ID) + require.Error(t, err, "Label 1 should be deleted") + + _, err = tRepos.Labels.GetOneByGroup(context.Background(), tGroup.ID, label2.ID) + require.Error(t, err, "Label 2 should be deleted") + + // 11. Verify locations are deleted + _, err = tRepos.Locations.Get(context.Background(), loc1.ID) + require.Error(t, err, "Location 1 should be deleted") + + _, err = tRepos.Locations.Get(context.Background(), loc2.ID) + require.Error(t, err, "Location 2 should be deleted") +} + +// TestWipeInventory_SelectiveWipe tests wiping only certain entity types +func TestWipeInventory_SelectiveWipe(t *testing.T) { + // Create test data + loc, err := tRepos.Locations.Create(context.Background(), tGroup.ID, LocationCreate{ + Name: "Test Office", + Description: "Office location", + }) + require.NoError(t, err) + + label, err := tRepos.Labels.Create(context.Background(), tGroup.ID, LabelCreate{ + Name: "Test Important", + Description: "Important label", + }) + require.NoError(t, err) + + item, err := tRepos.Items.Create(context.Background(), tGroup.ID, ItemCreate{ + Name: "Test Computer", + Description: "Desktop computer", + LocationID: loc.ID, + LabelIDs: []uuid.UUID{label.ID}, + }) + require.NoError(t, err) + + _, err = tRepos.MaintEntry.Create(context.Background(), item.ID, MaintenanceEntryCreate{ + CompletedDate: types.DateFromTime(time.Now()), + Name: "System update", + Description: "OS update", + Cost: 0, + }) + require.NoError(t, err) + + // Test: Wipe only items (keep labels and locations) + deleted, err := tRepos.Items.WipeInventory(context.Background(), tGroup.ID, false, false, false) + require.NoError(t, err) + assert.Greater(t, deleted, 0, "Should have deleted at least items") + + // Verify item is deleted + _, err = tRepos.Items.GetOneByGroup(context.Background(), tGroup.ID, item.ID) + require.Error(t, err, "Item should be deleted") + + // Verify maintenance is cascade deleted + maintList, err := tRepos.MaintEntry.GetMaintenanceByItemID(context.Background(), tGroup.ID, item.ID, MaintenanceFilters{}) + require.NoError(t, err) + assert.Empty(t, maintList, "Maintenance should be cascade deleted") + + // Verify label still exists + _, err = tRepos.Labels.GetOneByGroup(context.Background(), tGroup.ID, label.ID) + require.NoError(t, err, "Label should still exist") + + // Verify location still exists + _, err = tRepos.Locations.Get(context.Background(), loc.ID) + require.NoError(t, err, "Location should still exist") + + // Cleanup + _ = tRepos.Labels.DeleteByGroup(context.Background(), tGroup.ID, label.ID) + _ = tRepos.Locations.delete(context.Background(), loc.ID) +} diff --git a/frontend/components/WipeInventoryDialog.vue b/frontend/components/WipeInventoryDialog.vue index 29f323b3..64bf33c2 100644 --- a/frontend/components/WipeInventoryDialog.vue +++ b/frontend/components/WipeInventoryDialog.vue @@ -83,24 +83,12 @@ const wipeLocations = ref(false); const wipeMaintenance = ref(false); - let onCloseCallback: - | ((result?: { wipeLabels: boolean; wipeLocations: boolean; wipeMaintenance: boolean } | undefined) => void) - | undefined; - - registerOpenDialogCallback( - DialogID.WipeInventory, - (params?: { - onClose?: ( - result?: { wipeLabels: boolean; wipeLocations: boolean; wipeMaintenance: boolean } | undefined - ) => void; - }) => { - dialog.value = true; - wipeLabels.value = false; - wipeLocations.value = false; - wipeMaintenance.value = false; - onCloseCallback = params?.onClose; - } - ); + registerOpenDialogCallback(DialogID.WipeInventory, () => { + dialog.value = true; + wipeLabels.value = false; + wipeLocations.value = false; + wipeMaintenance.value = false; + }); watch( dialog, @@ -123,7 +111,6 @@ function close() { dialog.value = false; closeDialog(DialogID.WipeInventory, undefined); - onCloseCallback?.(undefined); } function confirm() { @@ -134,6 +121,5 @@ wipeMaintenance: wipeMaintenance.value, }; closeDialog(DialogID.WipeInventory, result); - onCloseCallback?.(result); }