diff --git a/internal/api/api.go b/internal/api/api.go index 71bf2ff..a3a1ebd 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -132,6 +132,66 @@ func (c *APIClient) RetrieveAllShelves() ([]*types.ShelfLocation, error) { return resp.ShelfLocations, nil } +func (c *APIClient) DeleteShelfByID(idNumber string) (*types.ShelfLocation, error) { + url := fmt.Sprintf("%s/shelves/%s", c.Host, idNumber) + req, _ := http.NewRequest("DELETE", url, bytes.NewBuffer([]byte{})) + + resp, err := makeRequest[types.ShelfResponse](c, req) + if err != nil { + return nil, err + } + + return resp.ShelfLocation, nil +} + +func (c *APIClient) CreateShelf(request types.CreateShelfRequest) (*types.ShelfLocation, error) { + url := fmt.Sprintf("%s/shelves", c.Host) + + body, err := json.Marshal(request) + if err != nil { + return nil, err + } + + req, _ := http.NewRequest("POST", url, bytes.NewBuffer(body)) + + resp, err := makeRequest[types.ShelfResponse](c, req) + if err != nil { + return nil, err + } + + return resp.ShelfLocation, nil +} + +func (c *APIClient) UpdateShelf(id string, request types.CreateShelfRequest) (*types.ShelfLocation, error) { + url := fmt.Sprintf("%s/shelves/%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.ShelfResponse](c, req) + if err != nil { + return nil, err + } + + return resp.ShelfLocation, nil +} + +func (c *APIClient) RetrieveShelfByID(idNumber string) (*types.ShelfLocation, error) { + url := fmt.Sprintf("%s/shelves/%s", c.Host, idNumber) + req, _ := http.NewRequest("GET", url, bytes.NewBuffer([]byte{})) + + resp, err := makeRequest[types.ShelfResponse](c, req) + if err != nil { + return nil, err + } + + return resp.ShelfLocation, nil +} + func (c *APIClient) RetrieveAllCategories() ([]*types.Category, error) { url := fmt.Sprintf("%s/categories", c.Host) req, _ := http.NewRequest("GET", url, bytes.NewBuffer([]byte{})) @@ -143,3 +203,15 @@ func (c *APIClient) RetrieveAllCategories() ([]*types.Category, error) { return resp.Categories, nil } + +func (c *APIClient) RetrieveAllBuildings() ([]*types.Building, error) { + url := fmt.Sprintf("%s/buildings", c.Host) + req, _ := http.NewRequest("GET", url, bytes.NewBuffer([]byte{})) + + resp, err := makeRequest[types.MultipleBuildingResponse](c, req) + if err != nil { + return nil, err + } + + return resp.Buildings, nil +} diff --git a/internal/app/app.go b/internal/app/app.go index cf8ddad..a124e9b 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -6,6 +6,7 @@ import ( "git.brettb.xyz/goinv/client/internal/api" "git.brettb.xyz/goinv/client/internal/ui/assets" "git.brettb.xyz/goinv/client/internal/ui/help" + "git.brettb.xyz/goinv/client/internal/ui/shelves" "git.brettb.xyz/goinv/client/internal/ui/utils" "github.com/gdamore/tcell/v2" "github.com/rivo/tview" @@ -18,9 +19,9 @@ type App struct { pages *tview.Pages logger *zap.Logger - help *help.Help - assets *assets.Assets - // shelves *shelves.Shelves + help *help.Help + assets *assets.Assets + shelves *shelves.Shelves // categories *categories.Categories // buildings *buildings.Buildings menu *tview.TextView @@ -48,7 +49,7 @@ func NewApp(name, version, host string, logger *zap.Logger) *App { menuItems := [][]string{ {utils.HelpScreenKey.Label(), app.help.GetTitle()}, {utils.AssetsScreenKey.Label(), app.assets.GetTitle()}, - //{utils.ShelvesScreenKey.Label(), app.shelves.GetTitle()}, + {utils.ShelvesScreenKey.Label(), app.shelves.GetTitle()}, //{utils.CategoriesScreenKey.Label(), app.categories.GetTitle()}, //{utils.BuildingsScreenKey.Label(), app.buildings.GetTitle()}, } @@ -57,6 +58,7 @@ func NewApp(name, version, host string, logger *zap.Logger) *App { app.pages.AddPage(app.help.GetTitle(), app.help, true, false) app.pages.AddPage(app.assets.GetTitle(), app.assets, true, false) + app.pages.AddPage(app.shelves.GetTitle(), app.shelves, true, false) return &app } @@ -98,6 +100,9 @@ func (app *App) Run() error { case utils.AssetsScreenKey.EventKey(): app.switchToScreen(app.assets.GetTitle()) return nil + case utils.ShelvesScreenKey.EventKey(): + app.switchToScreen(app.shelves.GetTitle()) + return nil } } diff --git a/internal/app/init.go b/internal/app/init.go index 5ae6293..c63ff6e 100644 --- a/internal/app/init.go +++ b/internal/app/init.go @@ -5,4 +5,8 @@ func (app *App) initUI() { // Assets page app.assets.UpdateShelfData() app.assets.UpdateAssetData() + + // Shelves page + app.shelves.UpdateShelfData() + app.shelves.UpdateBuildingData() } diff --git a/internal/types/buildings.go b/internal/types/buildings.go index 30d8198..084ee79 100644 --- a/internal/types/buildings.go +++ b/internal/types/buildings.go @@ -9,3 +9,9 @@ type Building struct { UpdatedAt time.Time `json:"updated_at"` DeletedAt *time.Time `json:"deleted_at,omitempty"` } + +type MultipleBuildingResponse struct { + *Response + Buildings []*Building `json:"buildings"` + Total int `json:"total"` +} diff --git a/internal/types/shelves.go b/internal/types/shelves.go index 85116df..081c35d 100644 --- a/internal/types/shelves.go +++ b/internal/types/shelves.go @@ -19,14 +19,15 @@ type ShelfLocation struct { RoomNumber string `json:"room_number,omitempty"` Description string `json:"description,omitempty"` BuildingID *uint64 `json:"building_id"` + Building *Building `json:"-"` CreatedAt time.Time `json:"created_at"` 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"` + 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/astdialogs/edit.go b/internal/ui/assets/astdialogs/edit.go index 3017dda..4fd48f6 100644 --- a/internal/ui/assets/astdialogs/edit.go +++ b/internal/ui/assets/astdialogs/edit.go @@ -394,7 +394,7 @@ func (d *AssetEditDialog) initData() { } // Comments category - d.assetCommentsArea.SetText("", true) + d.assetCommentsArea.SetText(d.asset.Comments, true) } } @@ -773,7 +773,7 @@ func (d *AssetEditDialog) EditAssetOptions() types.CreateAssetRequest { Manufacturer: d.assetManufacturerField.GetText(), ModelName: d.assetModelField.GetText(), Price: price, - Comments: d.assetCommentsArea.GetText(), + Comments: strings.TrimSpace(d.assetCommentsArea.GetText()), ShelfLocationID: shelfID, CategoryID: categoryID, } diff --git a/internal/ui/shelves/command.go b/internal/ui/shelves/command.go new file mode 100644 index 0000000..260b86d --- /dev/null +++ b/internal/ui/shelves/command.go @@ -0,0 +1,156 @@ +package shelves + +import ( + "fmt" + + "git.brettb.xyz/goinv/client/internal/ui/dialogs" + "git.brettb.xyz/goinv/client/internal/ui/style" +) + +func (a *Shelves) runCommand(cmd string) { + switch cmd { + case "create shelf": + a.createDialog.Display() + case "edit shelf": + a.cedit() + case "delete shelf": + a.cdelete() + case "refresh": + a.crefresh() + } +} + +func (a *Shelves) cNotImplemented() { + a.displayError("not implemented", fmt.Errorf("this command has not been implemented")) +} + +// Confirm deletion +func (a *Shelves) cdelete() { + selectedItem := a.getSelectedItem() + + // Empty table + if selectedItem == nil { + a.displayError("DELETE SHELF ERROR", fmt.Errorf("no shelfs to delete")) + return + } + + title := "delete shelf" + a.confirmDialog.SetTitle(title) + a.confirmData = status_CONFIRM_DELETE_SHELF + bgColor := style.GetColorHex(style.DialogSubBoxBorderColor) + fgColor := style.GetColorHex(style.DialogFgColor) + + shelfName := fmt.Sprintf("[%s:%s:b]SHELF NAME:[:-:-] %s", fgColor, bgColor, selectedItem.name) + + confirmMsg := fmt.Sprintf("%s\n%s\nAre you sure you want to delete the selected shelf ?", shelfName) + a.confirmDialog.SetText(confirmMsg) + a.confirmDialog.Display() +} + +func (a *Shelves) cedit() { + selectedItem := a.getSelectedItem() + if selectedItem == nil { + a.displayError("DELETE SHELF ERROR", fmt.Errorf("no shelves to edit")) + return + } + + shelf, err := a.client.RetrieveShelfByID(selectedItem.id) + if err != nil { + a.displayError("DELETE SHELF ERROR", fmt.Errorf("unable to retrieve shelf from server")) + return + } + + a.editDialog.SetShelf(shelf) + a.editDialog.Display() +} + +func (a *Shelves) edit() { + selectedItem := a.getSelectedItem() + createReq := a.editDialog.EditShelfOptions() + if createReq.Name == "" { + a.displayError("SHELF EDIT ERROR", fmt.Errorf("shelf name cannot be empty")) + return + } + + _, err := a.client.UpdateShelf(selectedItem.id, createReq) + if err != nil { + a.displayError("SHELF EDIT ERROR", err) + + return + } + + a.crefresh() +} + +func (a *Shelves) delete() { + selectedItem := a.getSelectedItem() + + a.progressDialog.SetTitle(fmt.Sprintf("deleting shelf %s", selectedItem.id)) + a.progressDialog.Display() + + del := func() { + _, err := a.client.DeleteShelfByID(selectedItem.id) + + a.progressDialog.Hide() + + if err != nil { + a.displayError("DELETE SHELF ERROR", err) + + return + } + + // display success message + a.messageDialog.SetTitle(fmt.Sprintf("deleting shelf %s", selectedItem.id)) + a.messageDialog.SetText(dialogs.MessageGeneric, "Success!", fmt.Sprintf("Shelf %s successfully deleted.", selectedItem.id)) + a.messageDialog.Display() + + a.UpdateShelfData() + } + + del() +} + +func (a *Shelves) crefresh() { + a.progressDialog.SetTitle("refreshing shelfs") + a.progressDialog.Display() + + ref := func() { + a.UpdateShelfData() + a.UpdateShelfData() + + a.progressDialog.Hide() + + if !a.errorDialog.IsDisplay() { + a.messageDialog.SetTitle("shelf refresh") + a.messageDialog.SetText(dialogs.MessageGeneric, "Refreshed!", "Successfully refreshed page.") + a.messageDialog.Display() + } + } + + ref() +} + +func (a *Shelves) create() { + createReq := a.createDialog.CreateShelfOptions() + if createReq.Name == "" { + a.displayError("SHELF CREATE ERROR", fmt.Errorf("shelf name cannot be empty")) + return + } + + _, err := a.client.CreateShelf(createReq) + if err != nil { + a.displayError("SHELF CREATE ERROR", err) + + return + } + + a.crefresh() + a.shelfTable.ScrollToEnd() + a.shelfTable.Select(a.shelfTable.GetRowCount(), 0) +} + +func (a *Shelves) displayError(title string, err error) { + a.errorDialog.SetTitle(title) + a.errorDialog.SetText(fmt.Sprintf("%v", err)) + a.errorDialog.Display() +} diff --git a/internal/ui/shelves/data.go b/internal/ui/shelves/data.go new file mode 100644 index 0000000..19246f2 --- /dev/null +++ b/internal/ui/shelves/data.go @@ -0,0 +1,48 @@ +package shelves + +import "git.brettb.xyz/goinv/client/internal/types" + +func (a *Shelves) UpdateData() { + a.UpdateShelfData() + a.UpdateBuildingData() +} + +func (a *Shelves) UpdateShelfData() { + shelves, err := a.shelfListFunc() + a.shelfList.mu.Lock() + defer a.shelfList.mu.Unlock() + if err != nil { + a.displayError("could not retrieve shelves", err) + a.shelfList.dirty = true + return + } + a.shelfList.dirty = false + a.shelfList.report = shelves +} + +func (a *Shelves) UpdateBuildingData() { + buildings, err := a.buildingListFunc() + if err != nil { + a.displayError("could not retrieve buildings", err) + return + } + a.buildingCache.mu.Lock() + a.buildingCache.report = buildings + a.buildingCache.mu.Unlock() +} + +func (a *Shelves) getShelfData() []types.ShelfLocation { + a.shelfList.mu.Lock() + shelfReport := a.shelfList.report + defer a.shelfList.mu.Unlock() + + return shelfReport +} + +func (a *Shelves) getBuildingData() map[uint64]types.Building { + a.buildingCache.mu.Lock() + buildingReport := a.buildingCache.report + defer a.buildingCache.mu.Unlock() + + return buildingReport +} diff --git a/internal/ui/shelves/draw.go b/internal/ui/shelves/draw.go new file mode 100644 index 0000000..7b2cd42 --- /dev/null +++ b/internal/ui/shelves/draw.go @@ -0,0 +1,24 @@ +package shelves + +import "github.com/gdamore/tcell/v2" + +func (a *Shelves) Draw(screen tcell.Screen) { + a.refresh() + a.Box.DrawForSubclass(screen, a) + + x, y, width, height := a.GetInnerRect() + + a.shelfTable.SetRect(x, y, width, height) + a.shelfTable.Draw(screen) + + x, y, width, height = a.shelfTable.GetInnerRect() + + for _, diag := range a.allDialogs { + if diag.IsDisplay() { + diag.SetRect(x, y, width, height) + diag.Draw(screen) + + return + } + } +} diff --git a/internal/ui/shelves/key.go b/internal/ui/shelves/key.go new file mode 100644 index 0000000..db3afda --- /dev/null +++ b/internal/ui/shelves/key.go @@ -0,0 +1,66 @@ +package shelves + +import ( + "git.brettb.xyz/goinv/client/internal/ui/utils" + "github.com/gdamore/tcell/v2" + "github.com/rivo/tview" +) + +func (a *Shelves) InputHandler() func(*tcell.EventKey, func(tview.Primitive)) { + return a.WrapInputHandler(func(event *tcell.EventKey, setFocus func(tview.Primitive)) { + a.logger.Sugar().Debugf("assets event %v received", event) + + if a.progressDialog.IsDisplay() { + setFocus(a.progressDialog) + return + } + + if a.errorDialog.HasFocus() { + if errorHandler := a.errorDialog.InputHandler(); errorHandler != nil { + errorHandler(event, setFocus) + } + } + + if a.messageDialog.HasFocus() { + if messageHandler := a.messageDialog.InputHandler(); messageHandler != nil { + messageHandler(event, setFocus) + } + } + + if a.confirmDialog.HasFocus() { + if confirmHandler := a.confirmDialog.InputHandler(); confirmHandler != nil { + confirmHandler(event, setFocus) + } + } + + 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) + } + } + + if a.shelfTable.HasFocus() { + if event.Rune() == utils.CommandMenuKey.Rune() { + a.cmdDialog.Display() + } else { + if tableHandler := a.shelfTable.InputHandler(); tableHandler != nil { + tableHandler(event, setFocus) + } + } + } + + setFocus(a) + }) +} diff --git a/internal/ui/shelves/refresh.go b/internal/ui/shelves/refresh.go new file mode 100644 index 0000000..bcdcc56 --- /dev/null +++ b/internal/ui/shelves/refresh.go @@ -0,0 +1,72 @@ +package shelves + +import ( + "fmt" + "strings" + + "git.brettb.xyz/goinv/client/internal/ui/style" + "github.com/rivo/tview" +) + +const tableHeaderOffset = 1 + +func (a *Shelves) refresh() { + shelfs := a.getShelfData() + a.shelfTable.Clear() + a.updateShelfTableTitle(len(shelfs)) + + a.writeTableHeaders() + + for i, shelf := range shelfs { + row := i + tableHeaderOffset + + // ID, Name, Building, Room + + a.shelfTable.SetCell(row, 0, + tview.NewTableCell(fmt.Sprintf("%d", shelf.ID)). + SetExpansion(a.shelfTableExpansions[0]). + SetAlign(tview.AlignLeft)) + + a.shelfTable.SetCell(row, 1, + tview.NewTableCell(shelf.Name). + SetExpansion(a.shelfTableExpansions[1]). + SetAlign(tview.AlignLeft)) + + building := "" + if shelf.Building != nil { + building = shelf.Building.Name + } + + a.shelfTable.SetCell(row, 2, + tview.NewTableCell(building). + SetExpansion(a.shelfTableExpansions[2]). + SetAlign(tview.AlignLeft)) + + a.shelfTable.SetCell(row, 2, + tview.NewTableCell(shelf.RoomNumber). + SetExpansion(a.shelfTableExpansions[3]). + SetAlign(tview.AlignLeft)) + } +} + +func (a *Shelves) updateShelfTableTitle(count int) { + dirtyFlag := "" + if a.shelfList.dirty { + dirtyFlag = "*" + } + title := fmt.Sprintf("[::b]SHELVES [%s%d]", dirtyFlag, count) + a.shelfTable.SetTitle(title) +} + +func (a *Shelves) writeTableHeaders() { + for i, headerText := range a.shelfTableHeaders { + header := fmt.Sprintf("[::b]%s", strings.ToUpper(headerText)) + a.shelfTable.SetCell(0, i, + tview.NewTableCell(header). + SetExpansion(a.shelfTableExpansions[i]). + SetBackgroundColor(style.TableHeaderBgColor). + SetTextColor(style.TableHeaderFgColor). + SetAlign(tview.AlignLeft). + SetSelectable(false)) + } +} diff --git a/internal/ui/shelves/shelves.go b/internal/ui/shelves/shelves.go new file mode 100644 index 0000000..7c80b87 --- /dev/null +++ b/internal/ui/shelves/shelves.go @@ -0,0 +1,218 @@ +package shelves + +import ( + "git.brettb.xyz/goinv/client/internal/ui/shelves/shlvdialogs" + "sync" + + "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 ( + status_CONFIRM_DELETE_SHELF = "delete_shelf" +) + +type Shelves struct { + *tview.Box + client *api.APIClient + title string + logger *zap.Logger + shelfTable *tview.Table + shelfList shelfListReport + buildingCache buildingListReport + shelfTableHeaders []string + shelfTableExpansions []int + cmdDialog *dialogs.CommandDialog + confirmDialog *dialogs.ConfirmDialog + errorDialog *dialogs.ErrorDialog + progressDialog *dialogs.ProgressDialog + messageDialog *dialogs.MessageDialog + createDialog *shlvdialogs.ShelfCreateDialog + editDialog *shlvdialogs.ShelfEditDialog + allDialogs []dialogs.Dialog + confirmData string + shelfListFunc func() ([]types.ShelfLocation, error) + buildingListFunc func() (map[uint64]types.Building, error) +} + +type shelfSelectedItem struct { + id string + name string + building string + roomNumber string +} + +type shelfListReport struct { + mu sync.Mutex + report []types.ShelfLocation + dirty bool +} + +type buildingListReport struct { + mu sync.Mutex + report map[uint64]types.Building + dirty bool +} + +func NewShelves(logger *zap.Logger, client *api.APIClient) *Shelves { + shelves := &Shelves{ + Box: tview.NewBox(), + client: client, + title: "shelves", + logger: logger, + shelfTable: tview.NewTable(), + shelfTableHeaders: []string{"id", "name", "building", "room number"}, + shelfTableExpansions: []int{1, 4, 2, 2}, + confirmDialog: dialogs.NewConfirmDialog(logger), + errorDialog: dialogs.NewErrorDialog(logger), + progressDialog: dialogs.NewProgressDialog(logger), + messageDialog: dialogs.NewMessageDialog(logger, ""), + createDialog: shlvdialogs.NewShelfCreateDialog(logger, client), + editDialog: shlvdialogs.NewShelfEditDialog(logger, client), + } + + shelves.shelfTable.SetBackgroundColor(style.BgColor) + shelves.shelfTable.SetBorder(true) + shelves.updateShelfTableTitle(0) + shelves.shelfTable.SetTitleColor(style.FgColor) + shelves.shelfTable.SetBorderColor(style.BorderColor) + shelves.shelfTable.SetFixed(1, 1) + shelves.shelfTable.SetSelectable(true, false) + + shelves.writeTableHeaders() + + shelves.shelfTable.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { + if shelves.shelfTable.GetRowCount() <= 1 { + return nil + } + return event + }) + + shelves.cmdDialog = dialogs.NewCommandDialog(logger, [][]string{ + {"create shelf", "create a new shelf"}, + {"edit shelf", "edit the selected shelf"}, + {"delete shelf", "delete the selected shelf"}, + {"refresh", "refresh the page"}, + }) + + shelves.cmdDialog.SetSelectedFunc(func() { + shelves.cmdDialog.Hide() + shelves.runCommand(shelves.cmdDialog.GetSelectedItem()) + }).SetCancelFunc(func() { + shelves.cmdDialog.Hide() + }) + + shelves.confirmDialog.SetSelectedFunc(func() { + shelves.confirmDialog.Hide() + switch shelves.confirmData { + case status_CONFIRM_DELETE_SHELF: + shelves.delete() + } + }).SetCancelFunc(func() { + shelves.confirmDialog.Hide() + }) + + shelves.messageDialog.SetCancelFunc(func() { + shelves.messageDialog.Hide() + }) + + shelves.createDialog.SetCancelFunc(func() { + shelves.createDialog.Hide() + }).SetCreateFunc(func() { + shelves.createDialog.Hide() + shelves.create() + }) + + shelves.editDialog.SetCancelFunc(func() { + shelves.editDialog.Hide() + }).SetEditFunc(func() { + shelves.editDialog.Hide() + shelves.edit() + }) + + shelves.SetBuildingListFunc(func() (map[uint64]types.Building, error) { + if resp, err := shelves.client.RetrieveAllBuildings(); err != nil { + return nil, err + } else { + buildings := map[uint64]types.Building{} + + for _, a := range resp { + buildings[a.ID] = *a + } + + return buildings, nil + } + }) + + shelves.allDialogs = []dialogs.Dialog{ + shelves.errorDialog, + shelves.messageDialog, + shelves.progressDialog, + shelves.confirmDialog, + shelves.createDialog, + shelves.editDialog, + shelves.cmdDialog, + } + + return shelves +} + +func (a *Shelves) GetTitle() string { + return a.title +} + +func (a *Shelves) HasFocus() bool { + return dialogs.CheckDialogFocus(a.allDialogs...) || utils.CheckFocus(a.shelfTable, a.Box) +} + +func (a *Shelves) SubDialogHasFocus() bool { + return dialogs.CheckDialogFocus(a.allDialogs...) +} + +func (a *Shelves) Focus(delegate func(tview.Primitive)) { + + for _, dialog := range a.allDialogs { + if dialog.IsDisplay() { + delegate(dialog) + return + } + } + + delegate(a.shelfTable) +} + +func (a *Shelves) SetBuildingListFunc(list func() (map[uint64]types.Building, error)) { + a.buildingListFunc = list +} + +func (a *Shelves) SetShelfListFunc(list func() ([]types.ShelfLocation, error)) { + a.shelfListFunc = list +} + +func (a *Shelves) hideAllDialogs() { + for _, dialog := range a.allDialogs { + dialog.Hide() + } +} + +func (a *Shelves) getSelectedItem() *shelfSelectedItem { + selectedItem := shelfSelectedItem{} + + if a.shelfTable.GetRowCount() <= 1 { + return nil + } + + row, _ := a.shelfTable.GetSelection() + selectedItem.id = a.shelfTable.GetCell(row, 0).Text + selectedItem.name = a.shelfTable.GetCell(row, 1).Text + selectedItem.building = a.shelfTable.GetCell(row, 2).Text + selectedItem.roomNumber = a.shelfTable.GetCell(row, 3).Text + + return &selectedItem +} diff --git a/internal/ui/shelves/shlvdialogs/create.go b/internal/ui/shelves/shlvdialogs/create.go new file mode 100644 index 0000000..32fe9be --- /dev/null +++ b/internal/ui/shelves/shlvdialogs/create.go @@ -0,0 +1,605 @@ +package shlvdialogs + +import ( + "fmt" + "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 ( + shelfCreateDialogMaxWidth = 100 + shelfCreateDialogHeight = 17 +) + +const ( + createShelfFormFocus = 0 + iota + createCategoriesFocus + createCategoryPagesFocus + createShelfNameFieldFocus + createShelfRoomFieldFocus + createShelfBuildingFieldFocus + createShelfDescriptionFieldFocus +) + +const ( + generalPageIndex = 0 + iota + locationPageIndex + commentPageIndex +) + +type ShelfCreateDialog struct { + *tview.Box + layout *tview.Flex + createCategoryLabels []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 + shelf *types.ShelfLocation + buildingList []*types.Building + + shelfNameField *tview.InputField + shelfRoomField *tview.InputField + shelfDescriptionArea *tview.TextArea + shelfBuildingField *tview.DropDown + + focusMap map[int]tview.Primitive + + cancelHandler func() + createHandler func() +} + +func NewShelfCreateDialog(logger *zap.Logger, client *api.APIClient) *ShelfCreateDialog { + createDialog := ShelfCreateDialog{ + Box: tview.NewBox(), + layout: tview.NewFlex(), + createCategoryLabels: []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, + shelfNameField: tview.NewInputField(), + shelfRoomField: tview.NewInputField(), + shelfBuildingField: tview.NewDropDown(), + shelfDescriptionArea: tview.NewTextArea(), + } + + createDialog.focusMap = map[int]tview.Primitive{ + createShelfNameFieldFocus: createDialog.shelfNameField, + createShelfRoomFieldFocus: createDialog.shelfRoomField, + createShelfBuildingFieldFocus: createDialog.shelfBuildingField, + createShelfDescriptionFieldFocus: createDialog.shelfDescriptionArea, + } + + createDialog.setupLayout() + createDialog.setActiveCategory(0) + createDialog.initCustomInputHandlers() + + return &createDialog +} + +func (d *ShelfCreateDialog) Display() { + d.display = true + d.initData() + d.focusElement = createCategoryPagesFocus +} + +func (d *ShelfCreateDialog) IsDisplay() bool { + return d.display +} + +func (d *ShelfCreateDialog) Hide() { + d.display = false +} + +func (d *ShelfCreateDialog) HasFocus() bool { + return utils.CheckFocus(d.categories, d.categoryPages, d.Box, d.form) +} + +func (d *ShelfCreateDialog) Focus(delegate func(tview.Primitive)) { + switch d.focusElement { + case createShelfFormFocus: + button := d.form.GetButton(d.form.GetButtonCount() - 1) + button.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { + if event.Key() == utils.SwitchFocusKey.EventKey() { + d.focusElement = createCategoriesFocus + + d.Focus(delegate) + d.form.SetFocus(0) + + return nil + } + + if event.Key() == tcell.KeyEnter { + return nil + } + + return event + }) + delegate(d.form) + case createCategoriesFocus: + d.categories.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { + if event.Key() == utils.SwitchFocusKey.EventKey() { + d.focusElement = createCategoryPagesFocus + d.Focus(delegate) + + return nil + } + + if event.Key() == tcell.KeyBacktab { + d.focusElement = createShelfFormFocus + 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 createCategoryPagesFocus: + delegate(d.categoryPages) + default: + delegate(d.focusMap[d.focusElement]) + } +} + +func (d *ShelfCreateDialog) InputHandler() func(*tcell.EventKey, func(tview.Primitive)) { + return d.WrapInputHandler(func(event *tcell.EventKey, setFocus func(tview.Primitive)) { + d.logger.Sugar().Debugf("shelf create 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() + } + + if event.Key() == tcell.KeyBacktab { + d.setGeneralInfoPrevFocus() + } + + handler(event, setFocus) + + return + } + } + + if d.locationPage.HasFocus() { + if handler := d.locationPage.InputHandler(); handler != nil { + if event.Key() == utils.SwitchFocusKey.EventKey() { + d.setLocationNextFocus() + } + + if event.Key() == tcell.KeyBacktab { + d.setLocationPrevFocus() + } + + handler(event, setFocus) + + return + } + } + + if d.commentsPage.HasFocus() { + if handler := d.commentsPage.InputHandler(); handler != nil { + if event.Key() == utils.SwitchFocusKey.EventKey() { + d.setCommentsNextFocus() + } + + if event.Key() == tcell.KeyBacktab { + d.setCommentsPrevFocus() + } + + 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.createHandler() + } + } + + formHandler(event, setFocus) + + return + } + } + }) +} + +func (d *ShelfCreateDialog) SetRect(x, y, width, height int) { + if width > shelfCreateDialogMaxWidth { + emptySpace := (width - shelfCreateDialogMaxWidth) / 2 + x += emptySpace + width = shelfCreateDialogMaxWidth + } + + if height > shelfCreateDialogHeight { + emptySpace := (height - shelfCreateDialogHeight) / 2 + y += emptySpace + height = shelfCreateDialogHeight + } + + d.Box.SetRect(x, y, width, height) +} + +func (d *ShelfCreateDialog) 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 *ShelfCreateDialog) SetCreateFunc(f func()) *ShelfCreateDialog { + d.createHandler = f + enterButton := d.form.GetButton(d.form.GetButtonCount() - 1) + + enterButton.SetSelectedFunc(f) + + return d +} + +func (d *ShelfCreateDialog) SetCancelFunc(f func()) *ShelfCreateDialog { + d.cancelHandler = f + cancelButton := d.form.GetButton(d.form.GetButtonCount() - 2) + + cancelButton.SetSelectedFunc(f) + + return d +} + +func (d *ShelfCreateDialog) dropdownHasFocus() bool { + return utils.CheckFocus(d.shelfBuildingField) +} + +func (d *ShelfCreateDialog) SetShelf(a *types.ShelfLocation) { + d.shelf = a +} + +func (d *ShelfCreateDialog) initData() { + + // Get available buildings + buildingList, _ := d.client.RetrieveAllBuildings() + d.buildingList = buildingList + + buildingOptions := []string{""} + for _, building := range d.buildingList { + buildingOptions = append(buildingOptions, building.Name) // In case we want to do custom formatting later + } + + d.setActiveCategory(0) + + // General category + + if d.shelf == nil { + d.shelfNameField.SetText("") + + // Location category + d.shelfBuildingField.SetOptions(buildingOptions, nil) + d.shelfRoomField.SetText("") + + // Comments category + d.shelfDescriptionArea.SetText("", true) + } else { + d.shelfNameField.SetText(d.shelf.Name) + + // Location category + d.shelfBuildingField.SetOptions(buildingOptions, nil) + if d.shelf.Building != nil { + d.shelfBuildingField.SetCurrentOption(findOption(buildingOptions, d.shelf.Building.Name)) + } + d.shelfRoomField.SetText("") + + // Comments category + d.shelfDescriptionArea.SetText("", true) + } +} + +func (d *ShelfCreateDialog) 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("Create", nil) + d.form.SetButtonsAlign(tview.AlignRight) + d.form.SetButtonBackgroundColor(style.ButtonBgColor) + d.form.SetButtonTextColor(style.ButtonFgColor) + d.form.SetButtonActivatedStyle(activatedStyle) + + d.categoryPages.AddPage(d.createCategoryLabels[generalPageIndex], d.generalInfoPage, true, true) + d.categoryPages.AddPage(d.createCategoryLabels[locationPageIndex], d.locationPage, true, true) + d.categoryPages.AddPage(d.createCategoryLabels[commentPageIndex], d.commentsPage, true, true) + + d.layout.SetBackgroundColor(bgColor) + d.layout.SetBorder(true) + d.layout.SetBorderColor(style.DialogBorderColor) + d.layout.SetTitle("SHELF CREATE") + d.layout.SetTitleColor(style.DialogFgColor) + + _, layoutWidth := utils.AlignStringListWidth(d.createCategoryLabels) + 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 *ShelfCreateDialog) setupGeneralInfoPageUI() { + bgColor := style.DialogBgColor + inputFieldBgColor := style.InputFieldBgColor + pageLabelWidth := 14 + + d.shelfNameField.SetLabel("name:") + d.shelfNameField.SetLabelWidth(pageLabelWidth) + d.shelfNameField.SetLabelColor(bgColor) + d.shelfNameField.SetBackgroundColor(style.DialogFgColor) + d.shelfNameField.SetFieldBackgroundColor(inputFieldBgColor) + + d.generalInfoPage.SetDirection(tview.FlexRow) + d.generalInfoPage.AddItem(d.shelfNameField, 1, 0, true) + d.generalInfoPage.AddItem(utils.EmptyBoxSpace(bgColor), 1, 0, true) +} + +func (d *ShelfCreateDialog) setupLocationPageUI() { + bgColor := style.DialogBgColor + inputFieldBgColor := style.InputFieldBgColor + pageLabelWidth := 12 + ddUnselectedStyle := style.DropdownUnselected + ddSelectedStyle := style.DropdownSelected + + d.shelfRoomField.SetLabel("room:") + d.shelfRoomField.SetLabelWidth(pageLabelWidth) + d.shelfRoomField.SetLabelColor(bgColor) + d.shelfRoomField.SetBackgroundColor(style.DialogFgColor) + d.shelfRoomField.SetFieldBackgroundColor(inputFieldBgColor) + + d.shelfBuildingField.SetLabel("building:") + d.shelfBuildingField.SetLabelWidth(pageLabelWidth) + d.shelfBuildingField.SetBackgroundColor(bgColor) + d.shelfBuildingField.SetLabelColor(style.DialogFgColor) + d.shelfBuildingField.SetListStyles(ddUnselectedStyle, ddSelectedStyle) + d.shelfBuildingField.SetFieldBackgroundColor(inputFieldBgColor) + + d.locationPage.SetDirection(tview.FlexRow) + d.locationPage.AddItem(d.shelfRoomField, 1, 0, true) + d.locationPage.AddItem(utils.EmptyBoxSpace(bgColor), 1, 0, true) + d.locationPage.AddItem(d.shelfBuildingField, 1, 0, true) + d.locationPage.SetBackgroundColor(bgColor) +} + +func (d *ShelfCreateDialog) setupCommentsPageUI() { + bgColor := style.DialogBgColor + inputFieldBgColor := style.InputFieldBgColor + pageLabelWidth := 12 + + d.shelfDescriptionArea.SetLabel("comments:") + d.shelfDescriptionArea.SetLabelWidth(pageLabelWidth) + d.shelfDescriptionArea.SetBackgroundColor(inputFieldBgColor) + d.shelfDescriptionArea.SetLabelStyle(tcell.StyleDefault.Foreground(style.DialogFgColor).Background(bgColor)) + d.shelfDescriptionArea.SetWrap(true) + + d.commentsPage.AddItem(d.shelfDescriptionArea, 0, 1, true) + d.commentsPage.SetBackgroundColor(bgColor) +} + +func (d *ShelfCreateDialog) 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.createCategoryLabels) + + 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.createCategoryLabels[idx]) +} + +func (d *ShelfCreateDialog) nextCategory() { + activePage := d.activePageIndex + if d.activePageIndex < len(d.createCategoryLabels)-1 { + activePage++ + + d.setActiveCategory(activePage) + + return + } + + d.setActiveCategory(0) +} + +func (d *ShelfCreateDialog) previousCategory() { + activePage := d.activePageIndex + if d.activePageIndex > 0 { + activePage-- + + d.setActiveCategory(activePage) + + return + } + + d.setActiveCategory(len(d.createCategoryLabels) - 1) +} + +func (d *ShelfCreateDialog) initCustomInputHandlers() { + d.shelfBuildingField.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { + event = utils.ParseKeyEventKey(d.logger, event) + + return event + }) +} + +func (d *ShelfCreateDialog) setGeneralInfoNextFocus() { + // Name -> Quantity -> Length -> Manufacturer -> Model -> Price + if d.shelfNameField.HasFocus() { + d.focusElement = createShelfFormFocus + + return + } + + d.focusElement = createShelfFormFocus +} + +func (d *ShelfCreateDialog) setGeneralInfoPrevFocus() { + if d.shelfNameField.HasFocus() { + d.focusElement = createCategoriesFocus + + return + } + + d.focusElement = createShelfNameFieldFocus +} + +func (d *ShelfCreateDialog) setLocationNextFocus() { + // Category -> Shelf + if d.shelfRoomField.HasFocus() { + d.focusElement = createShelfBuildingFieldFocus + + return + } + + d.focusElement = createShelfFormFocus +} + +func (d *ShelfCreateDialog) setLocationPrevFocus() { + if d.shelfRoomField.HasFocus() { + d.focusElement = createCategoriesFocus + + return + } + + if d.shelfBuildingField.HasFocus() { + d.focusElement = createShelfRoomFieldFocus + + return + } + + d.focusElement = createShelfBuildingFieldFocus +} + +func (d *ShelfCreateDialog) setCommentsNextFocus() { + d.focusElement = createShelfFormFocus +} + +func (d *ShelfCreateDialog) setCommentsPrevFocus() { + d.focusElement = createCategoriesFocus +} + +func (d *ShelfCreateDialog) CreateShelfOptions() types.CreateShelfRequest { + var ( + buildingID *uint64 + ) + + selectedBuildingIndex, _ := d.shelfBuildingField.GetCurrentOption() + if len(d.buildingList) > 0 && selectedBuildingIndex > 0 { + buildingID = &d.buildingList[selectedBuildingIndex-1].ID + } + + req := types.CreateShelfRequest{ + Name: d.shelfNameField.GetText(), + RoomNumber: d.shelfRoomField.GetText(), + Description: d.shelfDescriptionArea.GetText(), + BuildingID: buildingID, + } + + return req +} diff --git a/internal/ui/shelves/shlvdialogs/edit.go b/internal/ui/shelves/shlvdialogs/edit.go new file mode 100644 index 0000000..d48e1cd --- /dev/null +++ b/internal/ui/shelves/shlvdialogs/edit.go @@ -0,0 +1,604 @@ +package shlvdialogs + +import ( + "fmt" + "slices" + "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 ( + shelfEditDialogMaxWidth = 100 + shelfEditDialogHeight = 17 +) + +const ( + editShelfFormFocus = 0 + iota + editCategoriesFocus + editCategoryPagesFocus + editShelfNameFieldFocus + editShelfRoomFieldFocus + editShelfBuildingFieldFocus + editShelfDescriptionFieldFocus +) + +type ShelfEditDialog 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 + shelf *types.ShelfLocation + buildingList []*types.Building + + shelfNameField *tview.InputField + shelfRoomField *tview.InputField + shelfDescriptionArea *tview.TextArea + shelfBuildingField *tview.DropDown + + focusMap map[int]tview.Primitive + + cancelHandler func() + editHandler func() +} + +func NewShelfEditDialog(logger *zap.Logger, client *api.APIClient) *ShelfEditDialog { + editDialog := ShelfEditDialog{ + 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, + shelfNameField: tview.NewInputField(), + shelfRoomField: tview.NewInputField(), + shelfBuildingField: tview.NewDropDown(), + shelfDescriptionArea: tview.NewTextArea(), + } + + editDialog.focusMap = map[int]tview.Primitive{ + editShelfNameFieldFocus: editDialog.shelfNameField, + editShelfRoomFieldFocus: editDialog.shelfRoomField, + editShelfBuildingFieldFocus: editDialog.shelfBuildingField, + editShelfDescriptionFieldFocus: editDialog.shelfDescriptionArea, + } + + editDialog.setupLayout() + editDialog.setActiveCategory(0) + editDialog.initCustomInputHandlers() + + return &editDialog +} + +func (d *ShelfEditDialog) Display() { + d.display = true + d.initData() + d.focusElement = editCategoryPagesFocus +} + +func (d *ShelfEditDialog) IsDisplay() bool { + return d.display +} + +func (d *ShelfEditDialog) Hide() { + d.display = false +} + +func (d *ShelfEditDialog) HasFocus() bool { + return utils.CheckFocus(d.categories, d.categoryPages, d.Box, d.form) +} + +func (d *ShelfEditDialog) Focus(delegate func(tview.Primitive)) { + switch d.focusElement { + case editShelfFormFocus: + 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 + } + + if event.Key() == tcell.KeyBacktab { + d.focusElement = editShelfFormFocus + 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 *ShelfEditDialog) InputHandler() func(*tcell.EventKey, func(tview.Primitive)) { + return d.WrapInputHandler(func(event *tcell.EventKey, setFocus func(tview.Primitive)) { + d.logger.Sugar().Debugf("shelf 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() + } + + if event.Key() == tcell.KeyBacktab { + d.setGeneralInfoPrevFocus() + } + + handler(event, setFocus) + + return + } + } + + if d.locationPage.HasFocus() { + if handler := d.locationPage.InputHandler(); handler != nil { + if event.Key() == utils.SwitchFocusKey.EventKey() { + d.setLocationNextFocus() + } + + if event.Key() == tcell.KeyBacktab { + d.setLocationPrevFocus() + } + + handler(event, setFocus) + + return + } + } + + if d.commentsPage.HasFocus() { + if handler := d.commentsPage.InputHandler(); handler != nil { + if event.Key() == utils.SwitchFocusKey.EventKey() { + d.setCommentsNextFocus() + } + + if event.Key() == tcell.KeyBacktab { + d.setCommentsPrevFocus() + } + + 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 *ShelfEditDialog) SetRect(x, y, width, height int) { + if width > shelfEditDialogMaxWidth { + emptySpace := (width - shelfEditDialogMaxWidth) / 2 + x += emptySpace + width = shelfEditDialogMaxWidth + } + + if height > shelfEditDialogHeight { + emptySpace := (height - shelfEditDialogHeight) / 2 + y += emptySpace + height = shelfEditDialogHeight + } + + d.Box.SetRect(x, y, width, height) +} + +func (d *ShelfEditDialog) 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 *ShelfEditDialog) SetEditFunc(f func()) *ShelfEditDialog { + d.editHandler = f + enterButton := d.form.GetButton(d.form.GetButtonCount() - 1) + + enterButton.SetSelectedFunc(f) + + return d +} + +func (d *ShelfEditDialog) SetCancelFunc(f func()) *ShelfEditDialog { + d.cancelHandler = f + cancelButton := d.form.GetButton(d.form.GetButtonCount() - 2) + + cancelButton.SetSelectedFunc(f) + + return d +} + +func (d *ShelfEditDialog) dropdownHasFocus() bool { + return utils.CheckFocus(d.shelfBuildingField) +} + +func (d *ShelfEditDialog) SetShelf(a *types.ShelfLocation) { + d.shelf = a +} + +func (d *ShelfEditDialog) initData() { + + // Get available buildings + buildingList, _ := d.client.RetrieveAllBuildings() + d.buildingList = buildingList + + buildingOptions := []string{""} + for _, building := range d.buildingList { + buildingOptions = append(buildingOptions, building.Name) // In case we want to do custom formatting later + } + + d.setActiveCategory(0) + + // General category + + if d.shelf == nil { + d.shelfNameField.SetText("") + + // Location category + d.shelfBuildingField.SetOptions(buildingOptions, nil) + d.shelfRoomField.SetText("") + + // Comments category + d.shelfDescriptionArea.SetText("", true) + } else { + d.shelfNameField.SetText(d.shelf.Name) + + // Location category + d.shelfBuildingField.SetOptions(buildingOptions, nil) + if d.shelf.Building != nil { + d.shelfBuildingField.SetCurrentOption(findOption(buildingOptions, d.shelf.Building.Name)) + } + d.shelfRoomField.SetText("") + + // Comments category + d.shelfDescriptionArea.SetText("", true) + } +} + +func findOption(options []string, search string) int { + return slices.Index(options, search) +} + +func (d *ShelfEditDialog) 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("SHELF 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 *ShelfEditDialog) setupGeneralInfoPageUI() { + bgColor := style.DialogBgColor + inputFieldBgColor := style.InputFieldBgColor + pageLabelWidth := 14 + + d.shelfNameField.SetLabel("name:") + d.shelfNameField.SetLabelWidth(pageLabelWidth) + d.shelfNameField.SetLabelColor(bgColor) + d.shelfNameField.SetBackgroundColor(style.DialogFgColor) + d.shelfNameField.SetFieldBackgroundColor(inputFieldBgColor) + + d.generalInfoPage.SetDirection(tview.FlexRow) + d.generalInfoPage.AddItem(d.shelfNameField, 1, 0, true) + d.generalInfoPage.AddItem(utils.EmptyBoxSpace(bgColor), 1, 0, true) +} + +func (d *ShelfEditDialog) setupLocationPageUI() { + bgColor := style.DialogBgColor + inputFieldBgColor := style.InputFieldBgColor + pageLabelWidth := 12 + ddUnselectedStyle := style.DropdownUnselected + ddSelectedStyle := style.DropdownSelected + + d.shelfRoomField.SetLabel("room:") + d.shelfRoomField.SetLabelWidth(pageLabelWidth) + d.shelfRoomField.SetLabelColor(bgColor) + d.shelfRoomField.SetBackgroundColor(style.DialogFgColor) + d.shelfRoomField.SetFieldBackgroundColor(inputFieldBgColor) + + d.shelfBuildingField.SetLabel("building:") + d.shelfBuildingField.SetLabelWidth(pageLabelWidth) + d.shelfBuildingField.SetBackgroundColor(bgColor) + d.shelfBuildingField.SetLabelColor(style.DialogFgColor) + d.shelfBuildingField.SetListStyles(ddUnselectedStyle, ddSelectedStyle) + d.shelfBuildingField.SetFieldBackgroundColor(inputFieldBgColor) + + d.locationPage.SetDirection(tview.FlexRow) + d.locationPage.AddItem(d.shelfRoomField, 1, 0, true) + d.locationPage.AddItem(utils.EmptyBoxSpace(bgColor), 1, 0, true) + d.locationPage.AddItem(d.shelfBuildingField, 1, 0, true) + d.locationPage.SetBackgroundColor(bgColor) +} + +func (d *ShelfEditDialog) setupCommentsPageUI() { + bgColor := style.DialogBgColor + inputFieldBgColor := style.InputFieldBgColor + pageLabelWidth := 12 + + d.shelfDescriptionArea.SetLabel("comments:") + d.shelfDescriptionArea.SetLabelWidth(pageLabelWidth) + d.shelfDescriptionArea.SetBackgroundColor(inputFieldBgColor) + d.shelfDescriptionArea.SetLabelStyle(tcell.StyleDefault.Foreground(style.DialogFgColor).Background(bgColor)) + d.shelfDescriptionArea.SetWrap(true) + + d.commentsPage.AddItem(d.shelfDescriptionArea, 0, 1, true) + d.commentsPage.SetBackgroundColor(bgColor) +} + +func (d *ShelfEditDialog) 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 *ShelfEditDialog) nextCategory() { + activePage := d.activePageIndex + if d.activePageIndex < len(d.editCategoryLabels)-1 { + activePage++ + + d.setActiveCategory(activePage) + + return + } + + d.setActiveCategory(0) +} + +func (d *ShelfEditDialog) previousCategory() { + activePage := d.activePageIndex + if d.activePageIndex > 0 { + activePage-- + + d.setActiveCategory(activePage) + + return + } + + d.setActiveCategory(len(d.editCategoryLabels) - 1) +} + +func (d *ShelfEditDialog) initCustomInputHandlers() { + d.shelfBuildingField.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { + event = utils.ParseKeyEventKey(d.logger, event) + + return event + }) +} + +func (d *ShelfEditDialog) setGeneralInfoNextFocus() { + // Name -> Quantity -> Length -> Manufacturer -> Model -> Price + if d.shelfNameField.HasFocus() { + d.focusElement = editShelfFormFocus + + return + } + + d.focusElement = editShelfFormFocus +} + +func (d *ShelfEditDialog) setGeneralInfoPrevFocus() { + if d.shelfNameField.HasFocus() { + d.focusElement = createCategoriesFocus + + return + } + + d.focusElement = editShelfNameFieldFocus +} + +func (d *ShelfEditDialog) setLocationNextFocus() { + // Category -> Shelf + if d.shelfRoomField.HasFocus() { + d.focusElement = editShelfBuildingFieldFocus + + return + } + + d.focusElement = editShelfFormFocus +} + +func (d *ShelfEditDialog) setLocationPrevFocus() { + if d.shelfRoomField.HasFocus() { + d.focusElement = createCategoriesFocus + + return + } + + if d.shelfBuildingField.HasFocus() { + d.focusElement = editShelfRoomFieldFocus + + return + } + + d.focusElement = editShelfBuildingFieldFocus +} + +func (d *ShelfEditDialog) setCommentsNextFocus() { + d.focusElement = editShelfFormFocus +} + +func (d *ShelfEditDialog) setCommentsPrevFocus() { + d.focusElement = createCategoriesFocus +} + +func (d *ShelfEditDialog) EditShelfOptions() types.CreateShelfRequest { + var ( + buildingID *uint64 + ) + + selectedBuildingIndex, _ := d.shelfBuildingField.GetCurrentOption() + if len(d.buildingList) > 0 && selectedBuildingIndex > 0 { + buildingID = &d.buildingList[selectedBuildingIndex-1].ID + } + + req := types.CreateShelfRequest{ + Name: d.shelfNameField.GetText(), + RoomNumber: d.shelfRoomField.GetText(), + Description: d.shelfDescriptionArea.GetText(), + BuildingID: buildingID, + } + + return req +} diff --git a/internal/ui/utils/keys.go b/internal/ui/utils/keys.go index 358f434..b0dbe66 100644 --- a/internal/ui/utils/keys.go +++ b/internal/ui/utils/keys.go @@ -38,12 +38,12 @@ var ( } CloseDialogKey = uiKeyInfo{ Key: tcell.KeyEsc, - KeyLabel: "Esc", + KeyLabel: "esc", KeyDesc: "close the active dialog", } SwitchFocusKey = uiKeyInfo{ Key: tcell.KeyTab, - KeyLabel: "Tab", + KeyLabel: "tab", KeyDesc: "switch between widgets", } ArrowUpKey = uiKeyInfo{ @@ -58,17 +58,17 @@ var ( } ArrowLeftKey = uiKeyInfo{ Key: tcell.KeyLeft, - KeyLabel: "Arrow Left", + KeyLabel: "arrow Left", KeyDesc: "previous screen", } ArrowRightKey = uiKeyInfo{ Key: tcell.KeyRight, - KeyLabel: "Arrow Right", + KeyLabel: "arrow Right", KeyDesc: "next screen", } AppExitKey = uiKeyInfo{ Key: tcell.KeyCtrlC, - KeyLabel: "Ctrl+c", + KeyLabel: "ctrl+c", KeyDesc: "exit application", } HelpScreenKey = uiKeyInfo{ @@ -96,6 +96,11 @@ var ( KeyLabel: "F5", KeyDesc: "display buildings screen", } + GroupsScreenKey = uiKeyInfo{ + Key: tcell.KeyF6, + KeyLabel: "F6", + KeyDesc: "display groups screen", + } ) var UIKeyBindings = []uiKeyInfo{ @@ -116,6 +121,7 @@ var UIKeyBindings = []uiKeyInfo{ ShelvesScreenKey, CategoriesScreenKey, BuildingsScreenKey, + GroupsScreenKey, } type uiKeyInfo struct {