feat: add archive item options (#122)

Add archive option feature. Archived items can only be seen on the items page when including archived is selected. Archived items are excluded from the count and from other views
This commit is contained in:
Hayden 2022-10-31 23:30:42 -08:00 committed by GitHub
parent c722495fdd
commit a886fa86ca
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 325 additions and 38 deletions

View file

@ -47,15 +47,24 @@ func (ctrl *V1Controller) HandleItemsGetAll() server.HandlerFunc {
return i return i
} }
getBool := func(s string) bool {
b, err := strconv.ParseBool(s)
if err != nil {
return false
}
return b
}
extractQuery := func(r *http.Request) repo.ItemQuery { extractQuery := func(r *http.Request) repo.ItemQuery {
params := r.URL.Query() params := r.URL.Query()
return repo.ItemQuery{ return repo.ItemQuery{
Page: intOrNegativeOne(params.Get("page")), Page: intOrNegativeOne(params.Get("page")),
PageSize: intOrNegativeOne(params.Get("perPage")), PageSize: intOrNegativeOne(params.Get("perPage")),
Search: params.Get("q"), Search: params.Get("q"),
LocationIDs: uuidList(params, "locations"), LocationIDs: uuidList(params, "locations"),
LabelIDs: uuidList(params, "labels"), LabelIDs: uuidList(params, "labels"),
IncludeArchived: getBool(params.Get("includeArchived")),
} }
} }

View file

@ -1274,6 +1274,9 @@ const docTemplate = `{
"repo.ItemOut": { "repo.ItemOut": {
"type": "object", "type": "object",
"properties": { "properties": {
"archived": {
"type": "boolean"
},
"attachments": { "attachments": {
"type": "array", "type": "array",
"items": { "items": {
@ -1383,6 +1386,9 @@ const docTemplate = `{
"repo.ItemSummary": { "repo.ItemSummary": {
"type": "object", "type": "object",
"properties": { "properties": {
"archived": {
"type": "boolean"
},
"createdAt": { "createdAt": {
"type": "string" "type": "string"
}, },
@ -1421,6 +1427,9 @@ const docTemplate = `{
"repo.ItemUpdate": { "repo.ItemUpdate": {
"type": "object", "type": "object",
"properties": { "properties": {
"archived": {
"type": "boolean"
},
"description": { "description": {
"type": "string" "type": "string"
}, },

View file

@ -1266,6 +1266,9 @@
"repo.ItemOut": { "repo.ItemOut": {
"type": "object", "type": "object",
"properties": { "properties": {
"archived": {
"type": "boolean"
},
"attachments": { "attachments": {
"type": "array", "type": "array",
"items": { "items": {
@ -1375,6 +1378,9 @@
"repo.ItemSummary": { "repo.ItemSummary": {
"type": "object", "type": "object",
"properties": { "properties": {
"archived": {
"type": "boolean"
},
"createdAt": { "createdAt": {
"type": "string" "type": "string"
}, },
@ -1413,6 +1419,9 @@
"repo.ItemUpdate": { "repo.ItemUpdate": {
"type": "object", "type": "object",
"properties": { "properties": {
"archived": {
"type": "boolean"
},
"description": { "description": {
"type": "string" "type": "string"
}, },

View file

@ -85,6 +85,8 @@ definitions:
type: object type: object
repo.ItemOut: repo.ItemOut:
properties: properties:
archived:
type: boolean
attachments: attachments:
items: items:
$ref: '#/definitions/repo.ItemAttachment' $ref: '#/definitions/repo.ItemAttachment'
@ -161,6 +163,8 @@ definitions:
type: object type: object
repo.ItemSummary: repo.ItemSummary:
properties: properties:
archived:
type: boolean
createdAt: createdAt:
type: string type: string
description: description:
@ -187,6 +191,8 @@ definitions:
type: object type: object
repo.ItemUpdate: repo.ItemUpdate:
properties: properties:
archived:
type: boolean
description: description:
type: string type: string
fields: fields:

View file

@ -35,6 +35,8 @@ type Item struct {
Quantity int `json:"quantity,omitempty"` Quantity int `json:"quantity,omitempty"`
// Insured holds the value of the "insured" field. // Insured holds the value of the "insured" field.
Insured bool `json:"insured,omitempty"` Insured bool `json:"insured,omitempty"`
// Archived holds the value of the "archived" field.
Archived bool `json:"archived,omitempty"`
// SerialNumber holds the value of the "serial_number" field. // SerialNumber holds the value of the "serial_number" field.
SerialNumber string `json:"serial_number,omitempty"` SerialNumber string `json:"serial_number,omitempty"`
// ModelNumber holds the value of the "model_number" field. // ModelNumber holds the value of the "model_number" field.
@ -170,7 +172,7 @@ func (*Item) scanValues(columns []string) ([]any, error) {
values := make([]any, len(columns)) values := make([]any, len(columns))
for i := range columns { for i := range columns {
switch columns[i] { switch columns[i] {
case item.FieldInsured, item.FieldLifetimeWarranty: case item.FieldInsured, item.FieldArchived, item.FieldLifetimeWarranty:
values[i] = new(sql.NullBool) values[i] = new(sql.NullBool)
case item.FieldPurchasePrice, item.FieldSoldPrice: case item.FieldPurchasePrice, item.FieldSoldPrice:
values[i] = new(sql.NullFloat64) values[i] = new(sql.NullFloat64)
@ -257,6 +259,12 @@ func (i *Item) assignValues(columns []string, values []any) error {
} else if value.Valid { } else if value.Valid {
i.Insured = value.Bool i.Insured = value.Bool
} }
case item.FieldArchived:
if value, ok := values[j].(*sql.NullBool); !ok {
return fmt.Errorf("unexpected type %T for field archived", values[j])
} else if value.Valid {
i.Archived = value.Bool
}
case item.FieldSerialNumber: case item.FieldSerialNumber:
if value, ok := values[j].(*sql.NullString); !ok { if value, ok := values[j].(*sql.NullString); !ok {
return fmt.Errorf("unexpected type %T for field serial_number", values[j]) return fmt.Errorf("unexpected type %T for field serial_number", values[j])
@ -443,6 +451,9 @@ func (i *Item) String() string {
builder.WriteString("insured=") builder.WriteString("insured=")
builder.WriteString(fmt.Sprintf("%v", i.Insured)) builder.WriteString(fmt.Sprintf("%v", i.Insured))
builder.WriteString(", ") builder.WriteString(", ")
builder.WriteString("archived=")
builder.WriteString(fmt.Sprintf("%v", i.Archived))
builder.WriteString(", ")
builder.WriteString("serial_number=") builder.WriteString("serial_number=")
builder.WriteString(i.SerialNumber) builder.WriteString(i.SerialNumber)
builder.WriteString(", ") builder.WriteString(", ")

View file

@ -29,6 +29,8 @@ const (
FieldQuantity = "quantity" FieldQuantity = "quantity"
// FieldInsured holds the string denoting the insured field in the database. // FieldInsured holds the string denoting the insured field in the database.
FieldInsured = "insured" FieldInsured = "insured"
// FieldArchived holds the string denoting the archived field in the database.
FieldArchived = "archived"
// FieldSerialNumber holds the string denoting the serial_number field in the database. // FieldSerialNumber holds the string denoting the serial_number field in the database.
FieldSerialNumber = "serial_number" FieldSerialNumber = "serial_number"
// FieldModelNumber holds the string denoting the model_number field in the database. // FieldModelNumber holds the string denoting the model_number field in the database.
@ -125,6 +127,7 @@ var Columns = []string{
FieldNotes, FieldNotes,
FieldQuantity, FieldQuantity,
FieldInsured, FieldInsured,
FieldArchived,
FieldSerialNumber, FieldSerialNumber,
FieldModelNumber, FieldModelNumber,
FieldManufacturer, FieldManufacturer,
@ -188,6 +191,8 @@ var (
DefaultQuantity int DefaultQuantity int
// DefaultInsured holds the default value on creation for the "insured" field. // DefaultInsured holds the default value on creation for the "insured" field.
DefaultInsured bool DefaultInsured bool
// DefaultArchived holds the default value on creation for the "archived" field.
DefaultArchived bool
// SerialNumberValidator is a validator for the "serial_number" field. It is called by the builders before save. // SerialNumberValidator is a validator for the "serial_number" field. It is called by the builders before save.
SerialNumberValidator func(string) error SerialNumberValidator func(string) error
// ModelNumberValidator is a validator for the "model_number" field. It is called by the builders before save. // ModelNumberValidator is a validator for the "model_number" field. It is called by the builders before save.

View file

@ -138,6 +138,13 @@ func Insured(v bool) predicate.Item {
}) })
} }
// Archived applies equality check predicate on the "archived" field. It's identical to ArchivedEQ.
func Archived(v bool) predicate.Item {
return predicate.Item(func(s *sql.Selector) {
s.Where(sql.EQ(s.C(FieldArchived), v))
})
}
// SerialNumber applies equality check predicate on the "serial_number" field. It's identical to SerialNumberEQ. // SerialNumber applies equality check predicate on the "serial_number" field. It's identical to SerialNumberEQ.
func SerialNumber(v string) predicate.Item { func SerialNumber(v string) predicate.Item {
return predicate.Item(func(s *sql.Selector) { return predicate.Item(func(s *sql.Selector) {
@ -873,6 +880,20 @@ func InsuredNEQ(v bool) predicate.Item {
}) })
} }
// ArchivedEQ applies the EQ predicate on the "archived" field.
func ArchivedEQ(v bool) predicate.Item {
return predicate.Item(func(s *sql.Selector) {
s.Where(sql.EQ(s.C(FieldArchived), v))
})
}
// ArchivedNEQ applies the NEQ predicate on the "archived" field.
func ArchivedNEQ(v bool) predicate.Item {
return predicate.Item(func(s *sql.Selector) {
s.Where(sql.NEQ(s.C(FieldArchived), v))
})
}
// SerialNumberEQ applies the EQ predicate on the "serial_number" field. // SerialNumberEQ applies the EQ predicate on the "serial_number" field.
func SerialNumberEQ(v string) predicate.Item { func SerialNumberEQ(v string) predicate.Item {
return predicate.Item(func(s *sql.Selector) { return predicate.Item(func(s *sql.Selector) {

View file

@ -130,6 +130,20 @@ func (ic *ItemCreate) SetNillableInsured(b *bool) *ItemCreate {
return ic return ic
} }
// SetArchived sets the "archived" field.
func (ic *ItemCreate) SetArchived(b bool) *ItemCreate {
ic.mutation.SetArchived(b)
return ic
}
// SetNillableArchived sets the "archived" field if the given value is not nil.
func (ic *ItemCreate) SetNillableArchived(b *bool) *ItemCreate {
if b != nil {
ic.SetArchived(*b)
}
return ic
}
// SetSerialNumber sets the "serial_number" field. // SetSerialNumber sets the "serial_number" field.
func (ic *ItemCreate) SetSerialNumber(s string) *ItemCreate { func (ic *ItemCreate) SetSerialNumber(s string) *ItemCreate {
ic.mutation.SetSerialNumber(s) ic.mutation.SetSerialNumber(s)
@ -528,6 +542,10 @@ func (ic *ItemCreate) defaults() {
v := item.DefaultInsured v := item.DefaultInsured
ic.mutation.SetInsured(v) ic.mutation.SetInsured(v)
} }
if _, ok := ic.mutation.Archived(); !ok {
v := item.DefaultArchived
ic.mutation.SetArchived(v)
}
if _, ok := ic.mutation.LifetimeWarranty(); !ok { if _, ok := ic.mutation.LifetimeWarranty(); !ok {
v := item.DefaultLifetimeWarranty v := item.DefaultLifetimeWarranty
ic.mutation.SetLifetimeWarranty(v) ic.mutation.SetLifetimeWarranty(v)
@ -583,6 +601,9 @@ func (ic *ItemCreate) check() error {
if _, ok := ic.mutation.Insured(); !ok { if _, ok := ic.mutation.Insured(); !ok {
return &ValidationError{Name: "insured", err: errors.New(`ent: missing required field "Item.insured"`)} return &ValidationError{Name: "insured", err: errors.New(`ent: missing required field "Item.insured"`)}
} }
if _, ok := ic.mutation.Archived(); !ok {
return &ValidationError{Name: "archived", err: errors.New(`ent: missing required field "Item.archived"`)}
}
if v, ok := ic.mutation.SerialNumber(); ok { if v, ok := ic.mutation.SerialNumber(); ok {
if err := item.SerialNumberValidator(v); err != nil { 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)} return &ValidationError{Name: "serial_number", err: fmt.Errorf(`ent: validator failed for field "Item.serial_number": %w`, err)}
@ -720,6 +741,14 @@ func (ic *ItemCreate) createSpec() (*Item, *sqlgraph.CreateSpec) {
}) })
_node.Insured = value _node.Insured = value
} }
if value, ok := ic.mutation.Archived(); ok {
_spec.Fields = append(_spec.Fields, &sqlgraph.FieldSpec{
Type: field.TypeBool,
Value: value,
Column: item.FieldArchived,
})
_node.Archived = value
}
if value, ok := ic.mutation.SerialNumber(); ok { if value, ok := ic.mutation.SerialNumber(); ok {
_spec.Fields = append(_spec.Fields, &sqlgraph.FieldSpec{ _spec.Fields = append(_spec.Fields, &sqlgraph.FieldSpec{
Type: field.TypeString, Type: field.TypeString,

View file

@ -121,6 +121,20 @@ func (iu *ItemUpdate) SetNillableInsured(b *bool) *ItemUpdate {
return iu return iu
} }
// SetArchived sets the "archived" field.
func (iu *ItemUpdate) SetArchived(b bool) *ItemUpdate {
iu.mutation.SetArchived(b)
return iu
}
// SetNillableArchived sets the "archived" field if the given value is not nil.
func (iu *ItemUpdate) SetNillableArchived(b *bool) *ItemUpdate {
if b != nil {
iu.SetArchived(*b)
}
return iu
}
// SetSerialNumber sets the "serial_number" field. // SetSerialNumber sets the "serial_number" field.
func (iu *ItemUpdate) SetSerialNumber(s string) *ItemUpdate { func (iu *ItemUpdate) SetSerialNumber(s string) *ItemUpdate {
iu.mutation.SetSerialNumber(s) iu.mutation.SetSerialNumber(s)
@ -795,6 +809,13 @@ func (iu *ItemUpdate) sqlSave(ctx context.Context) (n int, err error) {
Column: item.FieldInsured, Column: item.FieldInsured,
}) })
} }
if value, ok := iu.mutation.Archived(); ok {
_spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{
Type: field.TypeBool,
Value: value,
Column: item.FieldArchived,
})
}
if value, ok := iu.mutation.SerialNumber(); ok { if value, ok := iu.mutation.SerialNumber(); ok {
_spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{ _spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{
Type: field.TypeString, Type: field.TypeString,
@ -1387,6 +1408,20 @@ func (iuo *ItemUpdateOne) SetNillableInsured(b *bool) *ItemUpdateOne {
return iuo return iuo
} }
// SetArchived sets the "archived" field.
func (iuo *ItemUpdateOne) SetArchived(b bool) *ItemUpdateOne {
iuo.mutation.SetArchived(b)
return iuo
}
// SetNillableArchived sets the "archived" field if the given value is not nil.
func (iuo *ItemUpdateOne) SetNillableArchived(b *bool) *ItemUpdateOne {
if b != nil {
iuo.SetArchived(*b)
}
return iuo
}
// SetSerialNumber sets the "serial_number" field. // SetSerialNumber sets the "serial_number" field.
func (iuo *ItemUpdateOne) SetSerialNumber(s string) *ItemUpdateOne { func (iuo *ItemUpdateOne) SetSerialNumber(s string) *ItemUpdateOne {
iuo.mutation.SetSerialNumber(s) iuo.mutation.SetSerialNumber(s)
@ -2091,6 +2126,13 @@ func (iuo *ItemUpdateOne) sqlSave(ctx context.Context) (_node *Item, err error)
Column: item.FieldInsured, Column: item.FieldInsured,
}) })
} }
if value, ok := iuo.mutation.Archived(); ok {
_spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{
Type: field.TypeBool,
Value: value,
Column: item.FieldArchived,
})
}
if value, ok := iuo.mutation.SerialNumber(); ok { if value, ok := iuo.mutation.SerialNumber(); ok {
_spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{ _spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{
Type: field.TypeString, Type: field.TypeString,

View file

@ -170,6 +170,7 @@ var (
{Name: "notes", Type: field.TypeString, Nullable: true, Size: 1000}, {Name: "notes", Type: field.TypeString, Nullable: true, Size: 1000},
{Name: "quantity", Type: field.TypeInt, Default: 1}, {Name: "quantity", Type: field.TypeInt, Default: 1},
{Name: "insured", Type: field.TypeBool, Default: false}, {Name: "insured", Type: field.TypeBool, Default: false},
{Name: "archived", Type: field.TypeBool, Default: false},
{Name: "serial_number", Type: field.TypeString, Nullable: true, Size: 255}, {Name: "serial_number", Type: field.TypeString, Nullable: true, Size: 255},
{Name: "model_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}, {Name: "manufacturer", Type: field.TypeString, Nullable: true, Size: 255},
@ -195,19 +196,19 @@ var (
ForeignKeys: []*schema.ForeignKey{ ForeignKeys: []*schema.ForeignKey{
{ {
Symbol: "items_groups_items", Symbol: "items_groups_items",
Columns: []*schema.Column{ItemsColumns[22]}, Columns: []*schema.Column{ItemsColumns[23]},
RefColumns: []*schema.Column{GroupsColumns[0]}, RefColumns: []*schema.Column{GroupsColumns[0]},
OnDelete: schema.Cascade, OnDelete: schema.Cascade,
}, },
{ {
Symbol: "items_items_children", Symbol: "items_items_children",
Columns: []*schema.Column{ItemsColumns[23]}, Columns: []*schema.Column{ItemsColumns[24]},
RefColumns: []*schema.Column{ItemsColumns[0]}, RefColumns: []*schema.Column{ItemsColumns[0]},
OnDelete: schema.SetNull, OnDelete: schema.SetNull,
}, },
{ {
Symbol: "items_locations_items", Symbol: "items_locations_items",
Columns: []*schema.Column{ItemsColumns[24]}, Columns: []*schema.Column{ItemsColumns[25]},
RefColumns: []*schema.Column{LocationsColumns[0]}, RefColumns: []*schema.Column{LocationsColumns[0]},
OnDelete: schema.Cascade, OnDelete: schema.Cascade,
}, },
@ -221,16 +222,21 @@ var (
{ {
Name: "item_manufacturer", Name: "item_manufacturer",
Unique: false, Unique: false,
Columns: []*schema.Column{ItemsColumns[11]}, Columns: []*schema.Column{ItemsColumns[12]},
}, },
{ {
Name: "item_model_number", Name: "item_model_number",
Unique: false, Unique: false,
Columns: []*schema.Column{ItemsColumns[10]}, Columns: []*schema.Column{ItemsColumns[11]},
}, },
{ {
Name: "item_serial_number", Name: "item_serial_number",
Unique: false, Unique: false,
Columns: []*schema.Column{ItemsColumns[10]},
},
{
Name: "item_archived",
Unique: false,
Columns: []*schema.Column{ItemsColumns[9]}, Columns: []*schema.Column{ItemsColumns[9]},
}, },
}, },

View file

@ -4133,6 +4133,7 @@ type ItemMutation struct {
quantity *int quantity *int
addquantity *int addquantity *int
insured *bool insured *bool
archived *bool
serial_number *string serial_number *string
model_number *string model_number *string
manufacturer *string manufacturer *string
@ -4623,6 +4624,42 @@ func (m *ItemMutation) ResetInsured() {
m.insured = nil m.insured = nil
} }
// SetArchived sets the "archived" field.
func (m *ItemMutation) SetArchived(b bool) {
m.archived = &b
}
// Archived returns the value of the "archived" field in the mutation.
func (m *ItemMutation) Archived() (r bool, exists bool) {
v := m.archived
if v == nil {
return
}
return *v, true
}
// OldArchived returns the old "archived" 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) OldArchived(ctx context.Context) (v bool, err error) {
if !m.op.Is(OpUpdateOne) {
return v, errors.New("OldArchived is only allowed on UpdateOne operations")
}
if m.id == nil || m.oldValue == nil {
return v, errors.New("OldArchived requires an ID field in the mutation")
}
oldValue, err := m.oldValue(ctx)
if err != nil {
return v, fmt.Errorf("querying old value for OldArchived: %w", err)
}
return oldValue.Archived, nil
}
// ResetArchived resets all changes to the "archived" field.
func (m *ItemMutation) ResetArchived() {
m.archived = nil
}
// SetSerialNumber sets the "serial_number" field. // SetSerialNumber sets the "serial_number" field.
func (m *ItemMutation) SetSerialNumber(s string) { func (m *ItemMutation) SetSerialNumber(s string) {
m.serial_number = &s m.serial_number = &s
@ -5613,7 +5650,7 @@ func (m *ItemMutation) Type() string {
// order to get all numeric fields that were incremented/decremented, call // order to get all numeric fields that were incremented/decremented, call
// AddedFields(). // AddedFields().
func (m *ItemMutation) Fields() []string { func (m *ItemMutation) Fields() []string {
fields := make([]string, 0, 21) fields := make([]string, 0, 22)
if m.created_at != nil { if m.created_at != nil {
fields = append(fields, item.FieldCreatedAt) fields = append(fields, item.FieldCreatedAt)
} }
@ -5638,6 +5675,9 @@ func (m *ItemMutation) Fields() []string {
if m.insured != nil { if m.insured != nil {
fields = append(fields, item.FieldInsured) fields = append(fields, item.FieldInsured)
} }
if m.archived != nil {
fields = append(fields, item.FieldArchived)
}
if m.serial_number != nil { if m.serial_number != nil {
fields = append(fields, item.FieldSerialNumber) fields = append(fields, item.FieldSerialNumber)
} }
@ -5701,6 +5741,8 @@ func (m *ItemMutation) Field(name string) (ent.Value, bool) {
return m.Quantity() return m.Quantity()
case item.FieldInsured: case item.FieldInsured:
return m.Insured() return m.Insured()
case item.FieldArchived:
return m.Archived()
case item.FieldSerialNumber: case item.FieldSerialNumber:
return m.SerialNumber() return m.SerialNumber()
case item.FieldModelNumber: case item.FieldModelNumber:
@ -5752,6 +5794,8 @@ func (m *ItemMutation) OldField(ctx context.Context, name string) (ent.Value, er
return m.OldQuantity(ctx) return m.OldQuantity(ctx)
case item.FieldInsured: case item.FieldInsured:
return m.OldInsured(ctx) return m.OldInsured(ctx)
case item.FieldArchived:
return m.OldArchived(ctx)
case item.FieldSerialNumber: case item.FieldSerialNumber:
return m.OldSerialNumber(ctx) return m.OldSerialNumber(ctx)
case item.FieldModelNumber: case item.FieldModelNumber:
@ -5843,6 +5887,13 @@ func (m *ItemMutation) SetField(name string, value ent.Value) error {
} }
m.SetInsured(v) m.SetInsured(v)
return nil return nil
case item.FieldArchived:
v, ok := value.(bool)
if !ok {
return fmt.Errorf("unexpected type %T for field %s", value, name)
}
m.SetArchived(v)
return nil
case item.FieldSerialNumber: case item.FieldSerialNumber:
v, ok := value.(string) v, ok := value.(string)
if !ok { if !ok {
@ -6127,6 +6178,9 @@ func (m *ItemMutation) ResetField(name string) error {
case item.FieldInsured: case item.FieldInsured:
m.ResetInsured() m.ResetInsured()
return nil return nil
case item.FieldArchived:
m.ResetArchived()
return nil
case item.FieldSerialNumber: case item.FieldSerialNumber:
m.ResetSerialNumber() m.ResetSerialNumber()
return nil return nil

View file

@ -271,36 +271,40 @@ func init() {
itemDescInsured := itemFields[3].Descriptor() itemDescInsured := itemFields[3].Descriptor()
// item.DefaultInsured holds the default value on creation for the insured field. // item.DefaultInsured holds the default value on creation for the insured field.
item.DefaultInsured = itemDescInsured.Default.(bool) item.DefaultInsured = itemDescInsured.Default.(bool)
// itemDescArchived is the schema descriptor for archived field.
itemDescArchived := itemFields[4].Descriptor()
// item.DefaultArchived holds the default value on creation for the archived field.
item.DefaultArchived = itemDescArchived.Default.(bool)
// itemDescSerialNumber is the schema descriptor for serial_number field. // itemDescSerialNumber is the schema descriptor for serial_number field.
itemDescSerialNumber := itemFields[4].Descriptor() itemDescSerialNumber := itemFields[5].Descriptor()
// item.SerialNumberValidator is a validator for the "serial_number" field. It is called by the builders before save. // 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) item.SerialNumberValidator = itemDescSerialNumber.Validators[0].(func(string) error)
// itemDescModelNumber is the schema descriptor for model_number field. // itemDescModelNumber is the schema descriptor for model_number field.
itemDescModelNumber := itemFields[5].Descriptor() itemDescModelNumber := itemFields[6].Descriptor()
// item.ModelNumberValidator is a validator for the "model_number" field. It is called by the builders before save. // 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) item.ModelNumberValidator = itemDescModelNumber.Validators[0].(func(string) error)
// itemDescManufacturer is the schema descriptor for manufacturer field. // itemDescManufacturer is the schema descriptor for manufacturer field.
itemDescManufacturer := itemFields[6].Descriptor() itemDescManufacturer := itemFields[7].Descriptor()
// item.ManufacturerValidator is a validator for the "manufacturer" field. It is called by the builders before save. // 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) item.ManufacturerValidator = itemDescManufacturer.Validators[0].(func(string) error)
// itemDescLifetimeWarranty is the schema descriptor for lifetime_warranty field. // itemDescLifetimeWarranty is the schema descriptor for lifetime_warranty field.
itemDescLifetimeWarranty := itemFields[7].Descriptor() itemDescLifetimeWarranty := itemFields[8].Descriptor()
// item.DefaultLifetimeWarranty holds the default value on creation for the lifetime_warranty field. // item.DefaultLifetimeWarranty holds the default value on creation for the lifetime_warranty field.
item.DefaultLifetimeWarranty = itemDescLifetimeWarranty.Default.(bool) item.DefaultLifetimeWarranty = itemDescLifetimeWarranty.Default.(bool)
// itemDescWarrantyDetails is the schema descriptor for warranty_details field. // itemDescWarrantyDetails is the schema descriptor for warranty_details field.
itemDescWarrantyDetails := itemFields[9].Descriptor() itemDescWarrantyDetails := itemFields[10].Descriptor()
// item.WarrantyDetailsValidator is a validator for the "warranty_details" field. It is called by the builders before save. // 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) item.WarrantyDetailsValidator = itemDescWarrantyDetails.Validators[0].(func(string) error)
// itemDescPurchasePrice is the schema descriptor for purchase_price field. // itemDescPurchasePrice is the schema descriptor for purchase_price field.
itemDescPurchasePrice := itemFields[12].Descriptor() itemDescPurchasePrice := itemFields[13].Descriptor()
// item.DefaultPurchasePrice holds the default value on creation for the purchase_price field. // item.DefaultPurchasePrice holds the default value on creation for the purchase_price field.
item.DefaultPurchasePrice = itemDescPurchasePrice.Default.(float64) item.DefaultPurchasePrice = itemDescPurchasePrice.Default.(float64)
// itemDescSoldPrice is the schema descriptor for sold_price field. // itemDescSoldPrice is the schema descriptor for sold_price field.
itemDescSoldPrice := itemFields[15].Descriptor() itemDescSoldPrice := itemFields[16].Descriptor()
// item.DefaultSoldPrice holds the default value on creation for the sold_price field. // item.DefaultSoldPrice holds the default value on creation for the sold_price field.
item.DefaultSoldPrice = itemDescSoldPrice.Default.(float64) item.DefaultSoldPrice = itemDescSoldPrice.Default.(float64)
// itemDescSoldNotes is the schema descriptor for sold_notes field. // itemDescSoldNotes is the schema descriptor for sold_notes field.
itemDescSoldNotes := itemFields[16].Descriptor() itemDescSoldNotes := itemFields[17].Descriptor()
// item.SoldNotesValidator is a validator for the "sold_notes" field. It is called by the builders before save. // 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) item.SoldNotesValidator = itemDescSoldNotes.Validators[0].(func(string) error)
// itemDescID is the schema descriptor for id field. // itemDescID is the schema descriptor for id field.

View file

@ -28,6 +28,7 @@ func (Item) Indexes() []ent.Index {
index.Fields("manufacturer"), index.Fields("manufacturer"),
index.Fields("model_number"), index.Fields("model_number"),
index.Fields("serial_number"), index.Fields("serial_number"),
index.Fields("archived"),
} }
} }
@ -45,6 +46,8 @@ func (Item) Fields() []ent.Field {
Default(1), Default(1),
field.Bool("insured"). field.Bool("insured").
Default(false), Default(false),
field.Bool("archived").
Default(false),
// ------------------------------------ // ------------------------------------
// item identification // item identification

View file

@ -0,0 +1,22 @@
-- 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, `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`, `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`, `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`);
-- enable back the enforcement of foreign-keys constraints
PRAGMA foreign_keys = on;

View file

@ -1,5 +1,6 @@
h1:mYTnmyrnBDST/r93NGJM33mIJqhp/U9qR440zI99eqQ= h1:i76VRMDIPdcmQtXTe9bzrgITAzLGjjVy9y8XaXIchAs=
20220929052825_init.sql h1:ZlCqm1wzjDmofeAcSX3jE4h4VcdTNGpRg2eabztDy9Q= 20220929052825_init.sql h1:ZlCqm1wzjDmofeAcSX3jE4h4VcdTNGpRg2eabztDy9Q=
20221001210956_group_invitations.sql h1:YQKJFtE39wFOcRNbZQ/d+ZlHwrcfcsZlcv/pLEYdpjw= 20221001210956_group_invitations.sql h1:YQKJFtE39wFOcRNbZQ/d+ZlHwrcfcsZlcv/pLEYdpjw=
20221009173029_add_user_roles.sql h1:vWmzAfgEWQeGk0Vn70zfVPCcfEZth3E0JcvyKTjpYyU= 20221009173029_add_user_roles.sql h1:vWmzAfgEWQeGk0Vn70zfVPCcfEZth3E0JcvyKTjpYyU=
20221020043305_allow_nesting_types.sql h1:4AyJpZ7l7SSJtJAQETYY802FHJ64ufYPJTqvwdiGn3M= 20221020043305_allow_nesting_types.sql h1:4AyJpZ7l7SSJtJAQETYY802FHJ64ufYPJTqvwdiGn3M=
20221101041931_add_archived_field.sql h1:L2WxiOh1svRn817cNURgqnEQg6DIcodZ1twK4tvxW94=

View file

@ -20,12 +20,13 @@ type ItemsRepository struct {
type ( type (
ItemQuery struct { ItemQuery struct {
Page int Page int
PageSize int PageSize int
Search string `json:"search"` Search string `json:"search"`
LocationIDs []uuid.UUID `json:"locationIds"` LocationIDs []uuid.UUID `json:"locationIds"`
LabelIDs []uuid.UUID `json:"labelIds"` LabelIDs []uuid.UUID `json:"labelIds"`
SortBy string `json:"sortBy"` SortBy string `json:"sortBy"`
IncludeArchived bool `json:"includeArchived"`
} }
ItemField struct { ItemField struct {
@ -55,6 +56,7 @@ type (
Description string `json:"description"` Description string `json:"description"`
Quantity int `json:"quantity"` Quantity int `json:"quantity"`
Insured bool `json:"insured"` Insured bool `json:"insured"`
Archived bool `json:"archived"`
// Edges // Edges
LocationID uuid.UUID `json:"locationId"` LocationID uuid.UUID `json:"locationId"`
@ -93,6 +95,7 @@ type (
Description string `json:"description"` Description string `json:"description"`
Quantity int `json:"quantity"` Quantity int `json:"quantity"`
Insured bool `json:"insured"` Insured bool `json:"insured"`
Archived bool `json:"archived"`
CreatedAt time.Time `json:"createdAt"` CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"` UpdatedAt time.Time `json:"updatedAt"`
@ -157,6 +160,7 @@ func mapItemSummary(item *ent.Item) ItemSummary {
Quantity: item.Quantity, Quantity: item.Quantity,
CreatedAt: item.CreatedAt, CreatedAt: item.CreatedAt,
UpdatedAt: item.UpdatedAt, UpdatedAt: item.UpdatedAt,
Archived: item.Archived,
// Edges // Edges
Location: location, Location: location,
@ -276,7 +280,20 @@ func (e *ItemsRepository) GetOneByGroup(ctx context.Context, gid, id uuid.UUID)
// QueryByGroup returns a list of items that belong to a specific group based on the provided query. // QueryByGroup returns a list of items that belong to a specific group based on the provided query.
func (e *ItemsRepository) QueryByGroup(ctx context.Context, gid uuid.UUID, q ItemQuery) (PaginationResult[ItemSummary], error) { func (e *ItemsRepository) QueryByGroup(ctx context.Context, gid uuid.UUID, q ItemQuery) (PaginationResult[ItemSummary], error) {
qb := e.db.Item.Query().Where(item.HasGroupWith(group.ID(gid))) qb := e.db.Item.Query().Where(
item.HasGroupWith(group.ID(gid)),
)
if q.IncludeArchived {
qb = qb.Where(
item.Or(
item.Archived(true),
item.Archived(false),
),
)
} else {
qb = qb.Where(item.Archived(false))
}
if len(q.LabelIDs) > 0 { if len(q.LabelIDs) > 0 {
labels := make([]predicate.Item, 0, len(q.LabelIDs)) labels := make([]predicate.Item, 0, len(q.LabelIDs))
@ -384,6 +401,7 @@ func (e *ItemsRepository) UpdateByGroup(ctx context.Context, gid uuid.UUID, data
SetSerialNumber(data.SerialNumber). SetSerialNumber(data.SerialNumber).
SetModelNumber(data.ModelNumber). SetModelNumber(data.ModelNumber).
SetManufacturer(data.Manufacturer). SetManufacturer(data.Manufacturer).
SetArchived(data.Archived).
SetPurchaseTime(data.PurchaseTime). SetPurchaseTime(data.PurchaseTime).
SetPurchaseFrom(data.PurchaseFrom). SetPurchaseFrom(data.PurchaseFrom).
SetPurchasePrice(data.PurchasePrice). SetPurchasePrice(data.PurchasePrice).

View file

@ -7,6 +7,7 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
"github.com/hay-kot/homebox/backend/internal/data/ent" "github.com/hay-kot/homebox/backend/internal/data/ent"
"github.com/hay-kot/homebox/backend/internal/data/ent/group" "github.com/hay-kot/homebox/backend/internal/data/ent/group"
"github.com/hay-kot/homebox/backend/internal/data/ent/item"
"github.com/hay-kot/homebox/backend/internal/data/ent/label" "github.com/hay-kot/homebox/backend/internal/data/ent/label"
"github.com/hay-kot/homebox/backend/internal/data/ent/predicate" "github.com/hay-kot/homebox/backend/internal/data/ent/predicate"
) )
@ -68,7 +69,9 @@ func (r *LabelRepository) getOne(ctx context.Context, where ...predicate.Label)
return mapLabelOutErr(r.db.Label.Query(). return mapLabelOutErr(r.db.Label.Query().
Where(where...). Where(where...).
WithGroup(). WithGroup().
WithItems(). WithItems(func(iq *ent.ItemQuery) {
iq.Where(item.Archived(false))
}).
Only(ctx), Only(ctx),
) )
} }

View file

@ -7,6 +7,7 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
"github.com/hay-kot/homebox/backend/internal/data/ent" "github.com/hay-kot/homebox/backend/internal/data/ent"
"github.com/hay-kot/homebox/backend/internal/data/ent/group" "github.com/hay-kot/homebox/backend/internal/data/ent/group"
"github.com/hay-kot/homebox/backend/internal/data/ent/item"
"github.com/hay-kot/homebox/backend/internal/data/ent/location" "github.com/hay-kot/homebox/backend/internal/data/ent/location"
"github.com/hay-kot/homebox/backend/internal/data/ent/predicate" "github.com/hay-kot/homebox/backend/internal/data/ent/predicate"
) )
@ -105,6 +106,7 @@ func (r *LocationRepository) GetAll(ctx context.Context, groupId uuid.UUID) ([]L
items items
WHERE WHERE
items.location_items = locations.id items.location_items = locations.id
AND items.archived = false
) as item_count ) as item_count
FROM FROM
locations locations
@ -139,7 +141,7 @@ func (r *LocationRepository) getOne(ctx context.Context, where ...predicate.Loca
Where(where...). Where(where...).
WithGroup(). WithGroup().
WithItems(func(iq *ent.ItemQuery) { WithItems(func(iq *ent.ItemQuery) {
iq.WithLabel() iq.Where(item.Archived(false)).WithLabel()
}). }).
WithParent(). WithParent().
WithChildren(). WithChildren().

View file

@ -2,7 +2,7 @@
<div v-if="!inline" class="form-control w-full"> <div v-if="!inline" class="form-control w-full">
<label class="label cursor-pointer"> <label class="label cursor-pointer">
<span class="label-text"> {{ label }}</span> <span class="label-text"> {{ label }}</span>
<input v-model="value" type="checkbox" class="checkbox" /> <input v-model="value" type="checkbox" class="checkbox checkbox-primary" />
</label> </label>
</div> </div>
<div v-else class="label cursor-pointer sm:grid sm:grid-cols-4 sm:items-start sm:gap-4"> <div v-else class="label cursor-pointer sm:grid sm:grid-cols-4 sm:items-start sm:gap-4">
@ -11,7 +11,7 @@
{{ label }} {{ label }}
</span> </span>
</label> </label>
<input v-model="value" type="checkbox" class="checkbox" /> <input v-model="value" type="checkbox" class="checkbox checkbox-primary" />
</div> </div>
</template> </template>

View file

@ -7,6 +7,7 @@
<h2 class="card-title"> <h2 class="card-title">
<Icon name="mdi-package-variant" /> <Icon name="mdi-package-variant" />
{{ item.name }} {{ item.name }}
<Icon v-if="item.archived" class="ml-auto" name="mdi-archive-outline" />
</h2> </h2>
<p>{{ description }}</p> <p>{{ description }}</p>
<div class="flex gap-2 flex-wrap justify-end"> <div class="flex gap-2 flex-wrap justify-end">

View file

@ -0,0 +1,3 @@
<template>
<div class="grow-1 max-w-full"></div>
</template>

View file

@ -10,13 +10,19 @@ export function useItemSearch(client: UserClient, opts?: SearchOptions) {
const locations = ref<LocationSummary[]>([]); const locations = ref<LocationSummary[]>([]);
const labels = ref<LabelSummary[]>([]); const labels = ref<LabelSummary[]>([]);
const results = ref<ItemSummary[]>([]); const results = ref<ItemSummary[]>([]);
const includeArchived = ref(false);
watchDebounced(query, search, { debounce: 250, maxWait: 1000 }); watchDebounced(query, search, { debounce: 250, maxWait: 1000 });
async function search() { async function search() {
const locIds = locations.value.map(l => l.id); const locIds = locations.value.map(l => l.id);
const labelIds = labels.value.map(l => l.id); const labelIds = labels.value.map(l => l.id);
const { data, error } = await client.items.getAll({ q: query.value, locations: locIds, labels: labelIds }); const { data, error } = await client.items.getAll({
q: query.value,
locations: locIds,
labels: labelIds,
includeArchived: includeArchived.value,
});
if (error) { if (error) {
return; return;
} }

View file

@ -11,6 +11,7 @@ import {
import { AttachmentTypes, PaginationResult } from "../types/non-generated"; import { AttachmentTypes, PaginationResult } from "../types/non-generated";
export type ItemsQuery = { export type ItemsQuery = {
includeArchived?: boolean;
page?: number; page?: number;
pageSize?: number; pageSize?: number;
locations?: string[]; locations?: string[];

View file

@ -63,6 +63,7 @@ export interface ItemField {
} }
export interface ItemOut { export interface ItemOut {
archived: boolean;
attachments: ItemAttachment[]; attachments: ItemAttachment[];
children: ItemSummary[]; children: ItemSummary[];
createdAt: Date; createdAt: Date;
@ -107,6 +108,7 @@ export interface ItemOut {
} }
export interface ItemSummary { export interface ItemSummary {
archived: boolean;
createdAt: Date; createdAt: Date;
description: string; description: string;
id: string; id: string;
@ -121,6 +123,7 @@ export interface ItemSummary {
} }
export interface ItemUpdate { export interface ItemUpdate {
archived: boolean;
description: string; description: string;
fields: ItemField[]; fields: ItemField[];
id: string; id: string;

View file

@ -115,6 +115,11 @@
label: "Insured", label: "Insured",
ref: "insured", ref: "insured",
}, },
{
type: "checkbox",
label: "Archived",
ref: "archived",
},
]; ];
const purchaseFields: FormField[] = [ const purchaseFields: FormField[] = [

View file

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { Detail, Details } from "~~/components/global/DetailsSection/types"; import { CustomDetail, Detail, Details } from "~~/components/global/DetailsSection/types";
import { ItemAttachment } from "~~/lib/api/types/data-contracts"; import { ItemAttachment } from "~~/lib/api/types/data-contracts";
definePageMeta({ definePageMeta({
@ -70,7 +70,7 @@
); );
}); });
const itemDetails = computed(() => { const itemDetails = computed<Details>(() => {
return [ return [
{ {
name: "Description", name: "Description",
@ -107,11 +107,11 @@
const url = maybeUrl(field.textValue); const url = maybeUrl(field.textValue);
if (url.isUrl) { if (url.isUrl) {
return { return {
type: "link",
name: field.name, name: field.name,
text: url.text, text: url.text,
type: "link",
href: url.url, href: url.url,
}; } as CustomDetail;
} }
return { return {

View file

@ -23,7 +23,12 @@
const locations = selectedLocations.value.map(l => l.id); const locations = selectedLocations.value.map(l => l.id);
const labels = selectedLabels.value.map(l => l.id); const labels = selectedLabels.value.map(l => l.id);
const { data, error } = await api.items.getAll({ q: query.value, locations, labels }); const { data, error } = await api.items.getAll({
q: query.value,
locations,
labels,
includeArchived: includeArchived.value,
});
if (error) { if (error) {
loading.value = false; loading.value = false;
return; return;
@ -46,6 +51,7 @@
const advanced = ref(false); const advanced = ref(false);
const selectedLocations = ref([]); const selectedLocations = ref([]);
const selectedLabels = ref([]); const selectedLabels = ref([]);
const includeArchived = ref(false);
watchEffect(() => { watchEffect(() => {
if (!advanced.value) { if (!advanced.value) {
@ -57,6 +63,7 @@
watchDebounced(query, search, { debounce: 250, maxWait: 1000 }); watchDebounced(query, search, { debounce: 250, maxWait: 1000 });
watchDebounced(selectedLocations, search, { debounce: 250, maxWait: 1000 }); watchDebounced(selectedLocations, search, { debounce: 250, maxWait: 1000 });
watchDebounced(selectedLabels, search, { debounce: 250, maxWait: 1000 }); watchDebounced(selectedLabels, search, { debounce: 250, maxWait: 1000 });
watch(includeArchived, search);
</script> </script>
<template> <template>
@ -77,6 +84,13 @@
<div class="px-4 pb-4"> <div class="px-4 pb-4">
<FormMultiselect v-model="selectedLabels" label="Labels" :items="labels ?? []" /> <FormMultiselect v-model="selectedLabels" label="Labels" :items="labels ?? []" />
<FormMultiselect v-model="selectedLocations" label="Locations" :items="locations ?? []" /> <FormMultiselect v-model="selectedLocations" label="Locations" :items="locations ?? []" />
<div class="flex pb-2 pt-5">
<label class="label cursor-pointer mr-auto">
<input v-model="includeArchived" type="checkbox" class="toggle toggle-primary" />
<span class="label-text ml-4"> Include Archived Items </span>
</label>
<Spacer />
</div>
</div> </div>
</BaseCard> </BaseCard>
<section class="mt-10"> <section class="mt-10">