package todo import ( "errors" "fmt" "github.com/google/uuid" . "github.com/labstack/echo/v4" "todo/crud" . "todo/html_components" "net/http" "os" "path/filepath" "strings" "time" ) const TodoPath = "/todo" type TodoCrud struct { e *Echo repo *TodoRepository html *GoHtmlHandler } func NewTodoCrud(e *Echo, repo *TodoRepository, html *GoHtmlHandler) *TodoCrud { return &TodoCrud{e: e, repo: repo, html: html} } func (i *TodoCrud) AddRoutes() { crud.Debug("Adding Todo crud routes") g := i.e.Group(TodoPath) g.GET("", i.GetItems) g.GET("/:id", i.GetItem) g.GET("/create", i.CreateNewItemInputs) g.POST("", i.CreateItem) g.GET("/:id/edit", i.EditItem) g.PUT("/:id", i.UpdateItem) g.DELETE("/:id", i.DeleteItem) } func (i *TodoCrud) readItem(c Context) (Todo, error) { userId := c.Get("userId").(int) id := ParseIntWithDefault(c.Param("id"), 1) return i.repo.Read(userId, id) } func (i *TodoCrud) GetItem(c Context) error { item, err := i.readItem(c) if err != nil { return err } queryString := GetCurrentUrlQueryParams(c) itemDisplay := ItemDisplay{ Columns: []ItemDisplayColumn{ {Label: "Name", Value: item.Name, Type: ItemDisplayTypeText}, {Label: "Completed", Value: fmt.Sprint(item.Completed), Type: ItemDisplayTypeText}, }, SubItems: i.getSubItemDisplays(item, c), EditItemUrl: fmt.Sprint(i.GetEntityUrl(c), "/", item.Id, "/edit", queryString), DeleteItemUrl: fmt.Sprint(i.GetEntityUrl(c), "/", item.Id, queryString), DeletePushUrl: fmt.Sprint(i.GetEntityUrl(c), queryString), HasImages: false, BackUrl: fmt.Sprint(i.GetEntityUrl(c), queryString), ItemId: item.Id, } return i.html.RenderPage(c, "todo", TodoDisplay{ IsDisplay: true, ItemDisplay: itemDisplay, }) } func (i *TodoCrud) getSubItemDisplays(item Todo, c Context) []ItemDisplaySubItem { var items []ItemDisplaySubItem return items } type TodoDisplay struct { IsTable bool Table GohtmlTable IsDisplay bool ItemDisplay ItemDisplay IsEdit bool EditItem EditItem } func (i *TodoCrud) GetItems(c Context) error { page, count, err := i.getPage(c) if err != nil { return err } table := i.itemsToTable(c, page, count) return i.html.RenderPage(c, "todo", TodoDisplay{ IsTable: true, Table: table, }) } func (i *TodoCrud) GetEntityUrl(c Context) string { return TodoPath } func (i *TodoCrud) GetParentEntityUrl(c Context) string { return "" } func (i *TodoCrud) CreateNewItemInputs(c Context) error { inputs := []EditItemInputs{ {Label: "Name", Value: "", Name: "Name", Type: InputTypeText, Options: []SelectInputOption{}}, {Label: "Completed", Value: "", Name: "Completed", Type: InputTypeBool, Options: []SelectInputOption{}}, } url := fmt.Sprint(i.GetEntityUrl(c), "?", c.QueryString()) s := EditItem{ Id: "", Title: "Create todo", Url: url, CancelUrl: url, IsCreate: true, SubmitButtonLabel: "Create", Inputs: inputs, HasFileUpload: false, } return i.html.RenderPage(c, "todo", TodoDisplay{ IsEdit: true, EditItem: s, }) } func (i *TodoCrud) CreateItem(c Context) error { userId := c.Get("userId").(int) item := Todo{ UserId: userId, Name: c.FormValue("Name"), Completed: ParseCheckboxWithDefault(c.FormValue("Completed"), false), } _, err := i.repo.Create(item) if err != nil { return err } return i.GetItems(c) } func (i *TodoCrud) EditItem(c Context) error { userId := c.Get("userId").(int) id := ParseIntWithDefault(c.Param("id"), 1) item, err := i.repo.Read(userId, id) if err != nil { return err } inputs := []EditItemInputs{ {Label: "Name", Value: item.Name, Name: "Name", Type: InputTypeText, Options: []SelectInputOption{}}, {Label: "Completed", Value: fmt.Sprint(item.Completed), Name: "Completed", Type: InputTypeBool, Options: []SelectInputOption{}}, } path := fmt.Sprint(i.GetEntityUrl(c), "/", item.Id) queryString := GetCurrentUrlQueryParams(c) url := fmt.Sprint(path, queryString) cancelUrl := url if HasFromTableHeader(c) { cancelUrl = fmt.Sprint(i.GetEntityUrl(c), queryString) } s := EditItem{ Id: fmt.Sprint(id), Title: fmt.Sprint("Update todo ", id), Url: url, CancelUrl: cancelUrl, IsCreate: false, SubmitButtonLabel: "Update", Inputs: inputs, HasFileUpload: false, FromTable: HasFromTableHeader(c), } return i.html.RenderPage(c, "todo", TodoDisplay{ IsEdit: true, EditItem: s, }) } func (i *TodoCrud) UpdateItem(c Context) error { userId := c.Get("userId").(int) id := ParseIntWithDefault(c.Param("id"), 1) item, err := i.repo.Read(userId, id) if err != nil { return err } item.Name = c.FormValue("Name") item.Completed = ParseCheckboxWithDefault(c.FormValue("Completed"), false) err = i.repo.Update(userId, item) if err != nil { return err } if HasFromTableHeader(c) { return i.GetItems(c) } return i.GetItem(c) } func (i *TodoCrud) DeleteItem(c Context) error { userId := c.Get("userId").(int) id := ParseIntWithDefault(c.Param("id"), 1) err := i.repo.Delete(userId, id) if err != nil { return err } return i.GetItems(c) } func (i *TodoCrud) parseDateTime(since string) time.Time { t, err := time.Parse("2006-01-02T15:04", since) if err != nil { return time.Now() } return t } func (i *TodoCrud) renderPage(c Context, repo *TodoRepository) error { page, count, err := i.getPage(c) if err != nil { return err } table := i.itemsToTable(c, page, count) return i.html.RenderPage(c, "todo", table) } func (i *TodoCrud) returnRenderTable(c Context, items []Todo, count int) error { table := i.itemsToTable(c, items, count) return i.html.RenderComponent(c, "table", table) } func (i *TodoCrud) itemsToTable(c Context, items []Todo, count int) GohtmlTable { filter := c.FormValue("filter") page := ParseIntWithDefault(c.FormValue("pageNumber"), 1) index := (page - 1) * 5 itemEnd := index + 5 if itemEnd > count { itemEnd = count } return GohtmlTable{ Headers: []string{ "Name", "Completed", }, Rows: i.structsToTableRows(c, items), EntityUrl: i.GetEntityUrl(c), CreateItemUrl: fmt.Sprint(i.GetEntityUrl(c), "/create?", c.QueryString()), OrderBy: string(i.getOrderBy(c)), OrderDirection: string(i.getOrderDirection(c)), FilterValue: c.FormValue("filterValue"), FilterSelect: SelectInput{ Label: "Filter by", Name: "filter", HideLabel: true, Options: []SelectInputOption{ {Label: "Name filter", Value: "Name", Selected: filter == "Name"}, {Label: "Completed filter", Value: "Completed", Selected: filter == "Completed"}, }, }, Pagination: Pagination{ CurrenItemStart: index + 1, CurrentItemEnd: index + 10, TotalNumberOfItems: count, PreviousDisabled: index == 0, NextDisabled: index+10 >= count, Page: page, PreviousPage: page - 1, NextPage: page + 1, }, ShowBack: false, BackUrl: i.GetParentEntityUrl(c), } } func (i *TodoCrud) structsToTableRows(c Context, items []Todo) []TableRow { var rows []TableRow for _, item := range items { rows = append(rows, i.structToRow(c, item)) } return rows } func (i *TodoCrud) structToRow(c Context, item Todo) TableRow { return TableRow{ Id: fmt.Sprint(item.Id), Columns: []TableColumn{ {Value: item.Name, Type: TableColumnTypeText}, {Value: fmt.Sprint(item.Completed), Type: TableColumnTypeText}, }, EntityUrl: i.GetEntityUrl(c), EditItemUrl: fmt.Sprint(i.GetEntityUrl(c), "/", item.Id, "/edit"), DeleteItemUrl: fmt.Sprint(i.GetEntityUrl(c), "/", item.Id), } } func (i *TodoCrud) formatDateRangeInputTimeStamp(time time.Time) string { return time.Format("2006-01-02T15:04") } func (i *TodoCrud) dateDisplay(time time.Time) string { return time.Format("2006-01-02 15:04:05") } func (i *TodoCrud) getPage(c Context) ([]Todo, int, error) { userId := c.Get("userId").(int) filter := c.FormValue("filter") filterValue := c.FormValue("filterValue") page := ParseIntWithDefault(c.FormValue("pageNumber"), 1) return i.repo.GetPage(TodoPaginationParams{ RowId: (page - 1) * 10, PageSize: 10, OrderBy: i.getOrderBy(c), OrderDirection: i.getOrderDirection(c), NameFilter: TodoNameFilter{ Active: filter == "Name", Value: filterValue, }, CompletedFilter: TodoCompletedFilter{ Active: filter == "Completed", Value: ParseBoolWithDefault(filterValue, false), }, References: TodoReferences{ UserId: userId, }, }) } func (i *TodoCrud) getOrderBy(c Context) TodoField { orderBy := c.QueryParam("orderBy") if orderBy == "" { return TodoFieldName } return TodoField(strings.ToLower(orderBy)) } func (i *TodoCrud) getOrderDirection(c Context) TodoOrderDirection { orderDirection := c.QueryParam("orderDirection") if orderDirection == "" { return TodoOrderDirectionAsc } return TodoOrderDirection(orderDirection) } func (i *TodoCrud) saveFileAndReturnFileName(c Context, name string, currentFilename string) string { file, err := c.FormFile(name) if errors.Is(err, http.ErrMissingFile) { return currentFilename } if err != nil { crud.Error("Failed to save file from input", name, "with error", err) return currentFilename } src, err := file.Open() if err != nil { crud.Error("Failed to save file from input", name, "with error", err) return currentFilename } defer src.Close() err = os.MkdirAll("files", os.ModePerm) if err != nil { crud.Error("Failed to save file from input", name, "with error", err) return currentFilename } extension := filepath.Ext(file.Filename) filename := fmt.Sprint(uuid.New().String(), extension) err = CreateFile("files/"+filename, src) if err != nil { crud.Error("Failed to save file from input", name, "with error", err) return currentFilename } return filename }