From 50f0f7dd1572684f9a2000a59e44e81e500e94cc Mon Sep 17 00:00:00 2001 From: Hayden <64056131+hay-kot@users.noreply.github.com> Date: Sat, 12 Nov 2022 16:19:27 -0900 Subject: [PATCH 001/360] add schema --- backend/internal/data/ent/schema/item.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/backend/internal/data/ent/schema/item.go b/backend/internal/data/ent/schema/item.go index f7799f4..3945ebb 100644 --- a/backend/internal/data/ent/schema/item.go +++ b/backend/internal/data/ent/schema/item.go @@ -29,6 +29,7 @@ func (Item) Indexes() []ent.Index { index.Fields("model_number"), index.Fields("serial_number"), index.Fields("archived"), + index.Fields("group_id", "asset_id"), } } @@ -48,6 +49,8 @@ func (Item) Fields() []ent.Field { Default(false), field.Bool("archived"). Default(false), + field.Int("asset_id"). + Default(0), // ------------------------------------ // item identification From ab406baf3334f17e5f9ef2a2b6ed629b84c768b8 Mon Sep 17 00:00:00 2001 From: Hayden <64056131+hay-kot@users.noreply.github.com> Date: Sat, 12 Nov 2022 16:24:08 -0900 Subject: [PATCH 002/360] run db migration --- backend/internal/data/ent/item.go | 13 ++- backend/internal/data/ent/item/item.go | 5 ++ backend/internal/data/ent/item/where.go | 71 +++++++++++++++ backend/internal/data/ent/item_create.go | 29 ++++++ backend/internal/data/ent/item_update.go | 70 +++++++++++++++ backend/internal/data/ent/migrate/schema.go | 18 ++-- backend/internal/data/ent/mutation.go | 89 ++++++++++++++++++- backend/internal/data/ent/runtime.go | 20 +++-- backend/internal/data/ent/schema/item.go | 2 +- .../20221113012312_add_asset_id_field.sql | 24 +++++ .../data/migrations/migrations/atlas.sum | 3 +- 11 files changed, 326 insertions(+), 18 deletions(-) create mode 100644 backend/internal/data/migrations/migrations/20221113012312_add_asset_id_field.sql diff --git a/backend/internal/data/ent/item.go b/backend/internal/data/ent/item.go index 802acff..a780945 100644 --- a/backend/internal/data/ent/item.go +++ b/backend/internal/data/ent/item.go @@ -37,6 +37,8 @@ type Item struct { Insured bool `json:"insured,omitempty"` // Archived holds the value of the "archived" field. Archived bool `json:"archived,omitempty"` + // AssetID holds the value of the "asset_id" field. + AssetID int `json:"asset_id,omitempty"` // SerialNumber holds the value of the "serial_number" field. SerialNumber string `json:"serial_number,omitempty"` // ModelNumber holds the value of the "model_number" field. @@ -176,7 +178,7 @@ func (*Item) scanValues(columns []string) ([]any, error) { values[i] = new(sql.NullBool) case item.FieldPurchasePrice, item.FieldSoldPrice: values[i] = new(sql.NullFloat64) - case item.FieldQuantity: + case item.FieldQuantity, item.FieldAssetID: values[i] = new(sql.NullInt64) case item.FieldName, item.FieldDescription, item.FieldImportRef, item.FieldNotes, item.FieldSerialNumber, item.FieldModelNumber, item.FieldManufacturer, item.FieldWarrantyDetails, item.FieldPurchaseFrom, item.FieldSoldTo, item.FieldSoldNotes: values[i] = new(sql.NullString) @@ -265,6 +267,12 @@ func (i *Item) assignValues(columns []string, values []any) error { } else if value.Valid { i.Archived = value.Bool } + case item.FieldAssetID: + if value, ok := values[j].(*sql.NullInt64); !ok { + return fmt.Errorf("unexpected type %T for field asset_id", values[j]) + } else if value.Valid { + i.AssetID = int(value.Int64) + } case item.FieldSerialNumber: if value, ok := values[j].(*sql.NullString); !ok { return fmt.Errorf("unexpected type %T for field serial_number", values[j]) @@ -454,6 +462,9 @@ func (i *Item) String() string { builder.WriteString("archived=") builder.WriteString(fmt.Sprintf("%v", i.Archived)) builder.WriteString(", ") + builder.WriteString("asset_id=") + builder.WriteString(fmt.Sprintf("%v", i.AssetID)) + builder.WriteString(", ") builder.WriteString("serial_number=") builder.WriteString(i.SerialNumber) builder.WriteString(", ") diff --git a/backend/internal/data/ent/item/item.go b/backend/internal/data/ent/item/item.go index c2991da..ab3b43f 100644 --- a/backend/internal/data/ent/item/item.go +++ b/backend/internal/data/ent/item/item.go @@ -31,6 +31,8 @@ const ( FieldInsured = "insured" // FieldArchived holds the string denoting the archived field in the database. FieldArchived = "archived" + // FieldAssetID holds the string denoting the asset_id field in the database. + FieldAssetID = "asset_id" // FieldSerialNumber holds the string denoting the serial_number field in the database. FieldSerialNumber = "serial_number" // FieldModelNumber holds the string denoting the model_number field in the database. @@ -128,6 +130,7 @@ var Columns = []string{ FieldQuantity, FieldInsured, FieldArchived, + FieldAssetID, FieldSerialNumber, FieldModelNumber, FieldManufacturer, @@ -193,6 +196,8 @@ var ( DefaultInsured bool // DefaultArchived holds the default value on creation for the "archived" field. DefaultArchived bool + // DefaultAssetID holds the default value on creation for the "asset_id" field. + DefaultAssetID int // SerialNumberValidator is a validator for the "serial_number" field. It is called by the builders before save. SerialNumberValidator func(string) error // ModelNumberValidator is a validator for the "model_number" field. It is called by the builders before save. diff --git a/backend/internal/data/ent/item/where.go b/backend/internal/data/ent/item/where.go index 2897e35..2174432 100644 --- a/backend/internal/data/ent/item/where.go +++ b/backend/internal/data/ent/item/where.go @@ -145,6 +145,13 @@ func Archived(v bool) predicate.Item { }) } +// AssetID applies equality check predicate on the "asset_id" field. It's identical to AssetIDEQ. +func AssetID(v int) predicate.Item { + return predicate.Item(func(s *sql.Selector) { + s.Where(sql.EQ(s.C(FieldAssetID), v)) + }) +} + // SerialNumber applies equality check predicate on the "serial_number" field. It's identical to SerialNumberEQ. func SerialNumber(v string) predicate.Item { return predicate.Item(func(s *sql.Selector) { @@ -894,6 +901,70 @@ func ArchivedNEQ(v bool) predicate.Item { }) } +// AssetIDEQ applies the EQ predicate on the "asset_id" field. +func AssetIDEQ(v int) predicate.Item { + return predicate.Item(func(s *sql.Selector) { + s.Where(sql.EQ(s.C(FieldAssetID), v)) + }) +} + +// AssetIDNEQ applies the NEQ predicate on the "asset_id" field. +func AssetIDNEQ(v int) predicate.Item { + return predicate.Item(func(s *sql.Selector) { + s.Where(sql.NEQ(s.C(FieldAssetID), v)) + }) +} + +// AssetIDIn applies the In predicate on the "asset_id" field. +func AssetIDIn(vs ...int) predicate.Item { + v := make([]any, len(vs)) + for i := range v { + v[i] = vs[i] + } + return predicate.Item(func(s *sql.Selector) { + s.Where(sql.In(s.C(FieldAssetID), v...)) + }) +} + +// AssetIDNotIn applies the NotIn predicate on the "asset_id" field. +func AssetIDNotIn(vs ...int) predicate.Item { + v := make([]any, len(vs)) + for i := range v { + v[i] = vs[i] + } + return predicate.Item(func(s *sql.Selector) { + s.Where(sql.NotIn(s.C(FieldAssetID), v...)) + }) +} + +// AssetIDGT applies the GT predicate on the "asset_id" field. +func AssetIDGT(v int) predicate.Item { + return predicate.Item(func(s *sql.Selector) { + s.Where(sql.GT(s.C(FieldAssetID), v)) + }) +} + +// AssetIDGTE applies the GTE predicate on the "asset_id" field. +func AssetIDGTE(v int) predicate.Item { + return predicate.Item(func(s *sql.Selector) { + s.Where(sql.GTE(s.C(FieldAssetID), v)) + }) +} + +// AssetIDLT applies the LT predicate on the "asset_id" field. +func AssetIDLT(v int) predicate.Item { + return predicate.Item(func(s *sql.Selector) { + s.Where(sql.LT(s.C(FieldAssetID), v)) + }) +} + +// AssetIDLTE applies the LTE predicate on the "asset_id" field. +func AssetIDLTE(v int) predicate.Item { + return predicate.Item(func(s *sql.Selector) { + s.Where(sql.LTE(s.C(FieldAssetID), v)) + }) +} + // SerialNumberEQ applies the EQ predicate on the "serial_number" field. func SerialNumberEQ(v string) predicate.Item { return predicate.Item(func(s *sql.Selector) { diff --git a/backend/internal/data/ent/item_create.go b/backend/internal/data/ent/item_create.go index f4de18e..36c9720 100644 --- a/backend/internal/data/ent/item_create.go +++ b/backend/internal/data/ent/item_create.go @@ -144,6 +144,20 @@ func (ic *ItemCreate) SetNillableArchived(b *bool) *ItemCreate { return ic } +// SetAssetID sets the "asset_id" field. +func (ic *ItemCreate) SetAssetID(i int) *ItemCreate { + ic.mutation.SetAssetID(i) + return ic +} + +// SetNillableAssetID sets the "asset_id" field if the given value is not nil. +func (ic *ItemCreate) SetNillableAssetID(i *int) *ItemCreate { + if i != nil { + ic.SetAssetID(*i) + } + return ic +} + // SetSerialNumber sets the "serial_number" field. func (ic *ItemCreate) SetSerialNumber(s string) *ItemCreate { ic.mutation.SetSerialNumber(s) @@ -546,6 +560,10 @@ func (ic *ItemCreate) defaults() { v := item.DefaultArchived ic.mutation.SetArchived(v) } + if _, ok := ic.mutation.AssetID(); !ok { + v := item.DefaultAssetID + ic.mutation.SetAssetID(v) + } if _, ok := ic.mutation.LifetimeWarranty(); !ok { v := item.DefaultLifetimeWarranty ic.mutation.SetLifetimeWarranty(v) @@ -604,6 +622,9 @@ func (ic *ItemCreate) check() error { if _, ok := ic.mutation.Archived(); !ok { return &ValidationError{Name: "archived", err: errors.New(`ent: missing required field "Item.archived"`)} } + if _, ok := ic.mutation.AssetID(); !ok { + return &ValidationError{Name: "asset_id", err: errors.New(`ent: missing required field "Item.asset_id"`)} + } if v, ok := ic.mutation.SerialNumber(); ok { if err := item.SerialNumberValidator(v); err != nil { return &ValidationError{Name: "serial_number", err: fmt.Errorf(`ent: validator failed for field "Item.serial_number": %w`, err)} @@ -749,6 +770,14 @@ func (ic *ItemCreate) createSpec() (*Item, *sqlgraph.CreateSpec) { }) _node.Archived = value } + if value, ok := ic.mutation.AssetID(); ok { + _spec.Fields = append(_spec.Fields, &sqlgraph.FieldSpec{ + Type: field.TypeInt, + Value: value, + Column: item.FieldAssetID, + }) + _node.AssetID = value + } if value, ok := ic.mutation.SerialNumber(); ok { _spec.Fields = append(_spec.Fields, &sqlgraph.FieldSpec{ Type: field.TypeString, diff --git a/backend/internal/data/ent/item_update.go b/backend/internal/data/ent/item_update.go index 4307051..52970f5 100644 --- a/backend/internal/data/ent/item_update.go +++ b/backend/internal/data/ent/item_update.go @@ -135,6 +135,27 @@ func (iu *ItemUpdate) SetNillableArchived(b *bool) *ItemUpdate { return iu } +// SetAssetID sets the "asset_id" field. +func (iu *ItemUpdate) SetAssetID(i int) *ItemUpdate { + iu.mutation.ResetAssetID() + iu.mutation.SetAssetID(i) + return iu +} + +// SetNillableAssetID sets the "asset_id" field if the given value is not nil. +func (iu *ItemUpdate) SetNillableAssetID(i *int) *ItemUpdate { + if i != nil { + iu.SetAssetID(*i) + } + return iu +} + +// AddAssetID adds i to the "asset_id" field. +func (iu *ItemUpdate) AddAssetID(i int) *ItemUpdate { + iu.mutation.AddAssetID(i) + return iu +} + // SetSerialNumber sets the "serial_number" field. func (iu *ItemUpdate) SetSerialNumber(s string) *ItemUpdate { iu.mutation.SetSerialNumber(s) @@ -816,6 +837,20 @@ func (iu *ItemUpdate) sqlSave(ctx context.Context) (n int, err error) { Column: item.FieldArchived, }) } + if value, ok := iu.mutation.AssetID(); ok { + _spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{ + Type: field.TypeInt, + Value: value, + Column: item.FieldAssetID, + }) + } + if value, ok := iu.mutation.AddedAssetID(); ok { + _spec.Fields.Add = append(_spec.Fields.Add, &sqlgraph.FieldSpec{ + Type: field.TypeInt, + Value: value, + Column: item.FieldAssetID, + }) + } if value, ok := iu.mutation.SerialNumber(); ok { _spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{ Type: field.TypeString, @@ -1422,6 +1457,27 @@ func (iuo *ItemUpdateOne) SetNillableArchived(b *bool) *ItemUpdateOne { return iuo } +// SetAssetID sets the "asset_id" field. +func (iuo *ItemUpdateOne) SetAssetID(i int) *ItemUpdateOne { + iuo.mutation.ResetAssetID() + iuo.mutation.SetAssetID(i) + return iuo +} + +// SetNillableAssetID sets the "asset_id" field if the given value is not nil. +func (iuo *ItemUpdateOne) SetNillableAssetID(i *int) *ItemUpdateOne { + if i != nil { + iuo.SetAssetID(*i) + } + return iuo +} + +// AddAssetID adds i to the "asset_id" field. +func (iuo *ItemUpdateOne) AddAssetID(i int) *ItemUpdateOne { + iuo.mutation.AddAssetID(i) + return iuo +} + // SetSerialNumber sets the "serial_number" field. func (iuo *ItemUpdateOne) SetSerialNumber(s string) *ItemUpdateOne { iuo.mutation.SetSerialNumber(s) @@ -2133,6 +2189,20 @@ func (iuo *ItemUpdateOne) sqlSave(ctx context.Context) (_node *Item, err error) Column: item.FieldArchived, }) } + if value, ok := iuo.mutation.AssetID(); ok { + _spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{ + Type: field.TypeInt, + Value: value, + Column: item.FieldAssetID, + }) + } + if value, ok := iuo.mutation.AddedAssetID(); ok { + _spec.Fields.Add = append(_spec.Fields.Add, &sqlgraph.FieldSpec{ + Type: field.TypeInt, + Value: value, + Column: item.FieldAssetID, + }) + } if value, ok := iuo.mutation.SerialNumber(); ok { _spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{ Type: field.TypeString, diff --git a/backend/internal/data/ent/migrate/schema.go b/backend/internal/data/ent/migrate/schema.go index 6fd2d9e..464c3c6 100644 --- a/backend/internal/data/ent/migrate/schema.go +++ b/backend/internal/data/ent/migrate/schema.go @@ -171,6 +171,7 @@ var ( {Name: "quantity", Type: field.TypeInt, Default: 1}, {Name: "insured", Type: field.TypeBool, Default: false}, {Name: "archived", Type: field.TypeBool, Default: false}, + {Name: "asset_id", Type: field.TypeInt, Default: 0}, {Name: "serial_number", Type: field.TypeString, Nullable: true, Size: 255}, {Name: "model_number", Type: field.TypeString, Nullable: true, Size: 255}, {Name: "manufacturer", Type: field.TypeString, Nullable: true, Size: 255}, @@ -196,19 +197,19 @@ var ( ForeignKeys: []*schema.ForeignKey{ { Symbol: "items_groups_items", - Columns: []*schema.Column{ItemsColumns[23]}, + Columns: []*schema.Column{ItemsColumns[24]}, RefColumns: []*schema.Column{GroupsColumns[0]}, OnDelete: schema.Cascade, }, { Symbol: "items_items_children", - Columns: []*schema.Column{ItemsColumns[24]}, + Columns: []*schema.Column{ItemsColumns[25]}, RefColumns: []*schema.Column{ItemsColumns[0]}, OnDelete: schema.SetNull, }, { Symbol: "items_locations_items", - Columns: []*schema.Column{ItemsColumns[25]}, + Columns: []*schema.Column{ItemsColumns[26]}, RefColumns: []*schema.Column{LocationsColumns[0]}, OnDelete: schema.Cascade, }, @@ -222,23 +223,28 @@ var ( { Name: "item_manufacturer", Unique: false, - Columns: []*schema.Column{ItemsColumns[12]}, + Columns: []*schema.Column{ItemsColumns[13]}, }, { Name: "item_model_number", Unique: false, - Columns: []*schema.Column{ItemsColumns[11]}, + Columns: []*schema.Column{ItemsColumns[12]}, }, { Name: "item_serial_number", Unique: false, - Columns: []*schema.Column{ItemsColumns[10]}, + Columns: []*schema.Column{ItemsColumns[11]}, }, { Name: "item_archived", Unique: false, Columns: []*schema.Column{ItemsColumns[9]}, }, + { + Name: "item_asset_id", + Unique: false, + Columns: []*schema.Column{ItemsColumns[10]}, + }, }, } // ItemFieldsColumns holds the columns for the "item_fields" table. diff --git a/backend/internal/data/ent/mutation.go b/backend/internal/data/ent/mutation.go index 73ccbde..2f1ebdc 100644 --- a/backend/internal/data/ent/mutation.go +++ b/backend/internal/data/ent/mutation.go @@ -4134,6 +4134,8 @@ type ItemMutation struct { addquantity *int insured *bool archived *bool + asset_id *int + addasset_id *int serial_number *string model_number *string manufacturer *string @@ -4660,6 +4662,62 @@ func (m *ItemMutation) ResetArchived() { m.archived = nil } +// SetAssetID sets the "asset_id" field. +func (m *ItemMutation) SetAssetID(i int) { + m.asset_id = &i + m.addasset_id = nil +} + +// AssetID returns the value of the "asset_id" field in the mutation. +func (m *ItemMutation) AssetID() (r int, exists bool) { + v := m.asset_id + if v == nil { + return + } + return *v, true +} + +// OldAssetID returns the old "asset_id" field's value of the Item entity. +// If the Item object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *ItemMutation) OldAssetID(ctx context.Context) (v int, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldAssetID is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldAssetID requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldAssetID: %w", err) + } + return oldValue.AssetID, nil +} + +// AddAssetID adds i to the "asset_id" field. +func (m *ItemMutation) AddAssetID(i int) { + if m.addasset_id != nil { + *m.addasset_id += i + } else { + m.addasset_id = &i + } +} + +// AddedAssetID returns the value that was added to the "asset_id" field in this mutation. +func (m *ItemMutation) AddedAssetID() (r int, exists bool) { + v := m.addasset_id + if v == nil { + return + } + return *v, true +} + +// ResetAssetID resets all changes to the "asset_id" field. +func (m *ItemMutation) ResetAssetID() { + m.asset_id = nil + m.addasset_id = nil +} + // SetSerialNumber sets the "serial_number" field. func (m *ItemMutation) SetSerialNumber(s string) { m.serial_number = &s @@ -5650,7 +5708,7 @@ func (m *ItemMutation) Type() string { // order to get all numeric fields that were incremented/decremented, call // AddedFields(). func (m *ItemMutation) Fields() []string { - fields := make([]string, 0, 22) + fields := make([]string, 0, 23) if m.created_at != nil { fields = append(fields, item.FieldCreatedAt) } @@ -5678,6 +5736,9 @@ func (m *ItemMutation) Fields() []string { if m.archived != nil { fields = append(fields, item.FieldArchived) } + if m.asset_id != nil { + fields = append(fields, item.FieldAssetID) + } if m.serial_number != nil { fields = append(fields, item.FieldSerialNumber) } @@ -5743,6 +5804,8 @@ func (m *ItemMutation) Field(name string) (ent.Value, bool) { return m.Insured() case item.FieldArchived: return m.Archived() + case item.FieldAssetID: + return m.AssetID() case item.FieldSerialNumber: return m.SerialNumber() case item.FieldModelNumber: @@ -5796,6 +5859,8 @@ func (m *ItemMutation) OldField(ctx context.Context, name string) (ent.Value, er return m.OldInsured(ctx) case item.FieldArchived: return m.OldArchived(ctx) + case item.FieldAssetID: + return m.OldAssetID(ctx) case item.FieldSerialNumber: return m.OldSerialNumber(ctx) case item.FieldModelNumber: @@ -5894,6 +5959,13 @@ func (m *ItemMutation) SetField(name string, value ent.Value) error { } m.SetArchived(v) return nil + case item.FieldAssetID: + v, ok := value.(int) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetAssetID(v) + return nil case item.FieldSerialNumber: v, ok := value.(string) if !ok { @@ -5996,6 +6068,9 @@ func (m *ItemMutation) AddedFields() []string { if m.addquantity != nil { fields = append(fields, item.FieldQuantity) } + if m.addasset_id != nil { + fields = append(fields, item.FieldAssetID) + } if m.addpurchase_price != nil { fields = append(fields, item.FieldPurchasePrice) } @@ -6012,6 +6087,8 @@ func (m *ItemMutation) AddedField(name string) (ent.Value, bool) { switch name { case item.FieldQuantity: return m.AddedQuantity() + case item.FieldAssetID: + return m.AddedAssetID() case item.FieldPurchasePrice: return m.AddedPurchasePrice() case item.FieldSoldPrice: @@ -6032,6 +6109,13 @@ func (m *ItemMutation) AddField(name string, value ent.Value) error { } m.AddQuantity(v) return nil + case item.FieldAssetID: + v, ok := value.(int) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.AddAssetID(v) + return nil case item.FieldPurchasePrice: v, ok := value.(float64) if !ok { @@ -6181,6 +6265,9 @@ func (m *ItemMutation) ResetField(name string) error { case item.FieldArchived: m.ResetArchived() return nil + case item.FieldAssetID: + m.ResetAssetID() + return nil case item.FieldSerialNumber: m.ResetSerialNumber() return nil diff --git a/backend/internal/data/ent/runtime.go b/backend/internal/data/ent/runtime.go index af5dc22..25a7680 100644 --- a/backend/internal/data/ent/runtime.go +++ b/backend/internal/data/ent/runtime.go @@ -275,36 +275,40 @@ func init() { itemDescArchived := itemFields[4].Descriptor() // item.DefaultArchived holds the default value on creation for the archived field. item.DefaultArchived = itemDescArchived.Default.(bool) + // itemDescAssetID is the schema descriptor for asset_id field. + itemDescAssetID := itemFields[5].Descriptor() + // item.DefaultAssetID holds the default value on creation for the asset_id field. + item.DefaultAssetID = itemDescAssetID.Default.(int) // itemDescSerialNumber is the schema descriptor for serial_number field. - itemDescSerialNumber := itemFields[5].Descriptor() + itemDescSerialNumber := itemFields[6].Descriptor() // item.SerialNumberValidator is a validator for the "serial_number" field. It is called by the builders before save. item.SerialNumberValidator = itemDescSerialNumber.Validators[0].(func(string) error) // itemDescModelNumber is the schema descriptor for model_number field. - itemDescModelNumber := itemFields[6].Descriptor() + itemDescModelNumber := itemFields[7].Descriptor() // item.ModelNumberValidator is a validator for the "model_number" field. It is called by the builders before save. item.ModelNumberValidator = itemDescModelNumber.Validators[0].(func(string) error) // itemDescManufacturer is the schema descriptor for manufacturer field. - itemDescManufacturer := itemFields[7].Descriptor() + itemDescManufacturer := itemFields[8].Descriptor() // item.ManufacturerValidator is a validator for the "manufacturer" field. It is called by the builders before save. item.ManufacturerValidator = itemDescManufacturer.Validators[0].(func(string) error) // itemDescLifetimeWarranty is the schema descriptor for lifetime_warranty field. - itemDescLifetimeWarranty := itemFields[8].Descriptor() + itemDescLifetimeWarranty := itemFields[9].Descriptor() // item.DefaultLifetimeWarranty holds the default value on creation for the lifetime_warranty field. item.DefaultLifetimeWarranty = itemDescLifetimeWarranty.Default.(bool) // itemDescWarrantyDetails is the schema descriptor for warranty_details field. - itemDescWarrantyDetails := itemFields[10].Descriptor() + itemDescWarrantyDetails := itemFields[11].Descriptor() // item.WarrantyDetailsValidator is a validator for the "warranty_details" field. It is called by the builders before save. item.WarrantyDetailsValidator = itemDescWarrantyDetails.Validators[0].(func(string) error) // itemDescPurchasePrice is the schema descriptor for purchase_price field. - itemDescPurchasePrice := itemFields[13].Descriptor() + itemDescPurchasePrice := itemFields[14].Descriptor() // item.DefaultPurchasePrice holds the default value on creation for the purchase_price field. item.DefaultPurchasePrice = itemDescPurchasePrice.Default.(float64) // itemDescSoldPrice is the schema descriptor for sold_price field. - itemDescSoldPrice := itemFields[16].Descriptor() + itemDescSoldPrice := itemFields[17].Descriptor() // item.DefaultSoldPrice holds the default value on creation for the sold_price field. item.DefaultSoldPrice = itemDescSoldPrice.Default.(float64) // itemDescSoldNotes is the schema descriptor for sold_notes field. - itemDescSoldNotes := itemFields[17].Descriptor() + itemDescSoldNotes := itemFields[18].Descriptor() // item.SoldNotesValidator is a validator for the "sold_notes" field. It is called by the builders before save. item.SoldNotesValidator = itemDescSoldNotes.Validators[0].(func(string) error) // itemDescID is the schema descriptor for id field. diff --git a/backend/internal/data/ent/schema/item.go b/backend/internal/data/ent/schema/item.go index 3945ebb..388566d 100644 --- a/backend/internal/data/ent/schema/item.go +++ b/backend/internal/data/ent/schema/item.go @@ -29,7 +29,7 @@ func (Item) Indexes() []ent.Index { index.Fields("model_number"), index.Fields("serial_number"), index.Fields("archived"), - index.Fields("group_id", "asset_id"), + index.Fields("asset_id"), } } diff --git a/backend/internal/data/migrations/migrations/20221113012312_add_asset_id_field.sql b/backend/internal/data/migrations/migrations/20221113012312_add_asset_id_field.sql new file mode 100644 index 0000000..5bcf3ad --- /dev/null +++ b/backend/internal/data/migrations/migrations/20221113012312_add_asset_id_field.sql @@ -0,0 +1,24 @@ +-- disable the enforcement of foreign-keys constraints +PRAGMA foreign_keys = off; +-- create "new_items" table +CREATE TABLE `new_items` (`id` uuid NOT NULL, `created_at` datetime NOT NULL, `updated_at` datetime NOT NULL, `name` text NOT NULL, `description` text NULL, `import_ref` text NULL, `notes` text NULL, `quantity` integer NOT NULL DEFAULT 1, `insured` bool NOT NULL DEFAULT false, `archived` bool NOT NULL DEFAULT false, `asset_id` integer NOT NULL DEFAULT 0, `serial_number` text NULL, `model_number` text NULL, `manufacturer` text NULL, `lifetime_warranty` bool NOT NULL DEFAULT false, `warranty_expires` datetime NULL, `warranty_details` text NULL, `purchase_time` datetime NULL, `purchase_from` text NULL, `purchase_price` real NOT NULL DEFAULT 0, `sold_time` datetime NULL, `sold_to` text NULL, `sold_price` real NOT NULL DEFAULT 0, `sold_notes` text NULL, `group_items` uuid NOT NULL, `item_children` uuid NULL, `location_items` uuid NULL, PRIMARY KEY (`id`), CONSTRAINT `items_groups_items` FOREIGN KEY (`group_items`) REFERENCES `groups` (`id`) ON DELETE CASCADE, CONSTRAINT `items_items_children` FOREIGN KEY (`item_children`) REFERENCES `items` (`id`) ON DELETE SET NULL, CONSTRAINT `items_locations_items` FOREIGN KEY (`location_items`) REFERENCES `locations` (`id`) ON DELETE CASCADE); +-- copy rows from old table "items" to new temporary table "new_items" +INSERT INTO `new_items` (`id`, `created_at`, `updated_at`, `name`, `description`, `import_ref`, `notes`, `quantity`, `insured`, `archived`, `serial_number`, `model_number`, `manufacturer`, `lifetime_warranty`, `warranty_expires`, `warranty_details`, `purchase_time`, `purchase_from`, `purchase_price`, `sold_time`, `sold_to`, `sold_price`, `sold_notes`, `group_items`, `item_children`, `location_items`) SELECT `id`, `created_at`, `updated_at`, `name`, `description`, `import_ref`, `notes`, `quantity`, `insured`, `archived`, `serial_number`, `model_number`, `manufacturer`, `lifetime_warranty`, `warranty_expires`, `warranty_details`, `purchase_time`, `purchase_from`, `purchase_price`, `sold_time`, `sold_to`, `sold_price`, `sold_notes`, `group_items`, `item_children`, `location_items` FROM `items`; +-- drop "items" table after copying rows +DROP TABLE `items`; +-- rename temporary table "new_items" to "items" +ALTER TABLE `new_items` RENAME TO `items`; +-- create index "item_name" to table: "items" +CREATE INDEX `item_name` ON `items` (`name`); +-- create index "item_manufacturer" to table: "items" +CREATE INDEX `item_manufacturer` ON `items` (`manufacturer`); +-- create index "item_model_number" to table: "items" +CREATE INDEX `item_model_number` ON `items` (`model_number`); +-- create index "item_serial_number" to table: "items" +CREATE INDEX `item_serial_number` ON `items` (`serial_number`); +-- create index "item_archived" to table: "items" +CREATE INDEX `item_archived` ON `items` (`archived`); +-- create index "item_asset_id" to table: "items" +CREATE INDEX `item_asset_id` ON `items` (`asset_id`); +-- enable back the enforcement of foreign-keys constraints +PRAGMA foreign_keys = on; diff --git a/backend/internal/data/migrations/migrations/atlas.sum b/backend/internal/data/migrations/migrations/atlas.sum index 2916627..d9c72fb 100644 --- a/backend/internal/data/migrations/migrations/atlas.sum +++ b/backend/internal/data/migrations/migrations/atlas.sum @@ -1,6 +1,7 @@ -h1:i76VRMDIPdcmQtXTe9bzrgITAzLGjjVy9y8XaXIchAs= +h1:z1tbZ3fYByqxL78Z+ov8mfQVjXcwsZeEcT0i+2DZ8a8= 20220929052825_init.sql h1:ZlCqm1wzjDmofeAcSX3jE4h4VcdTNGpRg2eabztDy9Q= 20221001210956_group_invitations.sql h1:YQKJFtE39wFOcRNbZQ/d+ZlHwrcfcsZlcv/pLEYdpjw= 20221009173029_add_user_roles.sql h1:vWmzAfgEWQeGk0Vn70zfVPCcfEZth3E0JcvyKTjpYyU= 20221020043305_allow_nesting_types.sql h1:4AyJpZ7l7SSJtJAQETYY802FHJ64ufYPJTqvwdiGn3M= 20221101041931_add_archived_field.sql h1:L2WxiOh1svRn817cNURgqnEQg6DIcodZ1twK4tvxW94= +20221113012312_add_asset_id_field.sql h1:DjD7e1PS8OfxGBWic8h0nO/X6CNnHEMqQjDCaaQ3M3Q= From 567e12a1e94e9a9bc85bf46eae50c6bcb78c532e Mon Sep 17 00:00:00 2001 From: Hayden <64056131+hay-kot@users.noreply.github.com> Date: Sat, 12 Nov 2022 18:57:51 -0900 Subject: [PATCH 003/360] bulk seed asset IDs --- .../app/api/handlers/v1/v1_ctrl_actions.go | 35 ++++++++++ backend/app/api/routes.go | 2 + backend/app/api/static/docs/docs.go | 39 +++++++++++ backend/app/api/static/docs/swagger.json | 39 +++++++++++ backend/app/api/static/docs/swagger.yaml | 24 +++++++ .../internal/core/services/service_items.go | 26 +++++++ backend/internal/data/repo/repo_items.go | 69 ++++++++++++++++++- backend/internal/data/repo/repo_items_test.go | 28 ++++++++ frontend/lib/api/classes/actions.ts | 10 +++ frontend/lib/api/types/data-contracts.ts | 8 +++ frontend/lib/api/user.ts | 3 + frontend/pages/item/[id]/index.vue | 4 ++ frontend/pages/profile.vue | 45 ++++++++++++ 13 files changed, 331 insertions(+), 1 deletion(-) create mode 100644 backend/app/api/handlers/v1/v1_ctrl_actions.go create mode 100644 frontend/lib/api/classes/actions.ts diff --git a/backend/app/api/handlers/v1/v1_ctrl_actions.go b/backend/app/api/handlers/v1/v1_ctrl_actions.go new file mode 100644 index 0000000..37e2b72 --- /dev/null +++ b/backend/app/api/handlers/v1/v1_ctrl_actions.go @@ -0,0 +1,35 @@ +package v1 + +import ( + "net/http" + + "github.com/hay-kot/homebox/backend/internal/core/services" + "github.com/hay-kot/homebox/backend/internal/sys/validate" + "github.com/hay-kot/homebox/backend/pkgs/server" + "github.com/rs/zerolog/log" +) + +type EnsureAssetIDResult struct { + Completed int `json:"completed"` +} + +// HandleGroupInvitationsCreate godoc +// @Summary Get the current user +// @Tags Group +// @Produce json +// @Success 200 {object} EnsureAssetIDResult +// @Router /v1/actions/ensure-asset-ids [Post] +// @Security Bearer +func (ctrl *V1Controller) HandleEnsureAssetID() server.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) error { + ctx := services.NewContext(r.Context()) + + totalCompleted, err := ctrl.svc.Items.EnsureAssetID(ctx, ctx.GID) + if err != nil { + log.Err(err).Msg("failed to ensure asset id") + return validate.NewRequestError(err, http.StatusInternalServerError) + } + + return server.Respond(w, http.StatusOK, EnsureAssetIDResult{Completed: totalCompleted}) + } +} diff --git a/backend/app/api/routes.go b/backend/app/api/routes.go index 992e70e..beec706 100644 --- a/backend/app/api/routes.go +++ b/backend/app/api/routes.go @@ -82,6 +82,8 @@ func (a *app) mountRoutes(repos *repo.AllRepos) { a.server.Get(v1Base("/groups"), v1Ctrl.HandleGroupGet(), a.mwAuthToken) a.server.Put(v1Base("/groups"), v1Ctrl.HandleGroupUpdate(), a.mwAuthToken) + a.server.Post(v1Base("/actions/ensure-asset-ids"), v1Ctrl.HandleEnsureAssetID(), a.mwAuthToken) + a.server.Get(v1Base("/locations"), v1Ctrl.HandleLocationGetAll(), a.mwAuthToken) a.server.Post(v1Base("/locations"), v1Ctrl.HandleLocationCreate(), a.mwAuthToken) a.server.Get(v1Base("/locations/{id}"), v1Ctrl.HandleLocationGet(), a.mwAuthToken) diff --git a/backend/app/api/static/docs/docs.go b/backend/app/api/static/docs/docs.go index f2c877e..6e02411 100644 --- a/backend/app/api/static/docs/docs.go +++ b/backend/app/api/static/docs/docs.go @@ -21,6 +21,30 @@ const docTemplate = `{ "host": "{{.Host}}", "basePath": "{{.BasePath}}", "paths": { + "/v1/actions/ensure-asset-ids": { + "post": { + "security": [ + { + "Bearer": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Group" + ], + "summary": "Get the current user", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/v1.EnsureAssetIDResult" + } + } + } + } + }, "/v1/groups": { "get": { "security": [ @@ -1326,6 +1350,10 @@ const docTemplate = `{ "archived": { "type": "boolean" }, + "assetId": { + "type": "string", + "example": "0" + }, "attachments": { "type": "array", "items": { @@ -1479,6 +1507,9 @@ const docTemplate = `{ "archived": { "type": "boolean" }, + "assetId": { + "type": "string" + }, "description": { "type": "string" }, @@ -1891,6 +1922,14 @@ const docTemplate = `{ } } }, + "v1.EnsureAssetIDResult": { + "type": "object", + "properties": { + "completed": { + "type": "integer" + } + } + }, "v1.GroupInvitation": { "type": "object", "properties": { diff --git a/backend/app/api/static/docs/swagger.json b/backend/app/api/static/docs/swagger.json index 7215fae..3be73bd 100644 --- a/backend/app/api/static/docs/swagger.json +++ b/backend/app/api/static/docs/swagger.json @@ -13,6 +13,30 @@ }, "basePath": "/api", "paths": { + "/v1/actions/ensure-asset-ids": { + "post": { + "security": [ + { + "Bearer": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Group" + ], + "summary": "Get the current user", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/v1.EnsureAssetIDResult" + } + } + } + } + }, "/v1/groups": { "get": { "security": [ @@ -1318,6 +1342,10 @@ "archived": { "type": "boolean" }, + "assetId": { + "type": "string", + "example": "0" + }, "attachments": { "type": "array", "items": { @@ -1471,6 +1499,9 @@ "archived": { "type": "boolean" }, + "assetId": { + "type": "string" + }, "description": { "type": "string" }, @@ -1883,6 +1914,14 @@ } } }, + "v1.EnsureAssetIDResult": { + "type": "object", + "properties": { + "completed": { + "type": "integer" + } + } + }, "v1.GroupInvitation": { "type": "object", "properties": { diff --git a/backend/app/api/static/docs/swagger.yaml b/backend/app/api/static/docs/swagger.yaml index e479583..72563b9 100644 --- a/backend/app/api/static/docs/swagger.yaml +++ b/backend/app/api/static/docs/swagger.yaml @@ -98,6 +98,9 @@ definitions: properties: archived: type: boolean + assetId: + example: "0" + type: string attachments: items: $ref: '#/definitions/repo.ItemAttachment' @@ -204,6 +207,8 @@ definitions: properties: archived: type: boolean + assetId: + type: string description: type: string fields: @@ -477,6 +482,11 @@ definitions: new: type: string type: object + v1.EnsureAssetIDResult: + properties: + completed: + type: integer + type: object v1.GroupInvitation: properties: expiresAt: @@ -516,6 +526,20 @@ info: title: Go API Templates version: "1.0" paths: + /v1/actions/ensure-asset-ids: + post: + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/v1.EnsureAssetIDResult' + security: + - Bearer: [] + summary: Get the current user + tags: + - Group /v1/groups: get: produces: diff --git a/backend/internal/core/services/service_items.go b/backend/internal/core/services/service_items.go index 4af80c8..0541577 100644 --- a/backend/internal/core/services/service_items.go +++ b/backend/internal/core/services/service_items.go @@ -23,6 +23,32 @@ type ItemService struct { at attachmentTokens } +func (svc *ItemService) EnsureAssetID(ctx context.Context, GID uuid.UUID) (int, error) { + items, err := svc.repo.Items.GetAllZeroAssetID(ctx, GID) + + if err != nil { + return 0, err + } + + highest, err := svc.repo.Items.GetHighestAssetID(ctx, GID) + if err != nil { + return 0, err + } + + finished := 0 + for _, item := range items { + highest++ + + err = svc.repo.Items.SetAssetID(ctx, GID, item.ID, repo.AssetID(highest)) + if err != nil { + return 0, err + } + + finished++ + } + + return finished, nil +} func (svc *ItemService) CsvImport(ctx context.Context, GID uuid.UUID, data [][]string) (int, error) { loaded := []csvRow{} diff --git a/backend/internal/data/repo/repo_items.go b/backend/internal/data/repo/repo_items.go index 71619fa..e983abf 100644 --- a/backend/internal/data/repo/repo_items.go +++ b/backend/internal/data/repo/repo_items.go @@ -2,6 +2,9 @@ package repo import ( "context" + "fmt" + "strconv" + "strings" "time" "github.com/google/uuid" @@ -18,6 +21,30 @@ type ItemsRepository struct { db *ent.Client } +type AssetID int + +func (aid AssetID) MarshalJSON() ([]byte, error) { + str := fmt.Sprintf("%d", aid) + + for len(str) < 6 { + str = "0" + str + } + + return []byte(fmt.Sprintf(`"%s"`, str)), nil +} + +func (aid *AssetID) UnmarshalJSON(data []byte) error { + str := string(strings.Replace(string(data), `"`, "", -1)) + aidInt, err := strconv.Atoi(str) + if err != nil { + return err + } + + *aid = AssetID(aidInt) + return nil + +} + type ( ItemQuery struct { Page int @@ -52,6 +79,7 @@ type ( ItemUpdate struct { ParentID uuid.UUID `json:"parentId" extensions:"x-nullable,x-omitempty"` ID uuid.UUID `json:"id"` + AssetID AssetID `json:"assetId"` Name string `json:"name"` Description string `json:"description"` Quantity int `json:"quantity"` @@ -107,6 +135,7 @@ type ( ItemOut struct { Parent *ItemSummary `json:"parent,omitempty" extensions:"x-nullable,x-omitempty"` ItemSummary + AssetID AssetID `json:"assetId,string"` SerialNumber string `json:"serialNumber"` ModelNumber string `json:"modelNumber"` @@ -215,6 +244,7 @@ func mapItemOut(item *ent.Item) ItemOut { return ItemOut{ Parent: parent, + AssetID: AssetID(item.AssetID), ItemSummary: mapItemSummary(item), LifetimeWarranty: item.LifetimeWarranty, WarrantyExpires: item.WarrantyExpires, @@ -359,6 +389,42 @@ func (e *ItemsRepository) GetAll(ctx context.Context, gid uuid.UUID) ([]ItemSumm All(ctx)) } +func (e *ItemsRepository) GetAllZeroAssetID(ctx context.Context, GID uuid.UUID) ([]ItemSummary, error) { + q := e.db.Item.Query().Where( + item.HasGroupWith(group.ID(GID)), + item.AssetID(0), + ).Order( + ent.Asc(item.FieldCreatedAt), + ) + + return mapItemsSummaryErr(q.All(ctx)) +} + +func (e *ItemsRepository) GetHighestAssetID(ctx context.Context, GID uuid.UUID) (AssetID, error) { + q := e.db.Item.Query().Where( + item.HasGroupWith(group.ID(GID)), + ).Order( + ent.Desc(item.FieldAssetID), + ).Limit(1) + + result, err := q.First(ctx) + if err != nil { + return 0, err + } + + return AssetID(result.AssetID), nil +} + +func (e *ItemsRepository) SetAssetID(ctx context.Context, GID uuid.UUID, ID uuid.UUID, assetID AssetID) error { + q := e.db.Item.Update().Where( + item.HasGroupWith(group.ID(GID)), + item.ID(ID), + ) + + _, err := q.SetAssetID(int(assetID)).Save(ctx) + return err +} + func (e *ItemsRepository) Create(ctx context.Context, gid uuid.UUID, data ItemCreate) (ItemOut, error) { q := e.db.Item.Create(). SetImportRef(data.ImportRef). @@ -414,7 +480,8 @@ func (e *ItemsRepository) UpdateByGroup(ctx context.Context, gid uuid.UUID, data SetInsured(data.Insured). SetWarrantyExpires(data.WarrantyExpires). SetWarrantyDetails(data.WarrantyDetails). - SetQuantity(data.Quantity) + SetQuantity(data.Quantity). + SetAssetID(int(data.AssetID)) currentLabels, err := e.db.Item.Query().Where(item.ID(data.ID)).QueryLabel().All(ctx) if err != nil { diff --git a/backend/internal/data/repo/repo_items_test.go b/backend/internal/data/repo/repo_items_test.go index 4b958b0..bc9c91d 100644 --- a/backend/internal/data/repo/repo_items_test.go +++ b/backend/internal/data/repo/repo_items_test.go @@ -2,6 +2,7 @@ package repo import ( "context" + "encoding/json" "testing" "time" @@ -9,6 +10,33 @@ import ( "github.com/stretchr/testify/assert" ) +func TestAssetID_UnmarshalJSON(t *testing.T) { + rawjson := `{"aid":"000123"}` + + st := struct { + AID AssetID `json:"aid"` + }{ + AID: AssetID(0), + } + + err := json.Unmarshal([]byte(rawjson), &st) + assert.NoError(t, err) + assert.Equal(t, AssetID(123), st.AID) +} + +func TestAssetID_MarshalJSON(t *testing.T) { + st := struct { + AID AssetID `json:"aid"` + }{ + AID: AssetID(123), + } + + b, err := json.Marshal(st) + + assert.NoError(t, err) + assert.JSONEq(t, `{"aid":"000123"}`, string(b)) +} + func itemFactory() ItemCreate { return ItemCreate{ Name: fk.Str(10), diff --git a/frontend/lib/api/classes/actions.ts b/frontend/lib/api/classes/actions.ts new file mode 100644 index 0000000..be892b3 --- /dev/null +++ b/frontend/lib/api/classes/actions.ts @@ -0,0 +1,10 @@ +import { BaseAPI, route } from "../base"; +import { EnsureAssetIDResult } from "../types/data-contracts"; + +export class ActionsAPI extends BaseAPI { + ensureAssetIDs() { + return this.http.post({ + url: route("/actions/ensure-asset-ids"), + }); + } +} diff --git a/frontend/lib/api/types/data-contracts.ts b/frontend/lib/api/types/data-contracts.ts index 7fd055c..9829f14 100644 --- a/frontend/lib/api/types/data-contracts.ts +++ b/frontend/lib/api/types/data-contracts.ts @@ -71,6 +71,9 @@ export interface ItemField { export interface ItemOut { archived: boolean; + + /** @example 0 */ + assetId: string; attachments: ItemAttachment[]; children: ItemSummary[]; createdAt: Date; @@ -131,6 +134,7 @@ export interface ItemSummary { export interface ItemUpdate { archived: boolean; + assetId: string; description: string; fields: ItemField[]; id: string; @@ -300,6 +304,10 @@ export interface ChangePassword { new: string; } +export interface EnsureAssetIDResult { + completed: number; +} + export interface GroupInvitation { expiresAt: Date; token: string; diff --git a/frontend/lib/api/user.ts b/frontend/lib/api/user.ts index d714bef..5c4940f 100644 --- a/frontend/lib/api/user.ts +++ b/frontend/lib/api/user.ts @@ -4,6 +4,7 @@ import { LabelsApi } from "./classes/labels"; import { LocationsApi } from "./classes/locations"; import { GroupApi } from "./classes/group"; import { UserApi } from "./classes/users"; +import { ActionsAPI } from "./classes/actions"; import { Requests } from "~~/lib/requests"; export class UserClient extends BaseAPI { @@ -12,6 +13,7 @@ export class UserClient extends BaseAPI { items: ItemsApi; group: GroupApi; user: UserApi; + actions: ActionsAPI; constructor(requests: Requests) { super(requests); @@ -21,6 +23,7 @@ export class UserClient extends BaseAPI { this.items = new ItemsApi(requests); this.group = new GroupApi(requests); this.user = new UserApi(requests); + this.actions = new ActionsAPI(requests); Object.freeze(this); } diff --git a/frontend/pages/item/[id]/index.vue b/frontend/pages/item/[id]/index.vue index 03ddfc5..c92508e 100644 --- a/frontend/pages/item/[id]/index.vue +++ b/frontend/pages/item/[id]/index.vue @@ -100,6 +100,10 @@ name: "Notes", text: item.value?.notes, }, + { + name: "Asset ID", + text: item.value?.assetId, + }, ...item.value.fields.map(field => { /** * Support Special URL Syntax diff --git a/frontend/pages/profile.vue b/frontend/pages/profile.vue index 5abb0f0..f2f32fe 100644 --- a/frontend/pages/profile.vue +++ b/frontend/pages/profile.vue @@ -163,6 +163,25 @@ passwordChange.current = ""; passwordChange.loading = false; } + + async function ensureAssetIDs() { + const { isCanceled } = await confirm.open( + "Are you sure you want to ensure all assets have an ID? This will take a while and cannot be undone." + ); + + if (isCanceled) { + return; + } + + const result = await api.actions.ensureAssetIDs(); + + if (result.error) { + notify.error("Failed to ensure asset IDs."); + return; + } + + notify.success(`${result.data.completed} assets have been updated.`); + } diff --git a/frontend/lib/api/__test__/user/items.test.ts b/frontend/lib/api/__test__/user/items.test.ts index 7837e50..1a5f94e 100644 --- a/frontend/lib/api/__test__/user/items.test.ts +++ b/frontend/lib/api/__test__/user/items.test.ts @@ -33,6 +33,7 @@ describe("user should be able to create an item and add an attachment", () => { const [location, cleanup] = await useLocation(api); const { response, data: item } = await api.items.create({ + parentId: null, name: "test-item", labelIds: [], description: "test-description", @@ -43,7 +44,7 @@ describe("user should be able to create an item and add an attachment", () => { // Add attachment { const testFile = new Blob(["test"], { type: "text/plain" }); - const { response } = await api.items.addAttachment(item.id, testFile, "test.txt", AttachmentTypes.Attachment); + const { response } = await api.items.attachments.add(item.id, testFile, "test.txt", AttachmentTypes.Attachment); expect(response.status).toBe(201); } @@ -54,7 +55,7 @@ describe("user should be able to create an item and add an attachment", () => { expect(data.attachments).toHaveLength(1); expect(data.attachments[0].document.title).toBe("test.txt"); - const resp = await api.items.deleteAttachment(data.id, data.attachments[0].id); + const resp = await api.items.attachments.delete(data.id, data.attachments[0].id); expect(resp.response.status).toBe(204); api.items.delete(item.id); @@ -66,6 +67,7 @@ describe("user should be able to create an item and add an attachment", () => { const [location, cleanup] = await useLocation(api); const { response, data: item } = await api.items.create({ + parentId: null, name: faker.vehicle.model(), labelIds: [], description: faker.lorem.paragraph(1), @@ -82,6 +84,7 @@ describe("user should be able to create an item and add an attachment", () => { // Add fields const itemUpdate = { + parentId: null, ...item, locationId: item.location.id, labelIds: item.labels.map(l => l.id), @@ -113,4 +116,41 @@ describe("user should be able to create an item and add an attachment", () => { cleanup(); }); + + test("users should be able to create and few maintenance logs for an item", async () => { + const api = await sharedUserClient(); + const [location, cleanup] = await useLocation(api); + const { response, data: item } = await api.items.create({ + parentId: null, + name: faker.vehicle.model(), + labelIds: [], + description: faker.lorem.paragraph(1), + locationId: location.id, + }); + expect(response.status).toBe(201); + + const maintenanceEntries = []; + for (let i = 0; i < 5; i++) { + const { response, data } = await api.items.maintenance.create(item.id, { + name: faker.vehicle.model(), + description: faker.lorem.paragraph(1), + date: faker.date.past(1), + cost: faker.datatype.number(100).toString(), + }); + + expect(response.status).toBe(201); + maintenanceEntries.push(data); + } + + // Log + { + const { response, data } = await api.items.maintenance.getLog(item.id); + expect(response.status).toBe(200); + expect(data.entries).toHaveLength(maintenanceEntries.length); + expect(data.costAverage).toBeGreaterThan(0); + expect(data.costTotal).toBeGreaterThan(0); + } + + cleanup(); + }); }); diff --git a/frontend/lib/api/classes/items.ts b/frontend/lib/api/classes/items.ts index f4fb38d..8522852 100644 --- a/frontend/lib/api/classes/items.ts +++ b/frontend/lib/api/classes/items.ts @@ -1,7 +1,18 @@ import { BaseAPI, route } from "../base"; import { parseDate } from "../base/base-api"; -import { ItemAttachmentUpdate, ItemCreate, ItemOut, ItemSummary, ItemUpdate } from "../types/data-contracts"; +import { + ItemAttachmentUpdate, + ItemCreate, + ItemOut, + ItemSummary, + ItemUpdate, + MaintenanceEntry, + MaintenanceEntryCreate, + MaintenanceEntryUpdate, + MaintenanceLog, +} from "../types/data-contracts"; import { AttachmentTypes, PaginationResult } from "../types/non-generated"; +import { Requests } from "~~/lib/requests"; export type ItemsQuery = { includeArchived?: boolean; @@ -12,7 +23,65 @@ export type ItemsQuery = { q?: string; }; +export class AttachmentsAPI extends BaseAPI { + add(id: string, file: File | Blob, filename: string, type: AttachmentTypes) { + const formData = new FormData(); + formData.append("file", file); + formData.append("type", type); + formData.append("name", filename); + + return this.http.post({ + url: route(`/items/${id}/attachments`), + data: formData, + }); + } + + delete(id: string, attachmentId: string) { + return this.http.delete({ url: route(`/items/${id}/attachments/${attachmentId}`) }); + } + + update(id: string, attachmentId: string, data: ItemAttachmentUpdate) { + return this.http.put({ + url: route(`/items/${id}/attachments/${attachmentId}`), + body: data, + }); + } +} + +export class MaintenanceAPI extends BaseAPI { + getLog(itemId: string) { + return this.http.get({ url: route(`/items/${itemId}/maintenance`) }); + } + + create(itemId: string, data: MaintenanceEntryCreate) { + return this.http.post({ + url: route(`/items/${itemId}/maintenance`), + body: data, + }); + } + + delete(itemId: string, entryId: string) { + return this.http.delete({ url: route(`/items/${itemId}/maintenance/${entryId}`) }); + } + + update(itemId: string, entryId: string, data: MaintenanceEntryUpdate) { + return this.http.put({ + url: route(`/items/${itemId}/maintenance/${entryId}`), + body: data, + }); + } +} + export class ItemsApi extends BaseAPI { + attachments: AttachmentsAPI; + maintenance: MaintenanceAPI; + + constructor(http: Requests, token: string) { + super(http, token); + this.attachments = new AttachmentsAPI(http); + this.maintenance = new MaintenanceAPI(http); + } + getAll(q: ItemsQuery = {}) { return this.http.get>({ url: route("/items", q) }); } @@ -59,27 +128,4 @@ export class ItemsApi extends BaseAPI { data: formData, }); } - - addAttachment(id: string, file: File | Blob, filename: string, type: AttachmentTypes) { - const formData = new FormData(); - formData.append("file", file); - formData.append("type", type); - formData.append("name", filename); - - return this.http.post({ - url: route(`/items/${id}/attachments`), - data: formData, - }); - } - - async deleteAttachment(id: string, attachmentId: string) { - return await this.http.delete({ url: route(`/items/${id}/attachments/${attachmentId}`) }); - } - - async updateAttachment(id: string, attachmentId: string, data: ItemAttachmentUpdate) { - return await this.http.put({ - url: route(`/items/${id}/attachments/${attachmentId}`), - body: data, - }); - } } diff --git a/frontend/lib/api/types/data-contracts.ts b/frontend/lib/api/types/data-contracts.ts index 09f10e7..e313175 100644 --- a/frontend/lib/api/types/data-contracts.ts +++ b/frontend/lib/api/types/data-contracts.ts @@ -54,7 +54,6 @@ export interface ItemAttachmentUpdate { export interface ItemCreate { description: string; labelIds: string[]; - /** Edges */ locationId: string; name: string; @@ -73,8 +72,7 @@ export interface ItemField { export interface ItemOut { archived: boolean; - - /** @example 0 */ + /** @example "0" */ assetId: string; attachments: ItemAttachment[]; children: ItemSummary[]; @@ -84,33 +82,26 @@ export interface ItemOut { id: string; insured: boolean; labels: LabelSummary[]; - /** Warranty */ lifetimeWarranty: boolean; - /** Edges */ location: LocationSummary | null; manufacturer: string; modelNumber: string; name: string; - /** Extras */ notes: string; parent: ItemSummary | null; purchaseFrom: string; - - /** @example 0 */ + /** @example "0" */ purchasePrice: string; - /** Purchase */ purchaseTime: Date; quantity: number; serialNumber: string; soldNotes: string; - - /** @example 0 */ + /** @example "0" */ soldPrice: string; - /** Sold */ soldTime: Date; soldTo: string; @@ -126,7 +117,6 @@ export interface ItemSummary { id: string; insured: boolean; labels: LabelSummary[]; - /** Edges */ location: LocationSummary | null; name: string; @@ -142,35 +132,27 @@ export interface ItemUpdate { id: string; insured: boolean; labelIds: string[]; - /** Warranty */ lifetimeWarranty: boolean; - /** Edges */ locationId: string; manufacturer: string; modelNumber: string; name: string; - /** Extras */ notes: string; parentId: string | null; purchaseFrom: string; - - /** @example 0 */ + /** @example "0" */ purchasePrice: string; - /** Purchase */ purchaseTime: Date; quantity: number; - /** Identifications */ serialNumber: string; soldNotes: string; - - /** @example 0 */ + /** @example "0" */ soldPrice: string; - /** Sold */ soldTime: Date; soldTo: string; @@ -241,6 +223,38 @@ export interface LocationUpdate { parentId: string | null; } +export interface MaintenanceEntry { + /** @example "0" */ + cost: string; + date: Date; + description: string; + id: string; + name: string; +} + +export interface MaintenanceEntryCreate { + /** @example "0" */ + cost: string; + date: Date; + description: string; + name: string; +} + +export interface MaintenanceEntryUpdate { + /** @example "0" */ + cost: string; + date: Date; + description: string; + name: string; +} + +export interface MaintenanceLog { + costAverage: number; + costTotal: number; + entries: MaintenanceEntry[]; + itemId: string; +} + export interface PaginationResultRepoItemSummary { items: ItemSummary[]; page: number; @@ -278,7 +292,7 @@ export interface ValueOverTime { } export interface ValueOverTimeEntry { - date: string; + date: Date; name: string; value: number; } diff --git a/frontend/nuxt.config.ts b/frontend/nuxt.config.ts index 6019d26..6f4638b 100644 --- a/frontend/nuxt.config.ts +++ b/frontend/nuxt.config.ts @@ -1,20 +1,16 @@ -import { defineNuxtConfig } from "nuxt"; +import { defineNuxtConfig } from "nuxt/config"; // https://v3.nuxtjs.org/api/configuration/nuxt.config export default defineNuxtConfig({ - target: "static", ssr: false, modules: ["@nuxtjs/tailwindcss", "@pinia/nuxt", "@vueuse/nuxt"], - meta: { - title: "Homebox", - link: [{ rel: "icon", type: "image/x-icon", href: "/favicon.svg" }], - }, - vite: { - server: { - proxy: { - "/api": "http://localhost:7745", - }, + nitro: { + devProxy: { + "/api": { + target: "http://localhost:7745/api", + changeOrigin: true, + } }, - plugins: [], }, + plugins: [], }); diff --git a/frontend/package.json b/frontend/package.json index a06db47..973e383 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -21,7 +21,7 @@ "eslint-plugin-prettier": "^4.2.1", "eslint-plugin-vue": "^9.4.0", "isomorphic-fetch": "^3.0.0", - "nuxt": "3.0.0-rc.11", + "nuxt": "3.0.0", "prettier": "^2.7.1", "typescript": "^4.8.3", "vite-plugin-eslint": "^1.8.1", @@ -29,7 +29,7 @@ }, "dependencies": { "@iconify/vue": "^3.2.1", - "@nuxtjs/tailwindcss": "^5.3.2", + "@nuxtjs/tailwindcss": "^6.1.3", "@pinia/nuxt": "^0.4.1", "@tailwindcss/aspect-ratio": "^0.4.0", "@tailwindcss/forms": "^0.5.2", diff --git a/frontend/pages/home.vue b/frontend/pages/home.vue index 92edee3..e8a52e7 100644 --- a/frontend/pages/home.vue +++ b/frontend/pages/home.vue @@ -46,7 +46,7 @@ const importDialog = ref(false); const importCsv = ref(null); const importLoading = ref(false); - const importRef = ref(null); + const importRef = ref(); whenever( () => !importDialog.value, () => { @@ -120,7 +120,7 @@
- +
diff --git a/frontend/pages/item/[id]/index/log.vue b/frontend/pages/item/[id]/index/log.vue index ab701db..b0f06b2 100644 --- a/frontend/pages/item/[id]/index/log.vue +++ b/frontend/pages/item/[id]/index/log.vue @@ -1,5 +1,6 @@ @@ -95,16 +112,32 @@ -
- - - Log Maintenance - -
-
-
+
+
+ + + Back + + + + Log Maintenance + +
+
+ +
+
@@ -137,37 +170,6 @@
-
-
-
-
{{ stat.title }}
-
{{ stat.value }}
-
{{ stat.subtitle }}
-
-
-
- - diff --git a/frontend/pages/items.vue b/frontend/pages/items.vue index 317a0f0..34d453d 100644 --- a/frontend/pages/items.vue +++ b/frontend/pages/items.vue @@ -135,7 +135,7 @@
diff --git a/frontend/pages/profile.vue b/frontend/pages/profile.vue index f2f32fe..fe3c3ef 100644 --- a/frontend/pages/profile.vue +++ b/frontend/pages/profile.vue @@ -22,8 +22,6 @@ if (group.value) { group.value.currency = currency.value.code; } - - console.log(group.value); }); const currencyExample = computed(() => { diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 8013d1b..1de5c6f 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -13,6 +13,7 @@ specifiers: '@typescript-eslint/parser': ^5.36.2 '@vueuse/nuxt': ^9.1.1 autoprefixer: ^10.4.8 + chart.js: ^4.0.1 daisyui: ^2.24.0 dompurify: ^2.4.1 eslint: ^8.23.0 @@ -30,6 +31,7 @@ specifiers: vite-plugin-eslint: ^1.8.1 vitest: ^0.22.1 vue: ^3.2.38 + vue-chartjs: ^4.1.2 dependencies: '@iconify/vue': 3.2.1_vue@3.2.45 @@ -40,6 +42,7 @@ dependencies: '@tailwindcss/typography': 0.5.8_tailwindcss@3.2.4 '@vueuse/nuxt': 9.6.0_nuxt@3.0.0+vue@3.2.45 autoprefixer: 10.4.13_postcss@8.4.19 + chart.js: 4.0.1 daisyui: 2.43.0_2lwn2upnx27dqeg6hqdu7sq75m dompurify: 2.4.1 markdown-it: 13.0.1 @@ -47,6 +50,7 @@ dependencies: postcss: 8.4.19 tailwindcss: 3.2.4_postcss@8.4.19 vue: 3.2.45 + vue-chartjs: 4.1.2_chart.js@4.0.1+vue@3.2.45 devDependencies: '@faker-js/faker': 7.6.0 @@ -1750,6 +1754,11 @@ packages: /chardet/0.7.0: resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} + /chart.js/4.0.1: + resolution: {integrity: sha512-5/8/9eBivwBZK81mKvmIwTb2Pmw4D/5h1RK9fBWZLLZ8mCJ+kfYNmV9rMrGoa5Hgy2/wVDBMLSUDudul2/9ihA==} + engines: {pnpm: ^7.0.0} + dev: false + /check-error/1.0.2: resolution: {integrity: sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==} dev: true @@ -6412,6 +6421,16 @@ packages: dependencies: ufo: 1.0.1 + /vue-chartjs/4.1.2_chart.js@4.0.1+vue@3.2.45: + resolution: {integrity: sha512-QSggYjeFv/L4jFSBQpX8NzrAvX0B+Ha6nDgxkTG8tEXxYOOTwKI4phRLe+B4f+REnkmg7hgPY24R0cixZJyXBg==} + peerDependencies: + chart.js: ^3.7.0 + vue: ^3.0.0-0 || ^2.6.0 + dependencies: + chart.js: 4.0.1 + vue: 3.2.45 + dev: false + /vue-demi/0.13.11_vue@3.2.45: resolution: {integrity: sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==} engines: {node: '>=12'} diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js index fc78c6c..acb95bb 100644 --- a/frontend/tailwind.config.js +++ b/frontend/tailwind.config.js @@ -4,6 +4,52 @@ module.exports = { theme: { extend: {}, }, + daisyui: { + themes: [ + { + homebox: { + primary: "#5C7F67", + secondary: "#ECF4E7", + accent: "#FFDA56", + neutral: "#2C2E27", + "base-100": "#FFFFFF", + info: "#3ABFF8", + success: "#36D399", + warning: "#FBBD23", + error: "#F87272", + }, + }, + "light", + "dark", + "cupcake", + "bumblebee", + "emerald", + "corporate", + "synthwave", + "retro", + "cyberpunk", + "valentine", + "halloween", + "garden", + "forest", + "aqua", + "lofi", + "pastel", + "fantasy", + "wireframe", + "black", + "luxury", + "dracula", + "cmyk", + "autumn", + "business", + "acid", + "lemonade", + "night", + "coffee", + "winter", + ], + }, variants: { extend: {}, }, From 58d6f9a28c13a060fcad19385abe2712825cea2e Mon Sep 17 00:00:00 2001 From: Hayden <64056131+hay-kot@users.noreply.github.com> Date: Thu, 29 Dec 2022 21:18:49 -0800 Subject: [PATCH 030/360] Fix/mobile-layouts (#192) * partial fix for location card spacing * update header on mobile --- frontend/components/Location/Card.vue | 9 ++++++++- frontend/layouts/default.vue | 23 ++++++++++++++++------- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/frontend/components/Location/Card.vue b/frontend/components/Location/Card.vue index 335a0f9..d0b6543 100644 --- a/frontend/components/Location/Card.vue +++ b/frontend/components/Location/Card.vue @@ -19,7 +19,14 @@ {{ location.name }} - {{ count }} + + {{ count }} + diff --git a/frontend/layouts/default.vue b/frontend/layouts/default.vue index 8dff148..ef1a8ca 100644 --- a/frontend/layouts/default.vue +++ b/frontend/layouts/default.vue @@ -13,14 +13,23 @@
-
- - - +
+
From 891d41b75f759348d9cae3839134656894e2922c Mon Sep 17 00:00:00 2001 From: Hayden <64056131+hay-kot@users.noreply.github.com> Date: Sun, 1 Jan 2023 13:50:48 -0800 Subject: [PATCH 031/360] feat: new-card-design (#196) * card option 1 * UI updates for item card * fix test error * fix pagination issues on backend * add integer support * remove date from cards * implement pagination for search page * resolve search state problems * other fixes * fix broken datetime * attempt to fix scroll behavior --- backend/app/api/handlers/v1/v1_ctrl_items.go | 7 + backend/go.mod | 8 +- backend/go.sum | 42 ++++-- backend/internal/data/repo/repo_items.go | 20 +-- .../data/repo/repo_maintenance_entry_test.go | 23 +++- frontend/components/Chart/Line.vue | 1 - frontend/components/Item/Card.vue | 80 ++++++++--- frontend/components/global/DateTime.vue | 71 ++-------- frontend/composables/use-formatters.ts | 60 +++++++- frontend/composables/use-route-params.ts | 38 +++-- frontend/layouts/default.vue | 3 + frontend/lib/api/base/base-api.ts | 10 ++ frontend/nuxt.config.ts | 1 - frontend/package.json | 4 +- frontend/pages/items.vue | 130 +++++++++++++++--- frontend/pages/label/[id].vue | 2 +- frontend/pages/location/[id].vue | 2 +- frontend/plugins/scroll.client.ts | 7 + frontend/pnpm-lock.yaml | 26 ++++ 19 files changed, 393 insertions(+), 142 deletions(-) create mode 100644 frontend/plugins/scroll.client.ts diff --git a/backend/app/api/handlers/v1/v1_ctrl_items.go b/backend/app/api/handlers/v1/v1_ctrl_items.go index ea961f3..d7be19d 100644 --- a/backend/app/api/handlers/v1/v1_ctrl_items.go +++ b/backend/app/api/handlers/v1/v1_ctrl_items.go @@ -1,6 +1,8 @@ package v1 import ( + "database/sql" + "errors" "net/http" "github.com/hay-kot/homebox/backend/internal/core/services" @@ -41,6 +43,11 @@ func (ctrl *V1Controller) HandleItemsGetAll() server.HandlerFunc { ctx := services.NewContext(r.Context()) items, err := ctrl.repo.Items.QueryByGroup(ctx, ctx.GID, extractQuery(r)) if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return server.Respond(w, http.StatusOK, repo.PaginationResult[repo.ItemSummary]{ + Items: []repo.ItemSummary{}, + }) + } log.Err(err).Msg("failed to get items") return validate.NewRequestError(err, http.StatusInternalServerError) } diff --git a/backend/go.mod b/backend/go.mod index 3f1f50f..275b010 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -35,15 +35,15 @@ require ( github.com/leodido/go-urn v1.2.1 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.16 // indirect + github.com/mattn/go-isatty v0.0.17 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a // indirect + github.com/swaggo/files v1.0.0 // indirect github.com/zclconf/go-cty v1.12.1 // indirect golang.org/x/mod v0.7.0 // indirect - golang.org/x/net v0.3.0 // indirect + golang.org/x/net v0.4.0 // indirect golang.org/x/sys v0.3.0 // indirect golang.org/x/text v0.5.0 // indirect - golang.org/x/tools v0.3.0 // indirect + golang.org/x/tools v0.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/backend/go.sum b/backend/go.sum index a5ad319..243b47a 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -70,13 +70,16 @@ github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= +github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -88,6 +91,8 @@ github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.28.0 h1:MirSo27VyNi7RJYP3078AA1+Cyzd2GB66qy3aUHvsWY= github.com/rs/zerolog v1.28.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0= github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= +github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -98,40 +103,61 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a h1:kAe4YSu0O0UFn1DowNo2MY5p6xzqtJ/wQ7LZynSvGaY= -github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a/go.mod h1:lKJPbtWzJ9JhsTN1k1gZgleJWY/cqq0psdoMmaThG3w= +github.com/swaggo/files v1.0.0 h1:1gGXVIeUFCS/dta17rnP0iOpr6CXFwKD7EO5ID233e4= +github.com/swaggo/files v1.0.0/go.mod h1:N59U6URJLyU1PQgFqPM7wXLMhJx7QAolnvfQkqO13kc= github.com/swaggo/http-swagger v1.3.3 h1:Hu5Z0L9ssyBLofaama21iYaF2VbWyA8jdohaaCGpHsc= github.com/swaggo/http-swagger v1.3.3/go.mod h1:sE+4PjD89IxMPm77FnkDz0sdO+p5lbXzrVWT6OTVVGo= github.com/swaggo/swag v1.8.9 h1:kHtaBe/Ob9AZzAANfcn5c6RyCke9gG9QpH0jky0I/sA= github.com/swaggo/swag v1.8.9/go.mod h1:ezQVUUhly8dludpVk+/PuwJWvLLanB13ygV5Pr9enSk= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zclconf/go-cty v1.12.1 h1:PcupnljUm9EIvbgSHQnHhUr3fO6oFmkOrvs2BAFNXXY= github.com/zclconf/go-cty v1.12.1/go.mod h1:s9IfD1LK5ccNMSWCVFCE2rJfHiZgi7JijgeWIMfhLvA= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.4.0 h1:UVQgzMY87xqpKNgb+kDsll2Igd33HszWHFLmpaRMq/8= golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA= golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.3.0 h1:VWL6FNY2bEEmsGVKabSlHu5Irp34xmMRoqb/9lF9lxk= -golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= +golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU= +golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM= golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.3.0 h1:SrNbZl6ECOS1qFzgTdQfWXZM9XBkiA6tkFrH9YSTPHM= -golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.4.0 h1:7mTAgkunk3fr4GAloyyCasadO6h9zSsQZbwvcaIciV4= +golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/backend/internal/data/repo/repo_items.go b/backend/internal/data/repo/repo_items.go index d8a3904..e22f30a 100644 --- a/backend/internal/data/repo/repo_items.go +++ b/backend/internal/data/repo/repo_items.go @@ -326,23 +326,23 @@ func (e *ItemsRepository) QueryByGroup(ctx context.Context, gid uuid.UUID, q Ite ) } + count, err := qb.Count(ctx) + if err != nil { + return PaginationResult[ItemSummary]{}, err + } + + qb = qb.Order(ent.Asc(item.FieldName)). + WithLabel(). + WithLocation() + if q.Page != -1 || q.PageSize != -1 { qb = qb. Offset(calculateOffset(q.Page, q.PageSize)). Limit(q.PageSize) } - items, err := mapItemsSummaryErr( - qb.Order(ent.Asc(item.FieldName)). - WithLabel(). - WithLocation(). - All(ctx), - ) - if err != nil { - return PaginationResult[ItemSummary]{}, err - } + items, err := mapItemsSummaryErr(qb.All(ctx)) - count, err := qb.Count(ctx) if err != nil { return PaginationResult[ItemSummary]{}, err } diff --git a/backend/internal/data/repo/repo_maintenance_entry_test.go b/backend/internal/data/repo/repo_maintenance_entry_test.go index 8babefc..e3df3d0 100644 --- a/backend/internal/data/repo/repo_maintenance_entry_test.go +++ b/backend/internal/data/repo/repo_maintenance_entry_test.go @@ -8,14 +8,35 @@ import ( "github.com/stretchr/testify/assert" ) +// get the previous month from the current month, accounts for errors when run +// near the beginning or end of the month/year +func getPrevMonth(now time.Time) time.Time { + t := now.AddDate(0, -1, 0) + + // avoid infinite loop + max := 15 + for t.Month() == now.Month() { + println("month is the same") + t = t.AddDate(0, 0, -1) + println(t.String()) + + max-- + if max == 0 { + panic("max exceeded") + } + } + + return t +} + func TestMaintenanceEntryRepository_GetLog(t *testing.T) { item := useItems(t, 1)[0] // Create 10 maintenance entries for the item created := make([]MaintenanceEntryCreate, 10) - lastMonth := time.Now().AddDate(0, -1, 0) thisMonth := time.Now() + lastMonth := getPrevMonth(thisMonth) for i := 0; i < 10; i++ { dt := lastMonth diff --git a/frontend/components/Chart/Line.vue b/frontend/components/Chart/Line.vue index c74e759..c36ef93 100644 --- a/frontend/components/Chart/Line.vue +++ b/frontend/components/Chart/Line.vue @@ -56,7 +56,6 @@ const calcWidth = ref(0); function resize() { - console.log("resize", el.value?.offsetHeight, el.value?.offsetWidth); calcHeight.value = el.value?.offsetHeight || 0; calcWidth.value = el.value?.offsetWidth || 0; } diff --git a/frontend/components/Item/Card.vue b/frontend/components/Item/Card.vue index 7dfb62a..96b3b04 100644 --- a/frontend/components/Item/Card.vue +++ b/frontend/components/Item/Card.vue @@ -1,22 +1,38 @@ + + + + diff --git a/frontend/lib/api/types/data-contracts.ts b/frontend/lib/api/types/data-contracts.ts index 7e4fd54..a36df88 100644 --- a/frontend/lib/api/types/data-contracts.ts +++ b/frontend/lib/api/types/data-contracts.ts @@ -96,7 +96,7 @@ export interface ItemOut { /** @example "0" */ purchasePrice: string; /** Purchase */ - purchaseTime: Date; + purchaseTime: string; quantity: number; serialNumber: string; soldNotes: string; @@ -148,7 +148,7 @@ export interface ItemUpdate { /** @example "0" */ purchasePrice: string; /** Purchase */ - purchaseTime: Date; + purchaseTime: string; quantity: number; /** Identifications */ serialNumber: string; @@ -228,7 +228,7 @@ export interface LocationUpdate { export interface MaintenanceEntry { /** @example "0" */ cost: string; - date: Date; + date: string; description: string; id: string; name: string; @@ -237,7 +237,7 @@ export interface MaintenanceEntry { export interface MaintenanceEntryCreate { /** @example "0" */ cost: string; - date: Date; + date: string; description: string; name: string; } @@ -245,7 +245,7 @@ export interface MaintenanceEntryCreate { export interface MaintenanceEntryUpdate { /** @example "0" */ cost: string; - date: Date; + date: string; description: string; name: string; } @@ -257,7 +257,7 @@ export interface MaintenanceLog { itemId: string; } -export interface PaginationResultRepoItemSummary { +export interface PaginationResultItemSummary { items: ItemSummary[]; page: number; pageSize: number; @@ -294,7 +294,7 @@ export interface ValueOverTime { } export interface ValueOverTimeEntry { - date: Date; + date: string; name: string; value: number; } @@ -347,13 +347,13 @@ export interface EnsureAssetIDResult { } export interface GroupInvitation { - expiresAt: Date; + expiresAt: string; token: string; uses: number; } export interface GroupInvitationCreate { - expiresAt: Date; + expiresAt: string; uses: number; } @@ -363,6 +363,6 @@ export interface ItemAttachmentToken { export interface TokenResponse { attachmentToken: string; - expiresAt: Date; + expiresAt: string; token: string; } diff --git a/frontend/pages/item/[id]/index.vue b/frontend/pages/item/[id]/index.vue index 4bcef2c..42c4995 100644 --- a/frontend/pages/item/[id]/index.vue +++ b/frontend/pages/item/[id]/index.vue @@ -30,6 +30,15 @@ refresh(); }); + const lastRoute = ref(route.fullPath); + watchEffect(() => { + if (lastRoute.value.endsWith("edit")) { + refresh(); + } + + lastRoute.value = route.fullPath; + }); + type FilteredAttachments = { attachments: ItemAttachment[]; warranty: ItemAttachment[]; @@ -325,6 +334,30 @@ onClickOutside(refDialogBody, () => { closeDialog(); }); + + const currentPath = computed(() => { + return route.path; + }); + + const tabs = computed(() => { + return [ + { + id: "details", + name: "Details", + to: `/item/${itemId.value}`, + }, + { + id: "log", + name: "Log", + to: `/item/${itemId.value}/log`, + }, + { + id: "edit", + name: "Edit", + to: `/item/${itemId.value}/edit`, + }, + ]; + }); diff --git a/scripts/process-types/main.go b/scripts/process-types/main.go index 59c2530..06e3ec7 100644 --- a/scripts/process-types/main.go +++ b/scripts/process-types/main.go @@ -2,7 +2,6 @@ package main import ( "fmt" - "io/ioutil" "os" "regexp" ) @@ -31,7 +30,7 @@ func main() { } text := "/* post-processed by ./scripts/process-types.go */\n" - data, err := ioutil.ReadFile(path) + data, err := os.ReadFile(path) if err != nil { fmt.Println(err) os.Exit(1) @@ -63,7 +62,7 @@ func main() { text = regex.ReplaceAllString(text, replace) } - err = ioutil.WriteFile(path, []byte(text), 0644) + err = os.WriteFile(path, []byte(text), 0644) if err != nil { fmt.Println(err) os.Exit(1) From 91d0c588d906e6008ca0c6ff993f8fd2fd43ee90 Mon Sep 17 00:00:00 2001 From: Hayden <64056131+hay-kot@users.noreply.github.com> Date: Sat, 21 Jan 2023 21:15:23 -0900 Subject: [PATCH 047/360] refactor: refactor item page UI (#235) * fix generated types * fix tailwind auto-complete * force lowercase buttons * add title and change style for items page * add copy button support for item details * empty state for log * fix duplicate padding * add option for create without closing the current dialog. * hide purchase price is not set * invert toggle for edit mode * update styles on item cards * add edit support for maintenance logs --- .vscode/settings.json | 9 +++ frontend/assets/css/main.css | 4 + frontend/components/Item/Card.vue | 18 ++--- frontend/components/Item/CreateModal.vue | 51 ++++++++----- frontend/components/global/CopyText.vue | 24 +++++- .../global/DetailsSection/DetailsSection.vue | 14 +++- .../components/global/DetailsSection/types.ts | 1 + frontend/composables/use-preferences.ts | 6 +- frontend/lib/api/types/data-contracts.ts | 14 ++-- frontend/pages/item/[id]/index.vue | 27 +++++-- frontend/pages/item/[id]/index/edit.vue | 20 ++--- frontend/pages/item/[id]/index/log.vue | 73 ++++++++++++++++++- frontend/pages/profile.vue | 4 +- 13 files changed, 197 insertions(+), 68 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 5080f25..d0ae55d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -21,4 +21,13 @@ "editor.defaultFormatter": "dbaeumer.vscode-eslint" }, "eslint.format.enable": true, + "css.validate": false, + "tailwindCSS.includeLanguages": { + "vue": "html", + "vue-html": "html" + }, + "editor.quickSuggestions": { + "strings": true + }, + "tailwindCSS.experimental.configFile": "./frontend/tailwind.config.js" } diff --git a/frontend/assets/css/main.css b/frontend/assets/css/main.css index a3c199c..d83faf6 100644 --- a/frontend/assets/css/main.css +++ b/frontend/assets/css/main.css @@ -1,3 +1,7 @@ .text-no-transform { text-transform: none !important; +} + +.btn { + text-transform: none !important; } \ No newline at end of file diff --git a/frontend/components/Item/Card.vue b/frontend/components/Item/Card.vue index 6ca1704..c0b5b88 100644 --- a/frontend/components/Item/Card.vue +++ b/frontend/components/Item/Card.vue @@ -1,22 +1,18 @@