From 1cfaa1f9bcccce0dc9a3d299f301b7b296a18720 Mon Sep 17 00:00:00 2001 From: Brett Bender Date: Sun, 21 Jan 2024 01:02:17 -0600 Subject: [PATCH] feat: edit menu --- internal/api/api.go | 18 + internal/types/api.go | 5 + internal/types/assets.go | 4 +- internal/types/categories.go | 4 + internal/types/shelves.go | 7 + internal/ui/assets/assets.go | 18 +- internal/ui/assets/astdialogs/create.go | 152 +++++- internal/ui/assets/astdialogs/edit.go | 695 ++++++++++++++++++++++++ internal/ui/assets/command.go | 60 +- internal/ui/assets/key.go | 12 + internal/ui/style/style.go | 2 +- 11 files changed, 961 insertions(+), 16 deletions(-) create mode 100644 internal/ui/assets/astdialogs/edit.go diff --git a/internal/api/api.go b/internal/api/api.go index 235fda3..71bf2ff 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -78,6 +78,24 @@ func (c *APIClient) CreateAsset(request types.CreateAssetRequest) (*types.Asset, return resp.Asset, nil } +func (c *APIClient) UpdateAsset(id string, request types.CreateAssetRequest) (*types.Asset, error) { + url := fmt.Sprintf("%s/assets/%s", c.Host, id) + + body, err := json.Marshal(request) + if err != nil { + return nil, err + } + + req, _ := http.NewRequest("PUT", url, bytes.NewBuffer(body)) + + resp, err := makeRequest[types.AssetResponse](c, req) + if err != nil { + return nil, err + } + + return resp.Asset, nil +} + func (c *APIClient) RetrieveAllAssets() ([]*types.Asset, error) { url := fmt.Sprintf("%s/assets", c.Host) req, _ := http.NewRequest("GET", url, bytes.NewBuffer([]byte{})) diff --git a/internal/types/api.go b/internal/types/api.go index 0f77121..13d8887 100644 --- a/internal/types/api.go +++ b/internal/types/api.go @@ -21,3 +21,8 @@ type APIError struct { Messages []string `json:"messages"` } + +type CountResponse struct { + *Response + Count int64 `json:"count"` +} diff --git a/internal/types/assets.go b/internal/types/assets.go index ebec4ae..bc5c106 100644 --- a/internal/types/assets.go +++ b/internal/types/assets.go @@ -23,8 +23,8 @@ type CreateAssetRequest struct { ModelName string `json:"model_name,omitempty"` Price float64 `json:"price,omitempty"` Comments string `json:"comments,omitempty"` - ShelfLocationID int `json:"shelf_location_id,omitempty"` - CategoryID int `json:"category_id,omitempty"` + ShelfLocationID *uint64 `json:"shelf_location_id,omitempty"` + CategoryID *uint64 `json:"category_id,omitempty"` } type Asset struct { diff --git a/internal/types/categories.go b/internal/types/categories.go index 9655574..e72b1ce 100644 --- a/internal/types/categories.go +++ b/internal/types/categories.go @@ -20,3 +20,7 @@ type MultipleCategoryResponse struct { Categories []*Category `json:"categories"` Total int64 `json:"total"` } + +type CreateCategoryRequest struct { + Name string `json:"name"` +} diff --git a/internal/types/shelves.go b/internal/types/shelves.go index 4a70a64..85116df 100644 --- a/internal/types/shelves.go +++ b/internal/types/shelves.go @@ -23,3 +23,10 @@ type ShelfLocation struct { UpdatedAt time.Time `json:"updated_at"` DeletedAt *time.Time `json:"deleted_at,omitempty"` } + +type CreateShelfRequest struct { + Name string `json:"name"` + RoomNumber string `json:"room_number,omitempty"` + Description string `json:"description,omitempty"` + BuildingID uint64 `json:"building_id"` +} diff --git a/internal/ui/assets/assets.go b/internal/ui/assets/assets.go index 178bdaa..2c0d3d9 100644 --- a/internal/ui/assets/assets.go +++ b/internal/ui/assets/assets.go @@ -1,9 +1,10 @@ package assets import ( - "git.brettb.xyz/goinv/client/internal/ui/assets/astdialogs" "sync" + "git.brettb.xyz/goinv/client/internal/ui/assets/astdialogs" + "git.brettb.xyz/goinv/client/internal/api" "git.brettb.xyz/goinv/client/internal/types" "git.brettb.xyz/goinv/client/internal/ui/dialogs" @@ -34,6 +35,7 @@ type Assets struct { progressDialog *dialogs.ProgressDialog messageDialog *dialogs.MessageDialog createDialog *astdialogs.AssetCreateDialog + editDialog *astdialogs.AssetEditDialog allDialogs []dialogs.Dialog confirmData string assetListFunc func() ([]types.Asset, error) @@ -74,6 +76,8 @@ func NewAssets(logger *zap.Logger, client *api.APIClient) *Assets { errorDialog: dialogs.NewErrorDialog(logger), progressDialog: dialogs.NewProgressDialog(logger), messageDialog: dialogs.NewMessageDialog(logger, ""), + createDialog: astdialogs.NewAssetCreateDialog(logger, client), + editDialog: astdialogs.NewAssetEditDialog(logger, client), } assets.assetTable.SetBackgroundColor(style.BgColor) @@ -95,7 +99,7 @@ func NewAssets(logger *zap.Logger, client *api.APIClient) *Assets { assets.cmdDialog = dialogs.NewCommandDialog(logger, [][]string{ {"create asset", "create a new asset"}, - {"view asset", "view the selected asset"}, + {"edit asset", "edit the selected asset"}, {"delete asset", "delete the selected asset"}, {"refresh", "refresh the page"}, }) @@ -125,6 +129,14 @@ func NewAssets(logger *zap.Logger, client *api.APIClient) *Assets { assets.createDialog.Hide() }).SetCreateFunc(func() { assets.createDialog.Hide() + assets.create() + }) + + assets.editDialog.SetCancelFunc(func() { + assets.editDialog.Hide() + }).SetEditFunc(func() { + assets.editDialog.Hide() + assets.edit() }) assets.SetAssetListFunc(func() ([]types.Asset, error) { @@ -160,6 +172,8 @@ func NewAssets(logger *zap.Logger, client *api.APIClient) *Assets { assets.messageDialog, assets.progressDialog, assets.confirmDialog, + assets.createDialog, + assets.editDialog, assets.cmdDialog, } diff --git a/internal/ui/assets/astdialogs/create.go b/internal/ui/assets/astdialogs/create.go index a1ad6e0..be136cb 100644 --- a/internal/ui/assets/astdialogs/create.go +++ b/internal/ui/assets/astdialogs/create.go @@ -2,6 +2,9 @@ package astdialogs import ( "fmt" + "strconv" + "strings" + "git.brettb.xyz/goinv/client/internal/api" "git.brettb.xyz/goinv/client/internal/types" "git.brettb.xyz/goinv/client/internal/ui/dialogs" @@ -10,12 +13,11 @@ import ( "github.com/gdamore/tcell/v2" "github.com/rivo/tview" "go.uber.org/zap" - "strings" ) const ( assetCreateDialogMaxWidth = 100 - assetCreateDialogHeight = 10 + assetCreateDialogHeight = 17 ) const ( @@ -172,6 +174,13 @@ func (d *AssetCreateDialog) Focus(delegate func(tview.Primitive)) { return nil } + if event.Key() == tcell.KeyBacktab { + d.focusElement = createAssetFormFocus + d.Focus(delegate) + + return nil + } + event = utils.ParseKeyEventKey(d.logger, event) if event.Key() == tcell.KeyDown { d.nextCategory() @@ -206,6 +215,10 @@ func (d *AssetCreateDialog) InputHandler() func(*tcell.EventKey, func(tview.Prim d.setGeneralInfoNextFocus() } + if event.Key() == tcell.KeyBacktab { + d.setGeneralInfoPrevFocus() + } + handler(event, setFocus) return @@ -218,6 +231,10 @@ func (d *AssetCreateDialog) InputHandler() func(*tcell.EventKey, func(tview.Prim d.setLocationNextFocus() } + if event.Key() == tcell.KeyBacktab { + d.setLocationPrevFocus() + } + handler(event, setFocus) return @@ -230,6 +247,10 @@ func (d *AssetCreateDialog) InputHandler() func(*tcell.EventKey, func(tview.Prim d.setCommentsNextFocus() } + if event.Key() == tcell.KeyBacktab { + d.setCommentsPrevFocus() + } + handler(event, setFocus) return @@ -300,6 +321,10 @@ func (d *AssetCreateDialog) SetCreateFunc(f func()) *AssetCreateDialog { func (d *AssetCreateDialog) SetCancelFunc(f func()) *AssetCreateDialog { d.cancelHandler = f + cancelButton := d.form.GetButton(d.form.GetButtonCount() - 2) + + cancelButton.SetSelectedFunc(f) + return d } @@ -324,9 +349,11 @@ func (d *AssetCreateDialog) initData() { categoryOptions := []string{""} for _, category := range d.categoryList { - categoryOptions = append(shelfOptions, fmt.Sprintf("%s", category.Name)) + categoryOptions = append(categoryOptions, fmt.Sprintf("%s", category.Name)) } + d.setActiveCategory(0) + // General category d.assetNameField.SetText("") d.assetQuantityField.SetText("") @@ -347,11 +374,13 @@ func (d *AssetCreateDialog) initData() { func (d *AssetCreateDialog) setupLayout() { bgColor := style.DialogBgColor + fgColor := style.DialogFgColor d.categories.SetDynamicColors(true). SetWrap(true). SetTextAlign(tview.AlignLeft) - d.categories.SetBackgroundColor(bgColor) + d.categories.SetBackgroundColor(fgColor) + d.categories.SetTextColor(bgColor) d.categories.SetBorder(true) d.categories.SetBorderColor(style.DialogSubBoxBorderColor) @@ -383,6 +412,7 @@ func (d *AssetCreateDialog) setupLayout() { d.layout.SetBorder(true) d.layout.SetBorderColor(style.DialogBorderColor) d.layout.SetTitle("ASSET CREATE") + d.layout.SetTitleColor(style.DialogFgColor) _, layoutWidth := utils.AlignStringListWidth(d.createCategoryLabels) layout := tview.NewFlex().SetDirection(tview.FlexColumn) @@ -390,6 +420,7 @@ func (d *AssetCreateDialog) setupLayout() { layout.AddItem(d.categories, layoutWidth+6, 0, true) layout.AddItem(d.categoryPages, 0, 1, true) layout.SetBackgroundColor(bgColor) + d.layout.SetDirection(tview.FlexRow) d.layout.AddItem(layout, 0, 1, true) d.layout.AddItem(d.form, dialogs.DialogFormHeight, 0, true) @@ -398,7 +429,7 @@ func (d *AssetCreateDialog) setupLayout() { func (d *AssetCreateDialog) setupGeneralInfoPageUI() { bgColor := style.DialogBgColor inputFieldBgColor := style.InputFieldBgColor - pageLabelWidth := 12 + pageLabelWidth := 14 d.assetNameField.SetLabel("name:") d.assetNameField.SetLabelWidth(pageLabelWidth) @@ -495,8 +526,8 @@ func (d *AssetCreateDialog) setupCommentsPageUI() { } func (d *AssetCreateDialog) setActiveCategory(idx int) { - fgColor := style.DialogFgColor - bgBolor := style.ButtonBgColor + fgColor := style.ButtonSelectedFgColor + bgBolor := style.ButtonSelectedBgColor ctgTextColor := style.GetColorHex(fgColor) ctgBgColor := style.GetColorHex(bgBolor) @@ -598,6 +629,44 @@ func (d *AssetCreateDialog) setGeneralInfoNextFocus() { d.focusElement = createAssetFormFocus } +func (d *AssetCreateDialog) setGeneralInfoPrevFocus() { + if d.assetNameField.HasFocus() { + d.focusElement = createCategoriesFocus + + return + } + + if d.assetQuantityField.HasFocus() { + d.focusElement = createAssetNameFieldFocus + + return + } + + if d.assetLengthField.HasFocus() { + d.focusElement = createAssetQuantityFieldFocus + + return + } + + if d.assetManufacturerField.HasFocus() { + d.focusElement = createAssetLengthFieldFocus + + return + } + + if d.assetModelField.HasFocus() { + d.focusElement = createAssetManufacturerFieldFocus + + return + } + + if d.assetPriceField.HasFocus() { + d.focusElement = createAssetModelFieldFocus + } + + d.focusElement = createAssetPriceFieldFocus +} + func (d *AssetCreateDialog) setLocationNextFocus() { // Category -> Shelf if d.assetCategoryField.HasFocus() { @@ -609,6 +678,73 @@ func (d *AssetCreateDialog) setLocationNextFocus() { d.focusElement = createAssetFormFocus } +func (d *AssetCreateDialog) setLocationPrevFocus() { + if d.assetCategoryField.HasFocus() { + d.focusElement = createCategoriesFocus + + return + } + + if d.assetShelfField.HasFocus() { + d.focusElement = createAssetCategoryFieldFocus + + return + } + + d.focusElement = createAssetShelfFieldFocus +} + func (d *AssetCreateDialog) setCommentsNextFocus() { d.focusElement = createAssetFormFocus -} \ No newline at end of file +} + +func (d *AssetCreateDialog) setCommentsPrevFocus() { + d.focusElement = createCategoriesFocus +} + +func (d *AssetCreateDialog) CreateAssetOptions() types.CreateAssetRequest { + var ( + quantity int + categoryID *uint64 + shelfID *uint64 + price float64 + ) + + var err error + + selectedCategoryIndex, _ := d.assetCategoryField.GetCurrentOption() + if len(d.categoryList) > 0 && selectedCategoryIndex > 0 { + categoryID = &d.categoryList[selectedCategoryIndex-1].ID + } + + selectedShelfIndex, _ := d.assetShelfField.GetCurrentOption() + if len(d.shelfList) > 0 && selectedShelfIndex > 0 { + shelfID = &d.shelfList[selectedShelfIndex-1].ID + } + + quantityStr := d.assetQuantityField.GetText() + quantity, err = strconv.Atoi(quantityStr) + if err != nil { + quantity = 0 + } + + priceStr := d.assetPriceField.GetText() + price, err = strconv.ParseFloat(priceStr, 64) + if err != nil { + price = 0.0 + } + + req := types.CreateAssetRequest{ + Name: d.assetNameField.GetText(), + Quantity: quantity, + Length: d.assetLengthField.GetText(), + Manufacturer: d.assetManufacturerField.GetText(), + ModelName: d.assetModelField.GetText(), + Price: price, + Comments: d.assetCommentsArea.GetText(), + ShelfLocationID: shelfID, + CategoryID: categoryID, + } + + return req +} diff --git a/internal/ui/assets/astdialogs/edit.go b/internal/ui/assets/astdialogs/edit.go new file mode 100644 index 0000000..5567ee3 --- /dev/null +++ b/internal/ui/assets/astdialogs/edit.go @@ -0,0 +1,695 @@ +package astdialogs + +import ( + "fmt" + "slices" + "strconv" + "strings" + + "git.brettb.xyz/goinv/client/internal/api" + "git.brettb.xyz/goinv/client/internal/types" + "git.brettb.xyz/goinv/client/internal/ui/dialogs" + "git.brettb.xyz/goinv/client/internal/ui/style" + "git.brettb.xyz/goinv/client/internal/ui/utils" + "github.com/gdamore/tcell/v2" + "github.com/rivo/tview" + "go.uber.org/zap" +) + +const ( + assetEditDialogMaxWidth = 100 + assetEditDialogHeight = 17 +) + +const ( + editAssetFormFocus = 0 + iota + editCategoriesFocus + editCategoryPagesFocus + editAssetNameFieldFocus + editAssetQuantityFieldFocus + editAssetLengthFieldFocus + editAssetManufacturerFieldFocus + editAssetModelFieldFocus + editAssetPriceFieldFocus + editAssetCategoryFieldFocus + editAssetShelfFieldFocus + editAssetCommentFieldFocus +) + +type AssetEditDialog struct { + *tview.Box + layout *tview.Flex + editCategoryLabels []string + categories *tview.TextView + categoryPages *tview.Pages + generalInfoPage *tview.Flex + locationPage *tview.Flex + commentsPage *tview.Flex + form *tview.Form + + display bool + activePageIndex int + focusElement int + logger *zap.Logger + client *api.APIClient + asset *types.Asset + + categoryList []*types.Category + shelfList []*types.ShelfLocation + + assetNameField *tview.InputField + assetQuantityField *tview.InputField + assetLengthField *tview.InputField + assetManufacturerField *tview.InputField + assetModelField *tview.InputField + assetPriceField *tview.InputField + assetCategoryField *tview.DropDown + assetShelfField *tview.DropDown + assetCommentsArea *tview.TextArea + + focusMap map[int]tview.Primitive + + cancelHandler func() + editHandler func() +} + +func NewAssetEditDialog(logger *zap.Logger, client *api.APIClient) *AssetEditDialog { + editDialog := AssetEditDialog{ + Box: tview.NewBox(), + layout: tview.NewFlex(), + editCategoryLabels: []string{ + "General", + "Location", + "Comments", + }, + categories: tview.NewTextView(), + categoryPages: tview.NewPages(), + generalInfoPage: tview.NewFlex(), + locationPage: tview.NewFlex(), + commentsPage: tview.NewFlex(), + form: tview.NewForm(), + display: false, + activePageIndex: 0, + logger: logger, + client: client, + assetNameField: tview.NewInputField(), + assetQuantityField: tview.NewInputField(), + assetLengthField: tview.NewInputField(), + assetManufacturerField: tview.NewInputField(), + assetModelField: tview.NewInputField(), + assetPriceField: tview.NewInputField(), + assetCategoryField: tview.NewDropDown(), + assetShelfField: tview.NewDropDown(), + assetCommentsArea: tview.NewTextArea(), + } + + editDialog.focusMap = map[int]tview.Primitive{ + editAssetNameFieldFocus: editDialog.assetNameField, + editAssetQuantityFieldFocus: editDialog.assetQuantityField, + editAssetLengthFieldFocus: editDialog.assetLengthField, + editAssetManufacturerFieldFocus: editDialog.assetManufacturerField, + editAssetModelFieldFocus: editDialog.assetModelField, + editAssetPriceFieldFocus: editDialog.assetPriceField, + editAssetCategoryFieldFocus: editDialog.assetCategoryField, + editAssetShelfFieldFocus: editDialog.assetShelfField, + editAssetCommentFieldFocus: editDialog.assetCommentsArea, + } + + editDialog.setupLayout() + editDialog.setActiveCategory(0) + editDialog.initCustomInputHandlers() + + return &editDialog +} + +func (d *AssetEditDialog) Display() { + d.display = true + d.initData() + d.focusElement = editCategoryPagesFocus +} + +func (d *AssetEditDialog) IsDisplay() bool { + return d.display +} + +func (d *AssetEditDialog) Hide() { + d.display = false +} + +func (d *AssetEditDialog) HasFocus() bool { + return utils.CheckFocus(d.categories, d.categoryPages, d.Box, d.form) +} + +func (d *AssetEditDialog) Focus(delegate func(tview.Primitive)) { + switch d.focusElement { + case editAssetFormFocus: + button := d.form.GetButton(d.form.GetButtonCount() - 1) + button.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { + if event.Key() == utils.SwitchFocusKey.EventKey() { + d.focusElement = editCategoriesFocus + + d.Focus(delegate) + d.form.SetFocus(0) + + return nil + } + + if event.Key() == tcell.KeyEnter { + return nil + } + + return event + }) + delegate(d.form) + case editCategoriesFocus: + d.categories.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { + if event.Key() == utils.SwitchFocusKey.EventKey() { + d.focusElement = editCategoryPagesFocus + d.Focus(delegate) + + return nil + } + + event = utils.ParseKeyEventKey(d.logger, event) + if event.Key() == tcell.KeyDown { + d.nextCategory() + } + if event.Key() == tcell.KeyUp { + d.previousCategory() + } + + return event + }) + delegate(d.categories) + case editCategoryPagesFocus: + delegate(d.categoryPages) + default: + delegate(d.focusMap[d.focusElement]) + } +} + +func (d *AssetEditDialog) InputHandler() func(*tcell.EventKey, func(tview.Primitive)) { + return d.WrapInputHandler(func(event *tcell.EventKey, setFocus func(tview.Primitive)) { + d.logger.Sugar().Debugf("asset edit dialog event %v received", event) + + if event.Key() == utils.CloseDialogKey.EventKey() && !d.dropdownHasFocus() { + d.cancelHandler() + + return + } + + if d.generalInfoPage.HasFocus() { + if handler := d.generalInfoPage.InputHandler(); handler != nil { + if event.Key() == utils.SwitchFocusKey.EventKey() { + d.setGeneralInfoNextFocus() + } + + handler(event, setFocus) + + return + } + } + + if d.locationPage.HasFocus() { + if handler := d.locationPage.InputHandler(); handler != nil { + if event.Key() == utils.SwitchFocusKey.EventKey() { + d.setLocationNextFocus() + } + + handler(event, setFocus) + + return + } + } + + if d.commentsPage.HasFocus() { + if handler := d.commentsPage.InputHandler(); handler != nil { + if event.Key() == utils.SwitchFocusKey.EventKey() { + d.setCommentsNextFocus() + } + + handler(event, setFocus) + + return + } + } + + if d.categories.HasFocus() { + if categoryHandler := d.categories.InputHandler(); categoryHandler != nil { + categoryHandler(event, setFocus) + + return + } + } + + if d.form.HasFocus() { + if formHandler := d.form.InputHandler(); formHandler != nil { + if event.Key() == tcell.KeyEnter { + enterButton := d.form.GetButton(d.form.GetButtonCount() - 1) + if enterButton.HasFocus() { + d.editHandler() + } + } + + formHandler(event, setFocus) + + return + } + } + }) +} + +func (d *AssetEditDialog) SetRect(x, y, width, height int) { + if width > assetEditDialogMaxWidth { + emptySpace := (width - assetEditDialogMaxWidth) / 2 + x += emptySpace + width = assetEditDialogMaxWidth + } + + if height > assetEditDialogHeight { + emptySpace := (height - assetEditDialogHeight) / 2 + y += emptySpace + height = assetEditDialogHeight + } + + d.Box.SetRect(x, y, width, height) +} + +func (d *AssetEditDialog) Draw(screen tcell.Screen) { + if !d.display { + return + } + + d.Box.DrawForSubclass(screen, d) + x, y, width, height := d.Box.GetInnerRect() + + d.layout.SetRect(x, y, width, height) + d.layout.Draw(screen) +} + +func (d *AssetEditDialog) SetEditFunc(f func()) *AssetEditDialog { + d.editHandler = f + enterButton := d.form.GetButton(d.form.GetButtonCount() - 1) + + enterButton.SetSelectedFunc(f) + + return d +} + +func (d *AssetEditDialog) SetCancelFunc(f func()) *AssetEditDialog { + d.cancelHandler = f + cancelButton := d.form.GetButton(d.form.GetButtonCount() - 2) + + cancelButton.SetSelectedFunc(f) + + return d +} + +func (d *AssetEditDialog) dropdownHasFocus() bool { + return utils.CheckFocus(d.assetCategoryField, d.assetShelfField) +} + +func (d *AssetEditDialog) SetAsset(a *types.Asset) { + d.asset = a +} + +func (d *AssetEditDialog) initData() { + + // Get available shelves + shelfList, _ := d.client.RetrieveAllShelves() + d.shelfList = shelfList + + shelfOptions := []string{""} + for _, shelf := range d.shelfList { + shelfOptions = append(shelfOptions, shelf.Name) // In case we want to do custom formatting later + } + + // Get available categories + categoryList, _ := d.client.RetrieveAllCategories() + d.categoryList = categoryList + + categoryOptions := []string{""} + for _, category := range d.categoryList { + categoryOptions = append(categoryOptions, category.Name) + } + + d.setActiveCategory(0) + + // General category + if d.asset == nil { + d.assetNameField.SetText("") + d.assetQuantityField.SetText("") + d.assetLengthField.SetText("") + d.assetManufacturerField.SetText("") + d.assetModelField.SetText("") + d.assetPriceField.SetText("") + + // Location category + d.assetShelfField.SetOptions(shelfOptions, nil) + d.assetShelfField.SetCurrentOption(0) + d.assetCategoryField.SetOptions(categoryOptions, nil) + d.assetCategoryField.SetCurrentOption(0) + + // Comments category + d.assetCommentsArea.SetText("", true) + } else { + d.assetNameField.SetText(d.asset.Name) + d.assetQuantityField.SetText(fmt.Sprintf("%d", d.asset.Quantity)) + d.assetLengthField.SetText(d.asset.Length) + d.assetManufacturerField.SetText(d.asset.Manufacturer) + d.assetModelField.SetText(d.asset.ModelName) + d.assetPriceField.SetText(fmt.Sprintf("%.2f", d.asset.Price)) + + // Location category + d.assetShelfField.SetOptions(shelfOptions, nil) + d.assetShelfField.SetCurrentOption(findOption(shelfOptions, d.asset.ShelfLocation.Name)) + d.assetCategoryField.SetOptions(categoryOptions, nil) + d.assetCategoryField.SetCurrentOption(findOption(categoryOptions, d.asset.Category.Name)) + + // Comments category + d.assetCommentsArea.SetText("", true) + } +} + +func findOption(options []string, search string) int { + return slices.Index(options, search) +} + +func (d *AssetEditDialog) setupLayout() { + bgColor := style.DialogBgColor + fgColor := style.DialogFgColor + + d.categories.SetDynamicColors(true). + SetWrap(true). + SetTextAlign(tview.AlignLeft) + d.categories.SetBackgroundColor(fgColor) + d.categories.SetTextColor(bgColor) + d.categories.SetBorder(true) + d.categories.SetBorderColor(style.DialogSubBoxBorderColor) + + d.categoryPages.SetBackgroundColor(bgColor) + d.categoryPages.SetBorder(true) + d.categoryPages.SetBorderColor(style.DialogSubBoxBorderColor) + + d.setupGeneralInfoPageUI() + d.setupLocationPageUI() + d.setupCommentsPageUI() + + activatedStyle := tcell.StyleDefault. + Background(style.ButtonSelectedBgColor). + Foreground(style.ButtonSelectedFgColor) + + d.form.SetBackgroundColor(bgColor) + d.form.AddButton("Cancel", nil) + d.form.AddButton("Edit", nil) + d.form.SetButtonsAlign(tview.AlignRight) + d.form.SetButtonBackgroundColor(style.ButtonBgColor) + d.form.SetButtonTextColor(style.ButtonFgColor) + d.form.SetButtonActivatedStyle(activatedStyle) + + d.categoryPages.AddPage(d.editCategoryLabels[generalPageIndex], d.generalInfoPage, true, true) + d.categoryPages.AddPage(d.editCategoryLabels[locationPageIndex], d.locationPage, true, true) + d.categoryPages.AddPage(d.editCategoryLabels[commentPageIndex], d.commentsPage, true, true) + + d.layout.SetBackgroundColor(bgColor) + d.layout.SetBorder(true) + d.layout.SetBorderColor(style.DialogBorderColor) + d.layout.SetTitle("ASSET EDIT") + d.layout.SetTitleColor(style.DialogFgColor) + + _, layoutWidth := utils.AlignStringListWidth(d.editCategoryLabels) + layout := tview.NewFlex().SetDirection(tview.FlexColumn) + + layout.AddItem(d.categories, layoutWidth+6, 0, true) + layout.AddItem(d.categoryPages, 0, 1, true) + layout.SetBackgroundColor(bgColor) + d.layout.SetDirection(tview.FlexRow) + d.layout.AddItem(layout, 0, 1, true) + + d.layout.AddItem(d.form, dialogs.DialogFormHeight, 0, true) +} + +func (d *AssetEditDialog) setupGeneralInfoPageUI() { + bgColor := style.DialogBgColor + inputFieldBgColor := style.InputFieldBgColor + pageLabelWidth := 12 + + d.assetNameField.SetLabel("name:") + d.assetNameField.SetLabelWidth(pageLabelWidth) + d.assetNameField.SetLabelColor(bgColor) + d.assetNameField.SetBackgroundColor(style.DialogFgColor) + d.assetNameField.SetFieldBackgroundColor(inputFieldBgColor) + + d.assetQuantityField.SetLabel("quantity:") + d.assetQuantityField.SetLabelWidth(pageLabelWidth) + d.assetQuantityField.SetLabelColor(bgColor) + d.assetQuantityField.SetBackgroundColor(style.DialogFgColor) + d.assetQuantityField.SetFieldBackgroundColor(inputFieldBgColor) + + d.assetLengthField.SetLabel("length:") + d.assetLengthField.SetLabelWidth(pageLabelWidth) + d.assetLengthField.SetLabelColor(bgColor) + d.assetLengthField.SetBackgroundColor(style.DialogFgColor) + d.assetLengthField.SetFieldBackgroundColor(inputFieldBgColor) + + d.assetManufacturerField.SetLabel("manufacturer:") + d.assetManufacturerField.SetLabelWidth(pageLabelWidth) + d.assetManufacturerField.SetLabelColor(bgColor) + d.assetManufacturerField.SetBackgroundColor(style.DialogFgColor) + d.assetManufacturerField.SetFieldBackgroundColor(inputFieldBgColor) + + d.assetModelField.SetLabel("model:") + d.assetModelField.SetLabelWidth(pageLabelWidth) + d.assetModelField.SetLabelColor(bgColor) + d.assetModelField.SetBackgroundColor(style.DialogFgColor) + d.assetModelField.SetFieldBackgroundColor(inputFieldBgColor) + + d.assetPriceField.SetLabel("price:") + d.assetPriceField.SetLabelWidth(pageLabelWidth) + d.assetPriceField.SetLabelColor(bgColor) + d.assetPriceField.SetBackgroundColor(style.DialogFgColor) + d.assetPriceField.SetFieldBackgroundColor(inputFieldBgColor) + + d.generalInfoPage.SetDirection(tview.FlexRow) + d.generalInfoPage.AddItem(d.assetNameField, 1, 0, true) + d.generalInfoPage.AddItem(utils.EmptyBoxSpace(bgColor), 1, 0, true) + d.generalInfoPage.AddItem(d.assetQuantityField, 1, 0, true) + d.generalInfoPage.AddItem(utils.EmptyBoxSpace(bgColor), 1, 0, true) + d.generalInfoPage.AddItem(d.assetLengthField, 1, 0, true) + d.generalInfoPage.AddItem(utils.EmptyBoxSpace(bgColor), 1, 0, true) + d.generalInfoPage.AddItem(d.assetManufacturerField, 1, 0, true) + d.generalInfoPage.AddItem(utils.EmptyBoxSpace(bgColor), 1, 0, true) + d.generalInfoPage.AddItem(d.assetModelField, 1, 0, true) + d.generalInfoPage.AddItem(utils.EmptyBoxSpace(bgColor), 1, 0, true) + d.generalInfoPage.AddItem(d.assetPriceField, 1, 0, true) + d.generalInfoPage.AddItem(utils.EmptyBoxSpace(bgColor), 1, 0, true) +} + +func (d *AssetEditDialog) setupLocationPageUI() { + bgColor := style.DialogBgColor + inputFieldBgColor := style.InputFieldBgColor + pageLabelWidth := 12 + ddUnselectedStyle := style.DropdownUnselected + ddSelectedStyle := style.DropdownSelected + + d.assetCategoryField.SetLabel("category:") + d.assetCategoryField.SetLabelWidth(pageLabelWidth) + d.assetCategoryField.SetBackgroundColor(bgColor) + d.assetCategoryField.SetLabelColor(style.DialogFgColor) + d.assetCategoryField.SetListStyles(ddUnselectedStyle, ddSelectedStyle) + d.assetCategoryField.SetFieldBackgroundColor(inputFieldBgColor) + + d.assetShelfField.SetLabel("shelf:") + d.assetShelfField.SetLabelWidth(pageLabelWidth) + d.assetShelfField.SetBackgroundColor(bgColor) + d.assetShelfField.SetLabelColor(style.DialogFgColor) + d.assetShelfField.SetListStyles(ddUnselectedStyle, ddSelectedStyle) + d.assetShelfField.SetFieldBackgroundColor(inputFieldBgColor) + + d.locationPage.SetDirection(tview.FlexRow) + d.locationPage.AddItem(d.assetCategoryField, 1, 0, true) + d.locationPage.AddItem(utils.EmptyBoxSpace(bgColor), 1, 0, true) + d.locationPage.AddItem(d.assetShelfField, 1, 0, true) + d.locationPage.SetBackgroundColor(bgColor) +} + +func (d *AssetEditDialog) setupCommentsPageUI() { + bgColor := style.DialogBgColor + inputFieldBgColor := style.InputFieldBgColor + pageLabelWidth := 12 + + d.assetCommentsArea.SetLabel("comments:") + d.assetCommentsArea.SetLabelWidth(pageLabelWidth) + d.assetCommentsArea.SetBackgroundColor(inputFieldBgColor) + d.assetCommentsArea.SetLabelStyle(tcell.StyleDefault.Foreground(style.DialogFgColor).Background(bgColor)) + d.assetCommentsArea.SetWrap(true) + + d.commentsPage.AddItem(d.assetCommentsArea, 0, 1, true) + d.commentsPage.SetBackgroundColor(bgColor) +} + +func (d *AssetEditDialog) setActiveCategory(idx int) { + fgColor := style.ButtonSelectedFgColor + bgBolor := style.ButtonSelectedBgColor + ctgTextColor := style.GetColorHex(fgColor) + ctgBgColor := style.GetColorHex(bgBolor) + + d.activePageIndex = idx + + d.categories.Clear() + + alignedList, _ := utils.AlignStringListWidth(d.editCategoryLabels) + + var ctgList []string + + for i, lbl := range alignedList { + if i == idx { + ctgList = append(ctgList, fmt.Sprintf("[%s:%s:b]-> %s ", ctgTextColor, ctgBgColor, lbl)) + + continue + } + + ctgList = append(ctgList, fmt.Sprintf("[-:-:-] %s ", lbl)) + } + + d.categories.SetText(strings.Join(ctgList, "\n")) + + d.categoryPages.SwitchToPage(d.editCategoryLabels[idx]) +} + +func (d *AssetEditDialog) nextCategory() { + activePage := d.activePageIndex + if d.activePageIndex < len(d.editCategoryLabels)-1 { + activePage++ + + d.setActiveCategory(activePage) + + return + } + + d.setActiveCategory(0) +} + +func (d *AssetEditDialog) previousCategory() { + activePage := d.activePageIndex + if d.activePageIndex > 0 { + activePage-- + + d.setActiveCategory(activePage) + + return + } + + d.setActiveCategory(len(d.editCategoryLabels) - 1) +} + +func (d *AssetEditDialog) initCustomInputHandlers() { + d.assetShelfField.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { + event = utils.ParseKeyEventKey(d.logger, event) + + return event + }) + + d.assetCategoryField.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { + event = utils.ParseKeyEventKey(d.logger, event) + + return event + }) +} + +func (d *AssetEditDialog) setGeneralInfoNextFocus() { + // Name -> Quantity -> Length -> Manufacturer -> Model -> Price + if d.assetNameField.HasFocus() { + d.focusElement = editAssetQuantityFieldFocus + + return + } + + if d.assetQuantityField.HasFocus() { + d.focusElement = editAssetLengthFieldFocus + + return + } + + if d.assetLengthField.HasFocus() { + d.focusElement = editAssetManufacturerFieldFocus + + return + } + + if d.assetManufacturerField.HasFocus() { + d.focusElement = editAssetModelFieldFocus + + return + } + + if d.assetModelField.HasFocus() { + d.focusElement = editAssetPriceFieldFocus + + return + } + + d.focusElement = editAssetFormFocus +} + +func (d *AssetEditDialog) setLocationNextFocus() { + // Category -> Shelf + if d.assetCategoryField.HasFocus() { + d.focusElement = editAssetShelfFieldFocus + + return + } + + d.focusElement = editAssetFormFocus +} + +func (d *AssetEditDialog) setCommentsNextFocus() { + d.focusElement = editAssetFormFocus +} + +func (d *AssetEditDialog) EditAssetOptions() types.CreateAssetRequest { + var ( + quantity int + categoryID *uint64 + shelfID *uint64 + price float64 + ) + + var err error + + selectedCategoryIndex, _ := d.assetCategoryField.GetCurrentOption() + if len(d.categoryList) > 0 && selectedCategoryIndex > 0 { + categoryID = &d.categoryList[selectedCategoryIndex-1].ID + } + + selectedShelfIndex, _ := d.assetShelfField.GetCurrentOption() + if len(d.shelfList) > 0 && selectedShelfIndex > 0 { + shelfID = &d.shelfList[selectedShelfIndex-1].ID + } + + quantityStr := d.assetQuantityField.GetText() + quantity, err = strconv.Atoi(quantityStr) + if err != nil { + quantity = 0 + } + + priceStr := d.assetPriceField.GetText() + price, err = strconv.ParseFloat(priceStr, 64) + if err != nil { + price = 0.0 + } + + req := types.CreateAssetRequest{ + Name: d.assetNameField.GetText(), + Quantity: quantity, + Length: d.assetLengthField.GetText(), + Manufacturer: d.assetManufacturerField.GetText(), + ModelName: d.assetModelField.GetText(), + Price: price, + Comments: d.assetCommentsArea.GetText(), + ShelfLocationID: shelfID, + CategoryID: categoryID, + } + + return req +} diff --git a/internal/ui/assets/command.go b/internal/ui/assets/command.go index 8e52913..3a689a4 100644 --- a/internal/ui/assets/command.go +++ b/internal/ui/assets/command.go @@ -11,8 +11,8 @@ func (a *Assets) runCommand(cmd string) { switch cmd { case "create asset": a.createDialog.Display() - case "view asset": - a.cNotImplemented() + case "edit asset": + a.cedit() case "delete asset": a.cdelete() case "refresh": @@ -48,6 +48,41 @@ func (a *Assets) cdelete() { a.confirmDialog.Display() } +func (a *Assets) cedit() { + selectedItem := a.getSelectedItem() + if selectedItem == nil { + a.displayError("DELETE ASSET ERROR", fmt.Errorf("no assets to edit")) + return + } + + asset, err := a.client.RetrieveAssetByID(selectedItem.id) + if err != nil { + a.displayError("DELETE ASSET ERROR", fmt.Errorf("unable to retrieve asset from server")) + return + } + + a.editDialog.SetAsset(asset) + a.editDialog.Display() +} + +func (a *Assets) edit() { + selectedItem := a.getSelectedItem() + createReq := a.editDialog.EditAssetOptions() + if createReq.Name == "" { + a.displayError("ASSET EDIT ERROR", fmt.Errorf("asset name cannot be empty")) + return + } + + _, err := a.client.UpdateAsset(selectedItem.id, createReq) + if err != nil { + a.displayError("ASSET EDIT ERROR", err) + + return + } + + a.crefresh() +} + func (a *Assets) delete() { selectedItem := a.getSelectedItem() @@ -87,7 +122,7 @@ func (a *Assets) crefresh() { a.progressDialog.Hide() if !a.errorDialog.IsDisplay() { - a.messageDialog.SetTitle(fmt.Sprintf("asset refresh")) + a.messageDialog.SetTitle("asset refresh") a.messageDialog.SetText(dialogs.MessageGeneric, "Refreshed!", "Successfully refreshed page.") a.messageDialog.Display() } @@ -96,6 +131,25 @@ func (a *Assets) crefresh() { ref() } +func (a *Assets) create() { + createReq := a.createDialog.CreateAssetOptions() + if createReq.Name == "" { + a.displayError("ASSET CREATE ERROR", fmt.Errorf("asset name cannot be empty")) + return + } + + _, err := a.client.CreateAsset(createReq) + if err != nil { + a.displayError("ASSET CREATE ERROR", err) + + return + } + + a.crefresh() + a.assetTable.ScrollToEnd() + a.assetTable.Select(a.assetTable.GetRowCount(), 0) +} + func (a *Assets) displayError(title string, err error) { a.errorDialog.SetTitle(title) a.errorDialog.SetText(fmt.Sprintf("%v", err)) diff --git a/internal/ui/assets/key.go b/internal/ui/assets/key.go index ef62e92..4bc719c 100644 --- a/internal/ui/assets/key.go +++ b/internal/ui/assets/key.go @@ -33,6 +33,18 @@ func (a *Assets) InputHandler() func(*tcell.EventKey, func(tview.Primitive)) { } } + if a.createDialog.HasFocus() { + if createHandler := a.createDialog.InputHandler(); createHandler != nil { + createHandler(event, setFocus) + } + } + + if a.editDialog.HasFocus() { + if editHandler := a.editDialog.InputHandler(); editHandler != nil { + editHandler(event, setFocus) + } + } + if a.cmdDialog.HasFocus() { if cmdHandler := a.cmdDialog.InputHandler(); cmdHandler != nil { cmdHandler(event, setFocus) diff --git a/internal/ui/style/style.go b/internal/ui/style/style.go index c078540..ab26aab 100644 --- a/internal/ui/style/style.go +++ b/internal/ui/style/style.go @@ -19,7 +19,7 @@ var ( // dialogs DialogBgColor = tcell.ColorLightBlue DialogFgColor = tcell.ColorBlack - DialogBorderColor = tcell.ColorLightBlue + DialogBorderColor = tcell.ColorWhite DialogSubBoxBgColor = tcell.ColorWhite DialogSubBoxFgColor = tcell.ColorBlack DialogSubBoxBorderColor = tcell.ColorLightBlue