UXの大幅な改善
This commit is contained in:
@@ -3,6 +3,7 @@ package handler
|
||||
import (
|
||||
"encoding/csv"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -34,6 +35,18 @@ func (h *AssignmentHandler) getUserID(c *gin.Context) uint {
|
||||
return userID.(uint)
|
||||
}
|
||||
|
||||
func safeReferer(c *gin.Context, defaultPath string) string {
|
||||
referer := c.Request.Referer()
|
||||
if referer == "" {
|
||||
return defaultPath
|
||||
}
|
||||
u, err := url.Parse(referer)
|
||||
if err != nil || u.Host != c.Request.Host {
|
||||
return defaultPath
|
||||
}
|
||||
return referer
|
||||
}
|
||||
|
||||
func (h *AssignmentHandler) Dashboard(c *gin.Context) {
|
||||
userID := h.getUserID(c)
|
||||
stats, _ := h.assignmentService.GetDashboardStats(userID)
|
||||
@@ -64,6 +77,8 @@ func (h *AssignmentHandler) Index(c *gin.Context) {
|
||||
}
|
||||
query := c.Query("q")
|
||||
priority := c.Query("priority")
|
||||
subject := c.Query("subject")
|
||||
sort := c.Query("sort")
|
||||
pageStr := c.DefaultQuery("page", "1")
|
||||
page, err := strconv.Atoi(pageStr)
|
||||
if err != nil || page < 1 {
|
||||
@@ -71,7 +86,7 @@ func (h *AssignmentHandler) Index(c *gin.Context) {
|
||||
}
|
||||
const pageSize = 10
|
||||
|
||||
result, err := h.assignmentService.SearchAssignments(userID, query, priority, filter, page, pageSize)
|
||||
result, err := h.assignmentService.SearchAssignments(userID, query, priority, filter, sort, subject, page, pageSize)
|
||||
|
||||
var assignments []models.Assignment
|
||||
var totalPages, currentPage int
|
||||
@@ -85,6 +100,9 @@ func (h *AssignmentHandler) Index(c *gin.Context) {
|
||||
currentPage = result.CurrentPage
|
||||
}
|
||||
|
||||
subjects, _ := h.assignmentService.GetSubjectsByUser(userID)
|
||||
tabCounts := h.assignmentService.GetTabCounts(userID)
|
||||
|
||||
role, _ := c.Get(middleware.UserRoleKey)
|
||||
name, _ := c.Get(middleware.UserNameKey)
|
||||
|
||||
@@ -94,6 +112,10 @@ func (h *AssignmentHandler) Index(c *gin.Context) {
|
||||
"filter": filter,
|
||||
"query": query,
|
||||
"priority": priority,
|
||||
"subject": subject,
|
||||
"sort": sort,
|
||||
"subjects": subjects,
|
||||
"tabCounts": tabCounts,
|
||||
"isAdmin": role == "admin",
|
||||
"userName": name,
|
||||
"currentPage": currentPage,
|
||||
@@ -105,6 +127,59 @@ func (h *AssignmentHandler) Index(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
func (h *AssignmentHandler) TogglePin(c *gin.Context) {
|
||||
userID := h.getUserID(c)
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.Redirect(http.StatusFound, "/assignments")
|
||||
return
|
||||
}
|
||||
if _, err := h.assignmentService.TogglePin(userID, uint(id)); err != nil {
|
||||
c.Redirect(http.StatusFound, "/assignments")
|
||||
return
|
||||
}
|
||||
c.Redirect(http.StatusFound, safeReferer(c, "/assignments"))
|
||||
}
|
||||
|
||||
func (h *AssignmentHandler) BulkComplete(c *gin.Context) {
|
||||
userID := h.getUserID(c)
|
||||
rawIDs := c.PostFormArray("ids")
|
||||
var ids []uint
|
||||
for _, raw := range rawIDs {
|
||||
if v, err := strconv.ParseUint(raw, 10, 32); err == nil {
|
||||
ids = append(ids, uint(v))
|
||||
}
|
||||
}
|
||||
if err := h.assignmentService.BulkComplete(userID, ids); err != nil {
|
||||
c.Redirect(http.StatusFound, "/assignments")
|
||||
return
|
||||
}
|
||||
c.Redirect(http.StatusFound, safeReferer(c, "/assignments"))
|
||||
}
|
||||
|
||||
func (h *AssignmentHandler) BulkDelete(c *gin.Context) {
|
||||
userID := h.getUserID(c)
|
||||
rawIDs := c.PostFormArray("ids")
|
||||
var ids []uint
|
||||
for _, raw := range rawIDs {
|
||||
if v, err := strconv.ParseUint(raw, 10, 32); err == nil {
|
||||
ids = append(ids, uint(v))
|
||||
}
|
||||
}
|
||||
if c.PostForm("delete_recurring") == "true" {
|
||||
if recurringIDs, err := h.assignmentService.GetRecurringIDsByIDs(userID, ids); err == nil {
|
||||
for _, rid := range recurringIDs {
|
||||
h.recurringService.Delete(userID, rid, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := h.assignmentService.BulkDelete(userID, ids); err != nil {
|
||||
c.Redirect(http.StatusFound, "/assignments")
|
||||
return
|
||||
}
|
||||
c.Redirect(http.StatusFound, safeReferer(c, "/assignments"))
|
||||
}
|
||||
|
||||
func (h *AssignmentHandler) New(c *gin.Context) {
|
||||
role, _ := c.Get(middleware.UserRoleKey)
|
||||
name, _ := c.Get(middleware.UserNameKey)
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
|
||||
type Assignment struct {
|
||||
ID uint `gorm:"primarykey" json:"id"`
|
||||
UserID uint `gorm:"not null;index" json:"user_id"`
|
||||
UserID uint `gorm:"not null;index;index:idx_user_pinned" json:"user_id"`
|
||||
Title string `gorm:"not null" json:"title"`
|
||||
Description string `json:"description"`
|
||||
Subject string `json:"subject"`
|
||||
@@ -16,6 +16,7 @@ type Assignment struct {
|
||||
DueDate time.Time `gorm:"not null" json:"due_date"`
|
||||
SoftDueDate *time.Time `json:"soft_due_date,omitempty"`
|
||||
IsCompleted bool `gorm:"default:false" json:"is_completed"`
|
||||
IsPinned bool `gorm:"default:false;index:idx_user_pinned" json:"is_pinned"`
|
||||
IsArchived bool `gorm:"default:false;index" json:"is_archived"`
|
||||
CompletedAt *time.Time `json:"completed_at,omitempty"`
|
||||
ReminderEnabled bool `gorm:"default:false" json:"reminder_enabled"`
|
||||
|
||||
@@ -204,6 +204,28 @@ func (r *AssignmentRepository) CountOverdueByUserID(userID uint) (int64, error)
|
||||
return count, err
|
||||
}
|
||||
|
||||
func (r *AssignmentRepository) CountDueTodayByUserID(userID uint) (int64, error) {
|
||||
now := time.Now()
|
||||
startOfDay := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
||||
endOfDay := startOfDay.AddDate(0, 0, 1)
|
||||
var count int64
|
||||
err := r.db.Model(&models.Assignment{}).
|
||||
Where("user_id = ? AND is_completed = ? AND due_date >= ? AND due_date < ?",
|
||||
userID, false, startOfDay, endOfDay).Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
func (r *AssignmentRepository) CountDueThisWeekByUserID(userID uint) (int64, error) {
|
||||
now := time.Now()
|
||||
startOfDay := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
||||
weekLater := startOfDay.AddDate(0, 0, 7)
|
||||
var count int64
|
||||
err := r.db.Model(&models.Assignment{}).
|
||||
Where("user_id = ? AND is_completed = ? AND due_date >= ? AND due_date < ?",
|
||||
userID, false, startOfDay, weekLater).Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
type StatisticsFilter struct {
|
||||
Subject string
|
||||
From *time.Time
|
||||
@@ -372,7 +394,7 @@ func (r *AssignmentRepository) GetSubjectsByUserIDWithArchived(userID uint, incl
|
||||
return subjects, err
|
||||
}
|
||||
|
||||
func (r *AssignmentRepository) SearchWithPreload(userID uint, queryStr, priority, filter string, page, pageSize int) ([]models.Assignment, int64, error) {
|
||||
func (r *AssignmentRepository) SearchWithPreload(userID uint, queryStr, priority, filter, sort, subject string, page, pageSize int) ([]models.Assignment, int64, error) {
|
||||
var assignments []models.Assignment
|
||||
var totalCount int64
|
||||
|
||||
@@ -386,6 +408,10 @@ func (r *AssignmentRepository) SearchWithPreload(userID uint, queryStr, priority
|
||||
dbQuery = dbQuery.Where("priority = ?", priority)
|
||||
}
|
||||
|
||||
if subject != "" {
|
||||
dbQuery = dbQuery.Where("subject = ?", subject)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
startOfDay := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
||||
endOfDay := startOfDay.AddDate(0, 0, 1)
|
||||
@@ -410,11 +436,24 @@ func (r *AssignmentRepository) SearchWithPreload(userID uint, queryStr, priority
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
if filter == "completed" {
|
||||
dbQuery = dbQuery.Order("completed_at DESC")
|
||||
} else {
|
||||
dbQuery = dbQuery.Order("due_date ASC")
|
||||
var orderClause string
|
||||
switch sort {
|
||||
case "due_desc":
|
||||
orderClause = "is_pinned DESC, due_date DESC"
|
||||
case "priority":
|
||||
orderClause = "is_pinned DESC, CASE priority WHEN 'high' THEN 0 WHEN 'medium' THEN 1 ELSE 2 END ASC, due_date ASC"
|
||||
case "created_desc":
|
||||
orderClause = "is_pinned DESC, created_at DESC"
|
||||
case "subject":
|
||||
orderClause = "is_pinned DESC, subject ASC, due_date ASC"
|
||||
default:
|
||||
if filter == "completed" {
|
||||
orderClause = "is_pinned DESC, completed_at DESC"
|
||||
} else {
|
||||
orderClause = "is_pinned DESC, due_date ASC"
|
||||
}
|
||||
}
|
||||
dbQuery = dbQuery.Order(orderClause)
|
||||
|
||||
if page < 1 {
|
||||
page = 1
|
||||
@@ -427,3 +466,48 @@ func (r *AssignmentRepository) SearchWithPreload(userID uint, queryStr, priority
|
||||
err := dbQuery.Preload("RecurringAssignment").Limit(pageSize).Offset(offset).Find(&assignments).Error
|
||||
return assignments, totalCount, err
|
||||
}
|
||||
|
||||
func (r *AssignmentRepository) TogglePin(userID, assignmentID uint) (*models.Assignment, error) {
|
||||
var assignment models.Assignment
|
||||
if err := r.db.Where("id = ? AND user_id = ?", assignmentID, userID).First(&assignment).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
newPinned := !assignment.IsPinned
|
||||
if err := r.db.Model(&assignment).Update("is_pinned", newPinned).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
assignment.IsPinned = newPinned
|
||||
return &assignment, nil
|
||||
}
|
||||
|
||||
func (r *AssignmentRepository) BulkComplete(userID uint, ids []uint) error {
|
||||
if len(ids) == 0 {
|
||||
return nil
|
||||
}
|
||||
now := time.Now()
|
||||
return r.db.Model(&models.Assignment{}).
|
||||
Where("user_id = ? AND id IN ? AND is_completed = ?", userID, ids, false).
|
||||
Updates(map[string]interface{}{
|
||||
"is_completed": true,
|
||||
"completed_at": now,
|
||||
}).Error
|
||||
}
|
||||
|
||||
func (r *AssignmentRepository) BulkDelete(userID uint, ids []uint) error {
|
||||
if len(ids) == 0 {
|
||||
return nil
|
||||
}
|
||||
return r.db.Where("user_id = ? AND id IN ?", userID, ids).
|
||||
Delete(&models.Assignment{}).Error
|
||||
}
|
||||
|
||||
func (r *AssignmentRepository) GetRecurringIDsByIDs(userID uint, ids []uint) ([]uint, error) {
|
||||
if len(ids) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
var recurringIDs []uint
|
||||
err := r.db.Model(&models.Assignment{}).
|
||||
Where("user_id = ? AND id IN ? AND recurring_assignment_id IS NOT NULL", userID, ids).
|
||||
Pluck("recurring_assignment_id", &recurringIDs).Error
|
||||
return recurringIDs, err
|
||||
}
|
||||
|
||||
@@ -259,9 +259,12 @@ func Setup(cfg *config.Config) *gin.Engine {
|
||||
auth.GET("/assignments", assignmentHandler.Index)
|
||||
auth.GET("/assignments/new", assignmentHandler.New)
|
||||
auth.POST("/assignments", assignmentHandler.Create)
|
||||
auth.POST("/assignments/bulk-complete", assignmentHandler.BulkComplete)
|
||||
auth.POST("/assignments/bulk-delete", assignmentHandler.BulkDelete)
|
||||
auth.GET("/assignments/:id/edit", assignmentHandler.Edit)
|
||||
auth.POST("/assignments/:id", assignmentHandler.Update)
|
||||
auth.POST("/assignments/:id/toggle", assignmentHandler.Toggle)
|
||||
auth.POST("/assignments/:id/pin", assignmentHandler.TogglePin)
|
||||
auth.POST("/assignments/:id/delete", assignmentHandler.Delete)
|
||||
|
||||
auth.GET("/assignments/export", assignmentHandler.ExportCSV)
|
||||
|
||||
@@ -172,7 +172,7 @@ func (s *AssignmentService) GetOverdueByUserPaginated(userID uint, page, pageSiz
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *AssignmentService) SearchAssignments(userID uint, query, priority, filter string, page, pageSize int) (*PaginatedResult, error) {
|
||||
func (s *AssignmentService) SearchAssignments(userID uint, query, priority, filter, sort, subject string, page, pageSize int) (*PaginatedResult, error) {
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
@@ -180,7 +180,7 @@ func (s *AssignmentService) SearchAssignments(userID uint, query, priority, filt
|
||||
pageSize = 10
|
||||
}
|
||||
|
||||
assignments, totalCount, err := s.assignmentRepo.SearchWithPreload(userID, query, priority, filter, page, pageSize)
|
||||
assignments, totalCount, err := s.assignmentRepo.SearchWithPreload(userID, query, priority, filter, sort, subject, page, pageSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -196,6 +196,22 @@ func (s *AssignmentService) SearchAssignments(userID uint, query, priority, filt
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *AssignmentService) TogglePin(userID, assignmentID uint) (*models.Assignment, error) {
|
||||
return s.assignmentRepo.TogglePin(userID, assignmentID)
|
||||
}
|
||||
|
||||
func (s *AssignmentService) BulkComplete(userID uint, ids []uint) error {
|
||||
return s.assignmentRepo.BulkComplete(userID, ids)
|
||||
}
|
||||
|
||||
func (s *AssignmentService) BulkDelete(userID uint, ids []uint) error {
|
||||
return s.assignmentRepo.BulkDelete(userID, ids)
|
||||
}
|
||||
|
||||
func (s *AssignmentService) GetRecurringIDsByIDs(userID uint, ids []uint) ([]uint, error) {
|
||||
return s.assignmentRepo.GetRecurringIDsByIDs(userID, ids)
|
||||
}
|
||||
|
||||
func (s *AssignmentService) Update(userID, assignmentID uint, title, description, subject, priority string, dueDate time.Time, softDueDate *time.Time, reminderEnabled bool, reminderAt *time.Time, urgentReminderEnabled bool) (*models.Assignment, error) {
|
||||
assignment, err := s.GetByID(userID, assignmentID)
|
||||
if err != nil {
|
||||
@@ -398,3 +414,26 @@ func (s *AssignmentService) GetSubjectsWithArchived(userID uint, includeArchived
|
||||
func (s *AssignmentService) GetArchivedSubjects(userID uint) ([]string, error) {
|
||||
return s.assignmentRepo.GetArchivedSubjects(userID)
|
||||
}
|
||||
|
||||
type TabCounts struct {
|
||||
Pending int64
|
||||
DueToday int64
|
||||
DueThisWeek int64
|
||||
Completed int64
|
||||
Overdue int64
|
||||
}
|
||||
|
||||
func (s *AssignmentService) GetTabCounts(userID uint) TabCounts {
|
||||
pending, _ := s.assignmentRepo.CountPendingByUserID(userID)
|
||||
dueToday, _ := s.assignmentRepo.CountDueTodayByUserID(userID)
|
||||
dueThisWeek, _ := s.assignmentRepo.CountDueThisWeekByUserID(userID)
|
||||
completed, _ := s.assignmentRepo.CountCompletedByUserID(userID)
|
||||
overdue, _ := s.assignmentRepo.CountOverdueByUserID(userID)
|
||||
return TabCounts{
|
||||
Pending: pending,
|
||||
DueToday: dueToday,
|
||||
DueThisWeek: dueThisWeek,
|
||||
Completed: completed,
|
||||
Overdue: overdue,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user