diff --git a/cmd/main.go b/cmd/main.go index d9a0c10..6a6094a 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -120,16 +120,22 @@ func main() { r.Get("/profile", Catch(webUserHandler.ProfileHandler)) // schedule r.Get("/schedules", Catch(webScheduleHandler.HomeHandler)) + r.Get("/schedules-pagination", Catch(webScheduleHandler.PaginationHandler)) + r.Post("/schedules", Catch(webScheduleHandler.CreateHandler)) + r.Get("/schedules/{id}", Catch(webScheduleHandler.EditHandler)) + r.Put("/schedules/{id}", Catch(webScheduleHandler.UpdateHandler)) + r.Delete("/schedules/{id}", Catch(webScheduleHandler.DeleteHandler)) + r.Get("/schedules/logs", Catch(webScheduleHandler.LogPaginationHandler)) // request r.Get("/requests", Catch(webRequestHandler.HomeHandler)) r.Get("/requests-pagination", Catch(webRequestHandler.PaginationHandler)) r.Post("/requests", Catch(webRequestHandler.CreateHandler)) - r.Get("/requests/{id}/edit", Catch(webRequestHandler.EditHandler)) + r.Get("/requests/{id}", Catch(webRequestHandler.EditHandler)) r.Put("/requests/{id}", Catch(webRequestHandler.UpdateHandler)) r.Delete("/requests/{id}", Catch(webRequestHandler.DeleteHandler)) r.Get("/requests/headers-pagination", Catch(webRequestHandler.HeaderPaginationHandler)) r.Post("/requests/headers", Catch(webRequestHandler.HeaderCreateHandler)) - r.Get("/requests/headers/{id}/edit", Catch(webRequestHandler.HeaderEditHandler)) + r.Get("/requests/headers/{id}", Catch(webRequestHandler.HeaderEditHandler)) r.Put("/requests/headers/{id}", Catch(webRequestHandler.HeaderUpdateHandler)) r.Delete("/requests/headers/{id}", Catch(webRequestHandler.HeaderDeleteHandler)) // group @@ -144,17 +150,17 @@ func main() { r.Get("/notifications", Catch(webNotificationHandler.HomeHandler)) r.Get("/notifications-pagination", Catch(webNotificationHandler.PaginationHandler)) r.Post("/notifications", Catch(webNotificationHandler.CreateHandler)) - r.Get("/notifications/{id}/edit", Catch(webNotificationHandler.EditHandler)) + r.Get("/notifications/{id}", Catch(webNotificationHandler.EditHandler)) r.Put("/notifications/{id}", Catch(webNotificationHandler.UpdateHandler)) r.Delete("/notifications/{id}", Catch(webNotificationHandler.DeleteHandler)) r.Get("/notifications/email-pagination", Catch(webNotificationHandler.EmailPaginationHandler)) r.Post("/notifications/email", Catch(webNotificationHandler.EmailCreateHandler)) - r.Get("/notifications/email/{id}/edit", Catch(webNotificationHandler.EmailEditHandler)) + r.Get("/notifications/email/{id}", Catch(webNotificationHandler.EmailEditHandler)) r.Put("/notifications/email/{id}", Catch(webNotificationHandler.EmailUpdateHandler)) r.Delete("/notifications/email/{id}", Catch(webNotificationHandler.EmailDeleteHandler)) r.Get("/notifications/message-pagination", Catch(webNotificationHandler.MessagePaginationHandler)) r.Post("/notifications/message", Catch(webNotificationHandler.MessageCreateHandler)) - r.Get("/notifications/message/{id}/edit", Catch(webNotificationHandler.MessageEditHandler)) + r.Get("/notifications/message/{id}", Catch(webNotificationHandler.MessageEditHandler)) r.Put("/notifications/message/{id}", Catch(webNotificationHandler.MessageUpdateHandler)) r.Delete("/notifications/message/{id}", Catch(webNotificationHandler.MessageDeleteHandler)) // setting @@ -166,8 +172,8 @@ func main() { r.Post("/users/signup", Catch(webSettingHandler.UserSignUpHandler)) r.Put("/users/change-profile", Catch(webSettingHandler.UserChangeProfileHandler)) r.Put("/users/change-password", Catch(webSettingHandler.UserChangePasswordHandler)) - r.Delete("/users/{id}/delete", Catch(webSettingHandler.UserDeleteHandler)) - r.Delete("/app-logs/{id}/delete", Catch(webSettingHandler.AppLogDeleteHandler)) + r.Delete("/users/{id}", Catch(webSettingHandler.UserDeleteHandler)) + r.Delete("/app-logs/{id}", Catch(webSettingHandler.AppLogDeleteHandler)) }) }) diff --git a/handler/web/group.go b/handler/web/group.go index 81fe6ac..b86782b 100644 --- a/handler/web/group.go +++ b/handler/web/group.go @@ -18,9 +18,7 @@ type GroupHandler struct { } func (h *GroupHandler) HomeHandler(w http.ResponseWriter, r *http.Request) error { - _, data := h.ListService(w, r) - return services.Render(w, r, "group", map[string]any{"lists": data.Data}, "group/list", "group/new") } diff --git a/handler/web/schedule.go b/handler/web/schedule.go index 6f3b85b..6094333 100644 --- a/handler/web/schedule.go +++ b/handler/web/schedule.go @@ -1,13 +1,275 @@ package web import ( + "fmt" + "io" + "log" + "math" "net/http" + "strings" + "github.com/go-chi/chi/v5" + "github.com/mstgnz/cronjob/config" + "github.com/mstgnz/cronjob/models" "github.com/mstgnz/cronjob/services" ) -type ScheduleHandler struct{} +type ScheduleHandler struct { + schedule *services.ScheduleService + group *services.GroupService + request *services.RequestService + notification *services.NotificationService +} func (h *ScheduleHandler) HomeHandler(w http.ResponseWriter, r *http.Request) error { - return services.Render(w, r, "schedule", map[string]any{}, "schedule/list", "schedule/log", "schedule/new") + _, group := h.group.ListService(w, r) + _, requests := h.request.ListService(w, r) + _, schedules := h.schedule.ListService(w, r) + _, notifications := h.notification.ListService(w, r) + return services.Render(w, r, "schedule", map[string]any{"lists": schedules.Data, "groups": group.Data, "requests": requests.Data, "notifications": notifications.Data}, "schedule/list", "schedule/log", "schedule/new") +} + +func (h *ScheduleHandler) CreateHandler(w http.ResponseWriter, r *http.Request) error { + jsonData, err := config.ConvertStringBoolsToBool(r, "active") + if err != nil { + _, _ = w.Write([]byte(err.Error())) + return nil + } + r.Body = io.NopCloser(strings.NewReader(string(jsonData))) + + code, response := h.schedule.CreateService(w, r) + if response.Status && code == http.StatusCreated { + w.Header().Set("HX-Redirect", "/schedules") + } + _, _ = w.Write([]byte(response.Message)) + return nil +} + +func (h *ScheduleHandler) EditHandler(w http.ResponseWriter, r *http.Request) error { + + id := chi.URLParam(r, "id") + query := r.URL.Query() + query.Set("id", id) + r.URL.RawQuery = query.Encode() + + _, response := h.schedule.ListService(w, r) + + log.Println(response.Data) + data, _ := response.Data["schedules"].([]*models.Schedule) + var updatedAt = "" + if data[0].UpdatedAt != nil { + updatedAt = data[0].UpdatedAt.Format("2006-01-02 15:04:05") + } + + activeSelected := "" + deactiveSelected := "" + + if data[0].Active { + activeSelected = "selected" + } else { + deactiveSelected = "selected" + } + + form := fmt.Sprintf(` + + %d + %s + %s + %s + + + + %v + + %s + %s + +
+ + +
+ + + `, data[0].ID, data[0].ID, data[0].Group.Name, data[0].Request.Url, data[0].Notification.Title, data[0].Timing, data[0].Timeout, data[0].Retries, data[0].Running, activeSelected, deactiveSelected, data[0].CreatedAt.Format("2006-01-02 15:04:05"), updatedAt, data[0].ID) + + _, _ = w.Write([]byte(form)) + return nil +} + +func (h *ScheduleHandler) UpdateHandler(w http.ResponseWriter, r *http.Request) error { + jsonData, err := config.ConvertStringBoolsToBool(r, "active") + if err != nil { + _, _ = w.Write([]byte(err.Error())) + return nil + } + r.Body = io.NopCloser(strings.NewReader(string(jsonData))) + + code, response := h.schedule.UpdateService(w, r) + if response.Status && code == http.StatusOK { + w.Header().Set("HX-Redirect", "/schedules") + } + _, _ = w.Write([]byte(response.Message)) + return nil +} + +func (h *ScheduleHandler) DeleteHandler(w http.ResponseWriter, r *http.Request) error { + code, response := h.schedule.DeleteService(w, r) + if response.Status && code == http.StatusOK { + w.Header().Set("HX-Redirect", "/schedules") + } + _, _ = w.Write([]byte(response.Message)) + return nil +} + +func (h *ScheduleHandler) PaginationHandler(w http.ResponseWriter, r *http.Request) error { + cUser, _ := r.Context().Value(config.CKey("user")).(*models.User) + + schedule := &models.Schedule{} + + search := "" + total := schedule.Count(cUser.ID) + row := 20 + + page := config.GetIntQuery(r, "page") + size := int(math.Ceil(float64(total) / float64(row))) + + current := config.Clamp(page, 1, size) + previous := config.Clamp(current-1, 1, size) + next := config.Clamp(current+1, 1, size) + + if r.URL.Query().Has("pagination") { + pagination := fmt.Sprintf(`
  • + +
  • `, previous) + + for i := 1; i <= size; i++ { + pagination += fmt.Sprintf(`
  • + +
  • `, i, i) + } + + pagination += fmt.Sprintf(`
  • + +
  • `, next) + _, _ = w.Write([]byte(pagination)) + return nil + } + + schedules := schedule.Paginate(cUser.ID, (current-1)*row, row, search) + + tr := "" + for _, v := range schedules { + if v.DeletedAt != nil { + continue + } + var updatedAt = "" + if v.UpdatedAt != nil { + updatedAt = v.UpdatedAt.Format("2006-01-02 15:04:05") + } + + tr += fmt.Sprintf(` + %d + %s + %s + %s + %s + %d + %d + %v + %v + %s + %s + +
    + + +
    + + `, v.ID, v.Group.Name, v.Request.Url, v.Notification.Title, v.Timing, v.Timeout, v.Retries, v.Running, v.Active, v.CreatedAt.Format("2006-01-02 15:04:05"), updatedAt, v.ID, v.ID) + } + _, _ = w.Write([]byte(tr)) + return nil +} + +func (h *ScheduleHandler) LogPaginationHandler(w http.ResponseWriter, r *http.Request) error { + cUser, _ := r.Context().Value(config.CKey("user")).(*models.User) + + scheduleLog := &models.ScheduleLog{} + + search := "" + total := scheduleLog.Count(cUser.ID) + row := 20 + + page := config.GetIntQuery(r, "page") + size := int(math.Ceil(float64(total) / float64(row))) + + current := config.Clamp(page, 1, size) + previous := config.Clamp(current-1, 1, size) + next := config.Clamp(current+1, 1, size) + + if r.URL.Query().Has("pagination") { + pagination := fmt.Sprintf(`
  • + +
  • `, previous) + + for i := 1; i <= size; i++ { + pagination += fmt.Sprintf(`
  • + +
  • `, i, i) + } + + pagination += fmt.Sprintf(`
  • + +
  • `, next) + _, _ = w.Write([]byte(pagination)) + return nil + } + + schedules := scheduleLog.Paginate(cUser.ID, (current-1)*row, row, search) + + tr := "" + for _, v := range schedules { + tr += fmt.Sprintf(` + %d + %s + %s + %s + %f + %v + %s + `, v.ID, v.Schedule.Timing, v.StartedAt.Format("2006-01-02 15:04:05"), v.FinishedAt.Format("2006-01-02 15:04:05"), v.Took, v.Result, v.CreatedAt.Format("2006-01-02 15:04:05")) + } + _, _ = w.Write([]byte(tr)) + return nil } diff --git a/models/schedule.go b/models/schedule.go index d250da9..de24351 100644 --- a/models/schedule.go +++ b/models/schedule.go @@ -9,19 +9,22 @@ import ( ) type Schedule struct { - ID int `json:"id"` - UserID int `json:"user_id" validate:"number"` - GroupID int `json:"group_id" validate:"required,number"` - RequestID int `json:"request_id" validate:"required,number"` - NotificationID int `json:"notification_id" validate:"required,number"` - Timing string `json:"timing" validate:"required,cron"` // https://crontab.guru/ - Timeout int `json:"timeout" validate:"number"` - Retries int `json:"retries" validate:"number"` - Running bool `json:"running" validate:"boolean"` - Active bool `json:"active" validate:"boolean"` - CreatedAt *time.Time `json:"created_at,omitempty"` - UpdatedAt *time.Time `json:"updated_at,omitempty"` - DeletedAt *time.Time `json:"deleted_at,omitempty"` + ID int `json:"id"` + UserID int `json:"user_id" validate:"number"` + GroupID int `json:"group_id" validate:"required,number"` + RequestID int `json:"request_id" validate:"required,number"` + NotificationID int `json:"notification_id" validate:"required,number"` + Timing string `json:"timing" validate:"required,cron"` // https://crontab.guru/ + Timeout int `json:"timeout" validate:"number"` + Retries int `json:"retries" validate:"number"` + Running bool `json:"running" validate:"boolean"` + Active bool `json:"active" validate:"boolean"` + Group *Group `json:"group,omitempty"` + Request *Request `json:"request,omitempty"` + Notification *Notification `json:"notification,omitempty"` + CreatedAt *time.Time `json:"created_at,omitempty"` + UpdatedAt *time.Time `json:"updated_at,omitempty"` + DeletedAt *time.Time `json:"deleted_at,omitempty"` } type ScheduleUpdate struct { @@ -49,27 +52,51 @@ type ScheduleBulk struct { Notification *NotificationBulk `json:"notification" validate:"omitempty"` } -func (m *Schedule) Get(id, userID, groupID, requestID, NotificationID int, timing string) ([]Schedule, error) { +func (m *Schedule) Count(userID int) int { + rowCount := 0 + + // prepare count + stmt, err := config.App().DB.Prepare(config.App().QUERY["SCHEDULES_COUNT"]) + if err != nil { + return rowCount + } + + // query + rows, err := stmt.Query(userID) + if err != nil { + return rowCount + } + defer func() { + _ = stmt.Close() + _ = rows.Close() + }() + for rows.Next() { + if err := rows.Scan(&rowCount); err != nil { + return rowCount + } + } + + return rowCount +} + +func (m *Schedule) Get(id, userID, groupID, requestID, NotificationID int, timing string) ([]*Schedule, error) { query := strings.TrimSuffix(config.App().QUERY["SCHEDULES"], ";") if id > 0 { - query += fmt.Sprintf(" AND id=%v", id) - } - if userID > 0 { - query += fmt.Sprintf(" AND user_id=%v", userID) + query += fmt.Sprintf(" AND s.id=%d", id) } if groupID > 0 { - query += fmt.Sprintf(" AND group_id=%v", userID) + query += fmt.Sprintf(" AND s.group_id=%d", groupID) } if requestID > 0 { - query += fmt.Sprintf(" AND request_id=%v", requestID) + query += fmt.Sprintf(" AND s.request_id=%d", requestID) } if NotificationID > 0 { - query += fmt.Sprintf(" AND notification_id=%v", NotificationID) + query += fmt.Sprintf(" AND s.notification_id=%d", NotificationID) } if timing != "" { - query += fmt.Sprintf(" AND timing=%v", timing) + query += fmt.Sprintf(" AND s.timing='%s'", timing) } // prepare @@ -88,18 +115,59 @@ func (m *Schedule) Get(id, userID, groupID, requestID, NotificationID int, timin _ = rows.Close() }() - var schedules []Schedule + var schedules []*Schedule for rows.Next() { - var schedule Schedule - if err := rows.Scan(&schedule.ID, &schedule.UserID, &schedule.GroupID, &schedule.RequestID, &schedule.NotificationID, &schedule.Timing, &schedule.Timeout, &schedule.Retries, &schedule.Running, &schedule.Active, &schedule.CreatedAt, &schedule.UpdatedAt, &schedule.DeletedAt); err != nil { - return nil, err + schedule := &Schedule{ + Group: &Group{}, + Request: &Request{}, + Notification: &Notification{}, } + + if err := rows.Scan(&schedule.ID, &schedule.UserID, &schedule.GroupID, &schedule.RequestID, &schedule.NotificationID, &schedule.Timing, &schedule.Timeout, &schedule.Retries, &schedule.Running, &schedule.Active, &schedule.CreatedAt, &schedule.UpdatedAt, &schedule.DeletedAt, &schedule.Group.Name, &schedule.Request.Url, &schedule.Notification.Title); err != nil { + return schedules, err + } + schedules = append(schedules, schedule) } return schedules, nil } +func (m *Schedule) Paginate(userID, offset, limit int, search string) []*Schedule { + schedules := []*Schedule{} + + // prepare schedules paginate + stmt, err := config.App().DB.Prepare(config.App().QUERY["SCHEDULES_PAGINATE"]) + if err != nil { + return schedules + } + + // query + rows, err := stmt.Query(userID, "%"+search+"%", offset, limit) + if err != nil { + return schedules + } + defer func() { + _ = stmt.Close() + _ = rows.Close() + }() + for rows.Next() { + schedule := &Schedule{ + Group: &Group{}, + Request: &Request{}, + Notification: &Notification{}, + } + + if err := rows.Scan(&schedule.ID, &schedule.UserID, &schedule.GroupID, &schedule.RequestID, &schedule.NotificationID, &schedule.Timing, &schedule.Timeout, &schedule.Retries, &schedule.Running, &schedule.Active, &schedule.CreatedAt, &schedule.UpdatedAt, &schedule.DeletedAt, &schedule.Group.Name, &schedule.Request.Url, &schedule.Notification.Title); err != nil { + return schedules + } + + schedules = append(schedules, schedule) + } + + return schedules +} + func (m *Schedule) Create(exec any) error { stmt, err := config.App().DB.RunPrepare(exec, config.App().QUERY["SCHEDULES_INSERT"]) if err != nil { diff --git a/models/schedule_log.go b/models/schedule_log.go index 5d006ba..e0b0a59 100644 --- a/models/schedule_log.go +++ b/models/schedule_log.go @@ -13,11 +13,39 @@ type ScheduleLog struct { ScheduleID int `json:"schedule_id" validate:"required"` Took float32 `json:"took" validate:"required"` Result any `json:"result" validate:"required"` + Schedule *Schedule `json:"schedule,omitempty"` StartedAt *time.Time `json:"started_at,omitempty"` FinishedAt *time.Time `json:"finished_at,omitempty"` CreatedAt *time.Time `json:"created_at,omitempty"` } +func (m *ScheduleLog) Count(userID int) int { + rowCount := 0 + + // prepare count + stmt, err := config.App().DB.Prepare(config.App().QUERY["SCHEDULE_LOGS_COUNT"]) + if err != nil { + return rowCount + } + + // query + rows, err := stmt.Query(userID) + if err != nil { + return rowCount + } + defer func() { + _ = stmt.Close() + _ = rows.Close() + }() + for rows.Next() { + if err := rows.Scan(&rowCount); err != nil { + return rowCount + } + } + + return rowCount +} + func (m *ScheduleLog) Get(id, schedule_id, user_id int) ([]ScheduleLog, error) { query := strings.TrimSuffix(config.App().QUERY["SCHEDULE_LOGS"], ";") @@ -53,6 +81,39 @@ func (m *ScheduleLog) Get(id, schedule_id, user_id int) ([]ScheduleLog, error) { return scheduleLogs, nil } +func (m *ScheduleLog) Paginate(userID, offset, limit int, search string) []*ScheduleLog { + scheduleLogs := []*ScheduleLog{} + + // prepare paginate + stmt, err := config.App().DB.Prepare(config.App().QUERY["SCHEDULE_LOGS_PAGINATE"]) + if err != nil { + return scheduleLogs + } + + // query + rows, err := stmt.Query(userID, "%"+search+"%", offset, limit) + if err != nil { + return scheduleLogs + } + defer func() { + _ = stmt.Close() + _ = rows.Close() + }() + for rows.Next() { + scheduleLog := &ScheduleLog{ + Schedule: &Schedule{}, + } + + if err := rows.Scan(&scheduleLog.ID, &scheduleLog.ScheduleID, &scheduleLog.StartedAt, &scheduleLog.FinishedAt, &scheduleLog.Took, &scheduleLog.Result, &scheduleLog.CreatedAt, &scheduleLog.Schedule.Timing); err != nil { + return scheduleLogs + } + + scheduleLogs = append(scheduleLogs, scheduleLog) + } + + return scheduleLogs +} + func (m *ScheduleLog) Create(scheduleId int) error { stmt, err := config.App().DB.Prepare(config.App().QUERY["SCHEDULE_LOG_INSERT"]) if err != nil { diff --git a/query.sql b/query.sql index db27ddb..c7b2777 100644 --- a/query.sql +++ b/query.sql @@ -140,11 +140,15 @@ SELECT s.*, g.name, r.url, n.title FROM schedules s JOIN groups g ON g.id=s.group_id JOIN requests r ON r.id=s.request_id JOIN notifications n ON n.id=s.notification_id -WHERE user_id=$1 AND deleted_at isnull AND (s.timing ilike $2 OR g.name ilike $2 OR r.url ilike $2 OR n.title ilike $2) +WHERE s.user_id=$1 AND s.deleted_at isnull AND (s.timing ilike $2 OR g.name ilike $2 OR r.url ilike $2 OR n.title ilike $2) ORDER BY s.id DESC offset $3 LIMIT $4; -- SCHEDULES -SELECT * FROM schedules WHERE user_id=$1 AND deleted_at isnull; +SELECT s.*, g.name, r.url, n.title FROM schedules s +JOIN groups g ON g.id=s.group_id +JOIN requests r ON r.id=s.request_id +JOIN notifications n ON n.id=s.notification_id +WHERE s.user_id=$1 AND s.deleted_at isnull; -- SCHEDULES_WITH_ID SELECT * FROM schedules WHERE user_id=$1 AND id=$2 AND deleted_at isnull; diff --git a/views/components/schedule/list.gohtml b/views/components/schedule/list.gohtml index 93e7009..7f11920 100644 --- a/views/components/schedule/list.gohtml +++ b/views/components/schedule/list.gohtml @@ -1,47 +1,40 @@ {{template "base" . }} -{{define "scheduleList" }} +{{define "css"}} + +{{end}} - +{{define "scheduleList" }} +
    - - - + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - +
    #FirstLastHandleGroup NameRequest UrlNotificationTimingTimeoutRetriesRunningActiveCreatedUpdatedActions
    1MarkOtto@mdo
    2JacobThornton@fat
    3Larry the Bird@twitter
    -
    - {{end}} \ No newline at end of file diff --git a/views/components/schedule/log.gohtml b/views/components/schedule/log.gohtml index 7668988..eea6026 100644 --- a/views/components/schedule/log.gohtml +++ b/views/components/schedule/log.gohtml @@ -1,47 +1,35 @@ {{template "base" . }} -{{define "scheduleLog" }} +{{define "css"}} + +{{end}} - +{{define "scheduleLog" }} +
    - - - + + + + + + - - - - - - - - - - - - - - - - - - - +
    #FirstLastHandleTimingStartedFinishedTookResultCreated
    1MarkOtto@mdo
    2JacobThornton@fat
    3Larry the Bird@twitter
    -
    - {{end}} \ No newline at end of file diff --git a/views/components/schedule/new.gohtml b/views/components/schedule/new.gohtml index da884f2..8e21dd2 100644 --- a/views/components/schedule/new.gohtml +++ b/views/components/schedule/new.gohtml @@ -1,7 +1,58 @@ {{template "base" . }} {{define "scheduleNew" }} - -New Schedule - +
    +
    +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    + + +
    +
    +
    {{end}} \ No newline at end of file diff --git a/views/pages/schedule.gohtml b/views/pages/schedule.gohtml index 2ca3b33..f0247e6 100644 --- a/views/pages/schedule.gohtml +++ b/views/pages/schedule.gohtml @@ -5,7 +5,7 @@
    -
    +
    {{block "scheduleList" .}}{{end}}
    {{block "scheduleNew" .}}{{end}}
    -
    +
    {{block "scheduleLog" .}}{{end}}