繰り返し課題のAPI管理UIを追加、課題一覧のUX向上

This commit is contained in:
2026-01-11 12:02:04 +09:00
parent 30ba9510a6
commit b982c8acee
19 changed files with 1328 additions and 204 deletions

View File

@@ -13,11 +13,13 @@ import (
type APIHandler struct {
assignmentService *service.AssignmentService
recurringService *service.RecurringAssignmentService
}
func NewAPIHandler() *APIHandler {
return &APIHandler{
assignmentService: service.NewAssignmentService(),
recurringService: service.NewRecurringAssignmentService(),
}
}
@@ -26,17 +28,12 @@ func (h *APIHandler) getUserID(c *gin.Context) uint {
return userID.(uint)
}
// ListAssignments returns all assignments for the authenticated user with pagination
// GET /api/v1/assignments?filter=pending&page=1&page_size=20
func (h *APIHandler) ListAssignments(c *gin.Context) {
userID := h.getUserID(c)
filter := c.Query("filter") // pending, completed, overdue
// Parse pagination parameters
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
// Validate pagination parameters
if page < 1 {
page = 1
}
@@ -44,10 +41,9 @@ func (h *APIHandler) ListAssignments(c *gin.Context) {
pageSize = 20
}
if pageSize > 100 {
pageSize = 100 // Maximum page size to prevent abuse
pageSize = 100
}
// Use paginated methods for filtered queries
switch filter {
case "completed":
result, err := h.assignmentService.GetCompletedByUserPaginated(userID, page, pageSize)
@@ -56,12 +52,12 @@ func (h *APIHandler) ListAssignments(c *gin.Context) {
return
}
c.JSON(http.StatusOK, gin.H{
"assignments": result.Assignments,
"count": len(result.Assignments),
"total_count": result.TotalCount,
"total_pages": result.TotalPages,
"current_page": result.CurrentPage,
"page_size": result.PageSize,
"assignments": result.Assignments,
"count": len(result.Assignments),
"total_count": result.TotalCount,
"total_pages": result.TotalPages,
"current_page": result.CurrentPage,
"page_size": result.PageSize,
})
return
case "overdue":
@@ -71,12 +67,12 @@ func (h *APIHandler) ListAssignments(c *gin.Context) {
return
}
c.JSON(http.StatusOK, gin.H{
"assignments": result.Assignments,
"count": len(result.Assignments),
"total_count": result.TotalCount,
"total_pages": result.TotalPages,
"current_page": result.CurrentPage,
"page_size": result.PageSize,
"assignments": result.Assignments,
"count": len(result.Assignments),
"total_count": result.TotalCount,
"total_pages": result.TotalPages,
"current_page": result.CurrentPage,
"page_size": result.PageSize,
})
return
case "pending":
@@ -86,23 +82,21 @@ func (h *APIHandler) ListAssignments(c *gin.Context) {
return
}
c.JSON(http.StatusOK, gin.H{
"assignments": result.Assignments,
"count": len(result.Assignments),
"total_count": result.TotalCount,
"total_pages": result.TotalPages,
"current_page": result.CurrentPage,
"page_size": result.PageSize,
"assignments": result.Assignments,
"count": len(result.Assignments),
"total_count": result.TotalCount,
"total_pages": result.TotalPages,
"current_page": result.CurrentPage,
"page_size": result.PageSize,
})
return
default:
// For "all" filter, use simple pagination without a dedicated method
assignments, err := h.assignmentService.GetAllByUser(userID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch assignments"})
return
}
// Manual pagination for all assignments
totalCount := len(assignments)
totalPages := (totalCount + pageSize - 1) / pageSize
start := (page - 1) * pageSize
@@ -115,18 +109,16 @@ func (h *APIHandler) ListAssignments(c *gin.Context) {
}
c.JSON(http.StatusOK, gin.H{
"assignments": assignments[start:end],
"count": end - start,
"total_count": totalCount,
"total_pages": totalPages,
"current_page": page,
"page_size": pageSize,
"assignments": assignments[start:end],
"count": end - start,
"total_count": totalCount,
"total_pages": totalPages,
"current_page": page,
"page_size": pageSize,
})
}
}
// ListPendingAssignments returns pending assignments with pagination
// GET /api/v1/assignments/pending?page=1&page_size=20
func (h *APIHandler) ListPendingAssignments(c *gin.Context) {
userID := h.getUserID(c)
page, pageSize := h.parsePagination(c)
@@ -140,8 +132,6 @@ func (h *APIHandler) ListPendingAssignments(c *gin.Context) {
h.sendPaginatedResponse(c, result)
}
// ListCompletedAssignments returns completed assignments with pagination
// GET /api/v1/assignments/completed?page=1&page_size=20
func (h *APIHandler) ListCompletedAssignments(c *gin.Context) {
userID := h.getUserID(c)
page, pageSize := h.parsePagination(c)
@@ -155,8 +145,6 @@ func (h *APIHandler) ListCompletedAssignments(c *gin.Context) {
h.sendPaginatedResponse(c, result)
}
// ListOverdueAssignments returns overdue assignments with pagination
// GET /api/v1/assignments/overdue?page=1&page_size=20
func (h *APIHandler) ListOverdueAssignments(c *gin.Context) {
userID := h.getUserID(c)
page, pageSize := h.parsePagination(c)
@@ -170,8 +158,6 @@ func (h *APIHandler) ListOverdueAssignments(c *gin.Context) {
h.sendPaginatedResponse(c, result)
}
// ListDueTodayAssignments returns assignments due today
// GET /api/v1/assignments/due-today
func (h *APIHandler) ListDueTodayAssignments(c *gin.Context) {
userID := h.getUserID(c)
@@ -187,8 +173,6 @@ func (h *APIHandler) ListDueTodayAssignments(c *gin.Context) {
})
}
// ListDueThisWeekAssignments returns assignments due within this week
// GET /api/v1/assignments/due-this-week
func (h *APIHandler) ListDueThisWeekAssignments(c *gin.Context) {
userID := h.getUserID(c)
@@ -204,7 +188,6 @@ func (h *APIHandler) ListDueThisWeekAssignments(c *gin.Context) {
})
}
// parsePagination extracts and validates pagination parameters
func (h *APIHandler) parsePagination(c *gin.Context) (page int, pageSize int) {
page, _ = strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ = strconv.Atoi(c.DefaultQuery("page_size", "20"))
@@ -221,7 +204,6 @@ func (h *APIHandler) parsePagination(c *gin.Context) (page int, pageSize int) {
return page, pageSize
}
// sendPaginatedResponse sends a standard paginated JSON response
func (h *APIHandler) sendPaginatedResponse(c *gin.Context, result *service.PaginatedResult) {
c.JSON(http.StatusOK, gin.H{
"assignments": result.Assignments,
@@ -233,8 +215,6 @@ func (h *APIHandler) sendPaginatedResponse(c *gin.Context, result *service.Pagin
})
}
// GetAssignment returns a single assignment by ID
// GET /api/v1/assignments/:id
func (h *APIHandler) GetAssignment(c *gin.Context) {
userID := h.getUserID(c)
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
@@ -252,40 +232,121 @@ func (h *APIHandler) GetAssignment(c *gin.Context) {
c.JSON(http.StatusOK, assignment)
}
// CreateAssignmentInput represents the JSON input for creating an assignment
type CreateAssignmentInput struct {
Title string `json:"title" binding:"required"`
Description string `json:"description"`
Subject string `json:"subject"`
Priority string `json:"priority"` // low, medium, high (default: medium)
DueDate string `json:"due_date" binding:"required"` // RFC3339 or 2006-01-02T15:04
Priority string `json:"priority"`
DueDate string `json:"due_date" binding:"required"`
ReminderEnabled bool `json:"reminder_enabled"`
ReminderAt string `json:"reminder_at"`
UrgentReminderEnabled *bool `json:"urgent_reminder_enabled"`
Recurrence struct {
Type string `json:"type"`
Interval int `json:"interval"`
Weekday interface{} `json:"weekday"`
Day interface{} `json:"day"`
Until struct {
Type string `json:"type"`
Count int `json:"count"`
Date string `json:"date"`
} `json:"until"`
} `json:"recurrence"`
}
// CreateAssignment creates a new assignment
// POST /api/v1/assignments
func (h *APIHandler) CreateAssignment(c *gin.Context) {
userID := h.getUserID(c)
var input CreateAssignmentInput
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid input: title and due_date are required"})
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid input: " + err.Error()})
return
}
dueDate, err := time.Parse(time.RFC3339, input.DueDate)
dueDate, err := parseDateString(input.DueDate)
if err != nil {
dueDate, err = time.ParseInLocation("2006-01-02T15:04", input.DueDate, time.Local)
if err != nil {
dueDate, err = time.ParseInLocation("2006-01-02", input.DueDate, time.Local)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid due_date format. Use RFC3339 or 2006-01-02T15:04"})
return
}
dueDate = dueDate.Add(23*time.Hour + 59*time.Minute)
}
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid due_date format. Use RFC3339 or 2006-01-02T15:04"})
return
}
assignment, err := h.assignmentService.Create(userID, input.Title, input.Description, input.Subject, input.Priority, dueDate, false, nil, true)
var reminderAt *time.Time
if input.ReminderEnabled && input.ReminderAt != "" {
reminderTime, err := parseDateString(input.ReminderAt)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid reminder_at format"})
return
}
reminderAt = &reminderTime
}
urgentReminder := true
if input.UrgentReminderEnabled != nil {
urgentReminder = *input.UrgentReminderEnabled
}
if input.Recurrence.Type != "" && input.Recurrence.Type != "none" {
serviceInput := service.CreateRecurringAssignmentInput{
Title: input.Title,
Description: input.Description,
Subject: input.Subject,
Priority: input.Priority,
FirstDueDate: dueDate,
DueTime: dueDate.Format("15:04"),
RecurrenceType: input.Recurrence.Type,
RecurrenceInterval: input.Recurrence.Interval,
ReminderEnabled: input.ReminderEnabled,
ReminderOffset: nil,
UrgentReminderEnabled: urgentReminder,
}
if serviceInput.RecurrenceInterval < 1 {
serviceInput.RecurrenceInterval = 1
}
if input.Recurrence.Weekday != nil {
if wd, ok := input.Recurrence.Weekday.(float64); ok {
wdInt := int(wd)
serviceInput.RecurrenceWeekday = &wdInt
}
}
if input.Recurrence.Day != nil {
if d, ok := input.Recurrence.Day.(float64); ok {
dInt := int(d)
serviceInput.RecurrenceDay = &dInt
}
}
serviceInput.EndType = input.Recurrence.Until.Type
if serviceInput.EndType == "" {
serviceInput.EndType = "never"
}
if serviceInput.EndType == "count" {
count := input.Recurrence.Until.Count
serviceInput.EndCount = &count
} else if serviceInput.EndType == "date" && input.Recurrence.Until.Date != "" {
endDate, err := parseDateString(input.Recurrence.Until.Date)
if err == nil {
serviceInput.EndDate = &endDate
}
}
recurring, err := h.recurringService.Create(userID, serviceInput)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create recurring assignment: " + err.Error()})
return
}
c.JSON(http.StatusCreated, gin.H{
"message": "Recurring assignment created",
"recurring_assignment": recurring,
})
return
}
assignment, err := h.assignmentService.Create(userID, input.Title, input.Description, input.Subject, input.Priority, dueDate, input.ReminderEnabled, reminderAt, urgentReminder)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create assignment"})
return
@@ -294,17 +355,17 @@ func (h *APIHandler) CreateAssignment(c *gin.Context) {
c.JSON(http.StatusCreated, assignment)
}
// UpdateAssignmentInput represents the JSON input for updating an assignment
type UpdateAssignmentInput struct {
Title string `json:"title"`
Description string `json:"description"`
Subject string `json:"subject"`
Priority string `json:"priority"`
DueDate string `json:"due_date"`
Title string `json:"title"`
Description string `json:"description"`
Subject string `json:"subject"`
Priority string `json:"priority"`
DueDate string `json:"due_date"`
ReminderEnabled *bool `json:"reminder_enabled"`
ReminderAt string `json:"reminder_at"`
UrgentReminderEnabled *bool `json:"urgent_reminder_enabled"`
}
// UpdateAssignment updates an existing assignment
// PUT /api/v1/assignments/:id
func (h *APIHandler) UpdateAssignment(c *gin.Context) {
userID := h.getUserID(c)
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
@@ -313,7 +374,6 @@ func (h *APIHandler) UpdateAssignment(c *gin.Context) {
return
}
// Get existing assignment
existing, err := h.assignmentService.GetByID(userID, uint(id))
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Assignment not found"})
@@ -326,14 +386,21 @@ func (h *APIHandler) UpdateAssignment(c *gin.Context) {
return
}
// Use existing values if not provided
title := input.Title
if title == "" {
title = existing.Title
}
description := input.Description
if description == "" {
description = existing.Description
}
subject := input.Subject
if subject == "" {
subject = existing.Subject
}
priority := input.Priority
if priority == "" {
priority = existing.Priority
@@ -341,18 +408,36 @@ func (h *APIHandler) UpdateAssignment(c *gin.Context) {
dueDate := existing.DueDate
if input.DueDate != "" {
dueDate, err = time.Parse(time.RFC3339, input.DueDate)
parsedDate, err := parseDateString(input.DueDate)
if err != nil {
dueDate, err = time.ParseInLocation("2006-01-02T15:04", input.DueDate, time.Local)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid due_date format"})
return
}
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid due_date format"})
return
}
dueDate = parsedDate
}
// Preserve existing reminder settings for API updates
assignment, err := h.assignmentService.Update(userID, uint(id), title, description, subject, priority, dueDate, existing.ReminderEnabled, existing.ReminderAt, existing.UrgentReminderEnabled)
reminderEnabled := existing.ReminderEnabled
if input.ReminderEnabled != nil {
reminderEnabled = *input.ReminderEnabled
}
reminderAt := existing.ReminderAt
if input.ReminderAt != "" {
parsedReminderAt, err := parseDateString(input.ReminderAt)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid reminder_at format"})
return
}
reminderAt = &parsedReminderAt
} else if input.ReminderEnabled != nil && !*input.ReminderEnabled {
}
urgentReminderEnabled := existing.UrgentReminderEnabled
if input.UrgentReminderEnabled != nil {
urgentReminderEnabled = *input.UrgentReminderEnabled
}
assignment, err := h.assignmentService.Update(userID, uint(id), title, description, subject, priority, dueDate, reminderEnabled, reminderAt, urgentReminderEnabled)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update assignment"})
return
@@ -361,8 +446,6 @@ func (h *APIHandler) UpdateAssignment(c *gin.Context) {
c.JSON(http.StatusOK, assignment)
}
// DeleteAssignment deletes an assignment
// DELETE /api/v1/assignments/:id
func (h *APIHandler) DeleteAssignment(c *gin.Context) {
userID := h.getUserID(c)
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
@@ -371,6 +454,26 @@ func (h *APIHandler) DeleteAssignment(c *gin.Context) {
return
}
deleteRecurring := c.Query("delete_recurring") == "true"
if deleteRecurring {
assignment, err := h.assignmentService.GetByID(userID, uint(id))
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Assignment not found"})
return
}
if assignment.RecurringAssignmentID != nil {
if err := h.recurringService.Delete(userID, *assignment.RecurringAssignmentID, false); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete recurring assignment"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Assignment and recurring settings deleted"})
return
}
}
if err := h.assignmentService.Delete(userID, uint(id)); err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Assignment not found"})
return
@@ -379,8 +482,6 @@ func (h *APIHandler) DeleteAssignment(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "Assignment deleted"})
}
// ToggleAssignment toggles the completion status of an assignment
// PATCH /api/v1/assignments/:id/toggle
func (h *APIHandler) ToggleAssignment(c *gin.Context) {
userID := h.getUserID(c)
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
@@ -398,18 +499,14 @@ func (h *APIHandler) ToggleAssignment(c *gin.Context) {
c.JSON(http.StatusOK, assignment)
}
// GetStatistics returns statistics for the authenticated user
// GET /api/v1/statistics?subject=数学&from=2025-01-01&to=2025-12-31&include_archived=true
func (h *APIHandler) GetStatistics(c *gin.Context) {
userID := h.getUserID(c)
// Parse filter parameters
filter := service.StatisticsFilter{
Subject: c.Query("subject"),
IncludeArchived: c.Query("include_archived") == "true",
}
// Parse from date
if fromStr := c.Query("from"); fromStr != "" {
fromDate, err := time.ParseInLocation("2006-01-02", fromStr, time.Local)
if err != nil {
@@ -419,7 +516,6 @@ func (h *APIHandler) GetStatistics(c *gin.Context) {
filter.From = &fromDate
}
// Parse to date
if toStr := c.Query("to"); toStr != "" {
toDate, err := time.ParseInLocation("2006-01-02", toStr, time.Local)
if err != nil {
@@ -438,3 +534,18 @@ func (h *APIHandler) GetStatistics(c *gin.Context) {
c.JSON(http.StatusOK, stats)
}
func parseDateString(dateStr string) (time.Time, error) {
t, err := time.Parse(time.RFC3339, dateStr)
if err == nil {
return t, nil
}
t, err = time.ParseInLocation("2006-01-02T15:04", dateStr, time.Local)
if err == nil {
return t, nil
}
t, err = time.ParseInLocation("2006-01-02", dateStr, time.Local)
if err == nil {
return t.Add(23*time.Hour + 59*time.Minute), nil
}
return time.Time{}, err
}

View File

@@ -0,0 +1,167 @@
package handler
import (
"net/http"
"strconv"
"homework-manager/internal/middleware"
"homework-manager/internal/models"
"homework-manager/internal/service"
"github.com/gin-gonic/gin"
)
type APIRecurringHandler struct {
recurringService *service.RecurringAssignmentService
}
func NewAPIRecurringHandler() *APIRecurringHandler {
return &APIRecurringHandler{
recurringService: service.NewRecurringAssignmentService(),
}
}
func (h *APIRecurringHandler) getUserID(c *gin.Context) uint {
userID, _ := c.Get(middleware.UserIDKey)
return userID.(uint)
}
func (h *APIRecurringHandler) ListRecurring(c *gin.Context) {
userID := h.getUserID(c)
recurringList, err := h.recurringService.GetAllByUser(userID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch recurring assignments"})
return
}
c.JSON(http.StatusOK, gin.H{
"recurring_assignments": recurringList,
"count": len(recurringList),
})
}
func (h *APIRecurringHandler) GetRecurring(c *gin.Context) {
userID := h.getUserID(c)
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"})
return
}
recurring, err := h.recurringService.GetByID(userID, uint(id))
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Recurring assignment not found"})
return
}
c.JSON(http.StatusOK, recurring)
}
type UpdateRecurringAPIInput struct {
Title *string `json:"title"`
Description *string `json:"description"`
Subject *string `json:"subject"`
Priority *string `json:"priority"`
RecurrenceType *string `json:"recurrence_type"`
RecurrenceInterval *int `json:"recurrence_interval"`
RecurrenceWeekday *int `json:"recurrence_weekday"`
RecurrenceDay *int `json:"recurrence_day"`
DueTime *string `json:"due_time"`
EndType *string `json:"end_type"`
EndCount *int `json:"end_count"`
EndDate *string `json:"end_date"` // YYYY-MM-DD
IsActive *bool `json:"is_active"` // To stop/resume
ReminderEnabled *bool `json:"reminder_enabled"`
ReminderOffset *int `json:"reminder_offset"`
UrgentReminderEnabled *bool `json:"urgent_reminder_enabled"`
EditBehavior string `json:"edit_behavior"` // this_only, this_and_future, all (default: this_only)
}
func (h *APIRecurringHandler) UpdateRecurring(c *gin.Context) {
userID := h.getUserID(c)
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"})
return
}
var input UpdateRecurringAPIInput
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid input: " + err.Error()})
return
}
existing, err := h.recurringService.GetByID(userID, uint(id))
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Recurring assignment not found"})
return
}
if input.IsActive != nil {
if err := h.recurringService.SetActive(userID, uint(id), *input.IsActive); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update active status"})
return
}
existing.IsActive = *input.IsActive
}
serviceInput := service.UpdateRecurringInput{
Title: input.Title,
Description: input.Description,
Subject: input.Subject,
Priority: input.Priority,
RecurrenceType: input.RecurrenceType,
RecurrenceInterval: input.RecurrenceInterval,
RecurrenceWeekday: input.RecurrenceWeekday,
RecurrenceDay: input.RecurrenceDay,
DueTime: input.DueTime,
EndType: input.EndType,
EndCount: input.EndCount,
EditBehavior: input.EditBehavior,
ReminderEnabled: input.ReminderEnabled,
ReminderOffset: input.ReminderOffset,
UrgentReminderEnabled: input.UrgentReminderEnabled,
}
if input.EndDate != nil && *input.EndDate != "" {
endDate, err := parseDateString(*input.EndDate)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid end_date format"})
return
}
serviceInput.EndDate = &endDate
}
if serviceInput.EditBehavior == "" {
serviceInput.EditBehavior = models.EditBehaviorThisOnly
}
updated, err := h.recurringService.Update(userID, uint(id), serviceInput)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update recurring assignment"})
return
}
updated.IsActive = existing.IsActive
c.JSON(http.StatusOK, updated)
}
func (h *APIRecurringHandler) DeleteRecurring(c *gin.Context) {
userID := h.getUserID(c)
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"})
return
}
err = h.recurringService.Delete(userID, uint(id), false)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Recurring assignment not found or failed to delete"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Recurring assignment deleted"})
}

View File

@@ -16,12 +16,14 @@ import (
type AssignmentHandler struct {
assignmentService *service.AssignmentService
notificationService *service.NotificationService
recurringService *service.RecurringAssignmentService
}
func NewAssignmentHandler(notificationService *service.NotificationService) *AssignmentHandler {
return &AssignmentHandler{
assignmentService: service.NewAssignmentService(),
notificationService: notificationService,
recurringService: service.NewRecurringAssignmentService(),
}
}
@@ -104,11 +106,14 @@ func (h *AssignmentHandler) Index(c *gin.Context) {
func (h *AssignmentHandler) New(c *gin.Context) {
role, _ := c.Get(middleware.UserRoleKey)
name, _ := c.Get(middleware.UserNameKey)
now := time.Now()
RenderHTML(c, http.StatusOK, "assignments/new.html", gin.H{
"title": "課題登録",
"isAdmin": role == "admin",
"userName": name,
"title": "課題登録",
"isAdmin": role == "admin",
"userName": name,
"currentWeekday": int(now.Weekday()),
"currentDay": now.Day(),
})
}
@@ -196,7 +201,7 @@ func (h *AssignmentHandler) Create(c *gin.Context) {
dueTime := dueDate.Format("15:04")
recurringService := service.NewRecurringAssignmentService()
input := service.CreateRecurringInput{
input := service.CreateRecurringAssignmentInput{
Title: title,
Description: description,
Subject: subject,
@@ -266,12 +271,18 @@ func (h *AssignmentHandler) Edit(c *gin.Context) {
return
}
var recurring *models.RecurringAssignment
if assignment.RecurringAssignmentID != nil {
recurring, _ = h.recurringService.GetByID(userID, *assignment.RecurringAssignmentID)
}
role, _ := c.Get(middleware.UserRoleKey)
name, _ := c.Get(middleware.UserNameKey)
RenderHTML(c, http.StatusOK, "assignments/edit.html", gin.H{
"title": "課題編集",
"assignment": assignment,
"recurring": recurring,
"isAdmin": role == "admin",
"userName": name,
})
@@ -333,6 +344,14 @@ func (h *AssignmentHandler) Delete(c *gin.Context) {
userID := h.getUserID(c)
id, _ := strconv.ParseUint(c.Param("id"), 10, 32)
deleteRecurringStr := c.Query("stop_recurring")
if deleteRecurringStr != "" {
recurringID, err := strconv.ParseUint(deleteRecurringStr, 10, 32)
if err == nil {
h.recurringService.Delete(userID, uint(recurringID), false)
}
}
h.assignmentService.Delete(userID, uint(id))
c.Redirect(http.StatusFound, "/assignments")
@@ -417,3 +436,162 @@ func (h *AssignmentHandler) UnarchiveSubject(c *gin.Context) {
c.Redirect(http.StatusFound, "/statistics?include_archived=true")
}
func (h *AssignmentHandler) StopRecurring(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
}
h.recurringService.SetActive(userID, uint(id), false)
c.Redirect(http.StatusFound, "/assignments")
}
func (h *AssignmentHandler) ResumeRecurring(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
}
h.recurringService.SetActive(userID, uint(id), true)
referer := c.Request.Referer()
if referer == "" {
referer = "/assignments"
}
c.Redirect(http.StatusFound, referer)
}
func (h *AssignmentHandler) ListRecurring(c *gin.Context) {
userID := h.getUserID(c)
role, _ := c.Get(middleware.UserRoleKey)
name, _ := c.Get(middleware.UserNameKey)
recurrings, err := h.recurringService.GetAllByUser(userID)
if err != nil {
recurrings = []models.RecurringAssignment{}
}
RenderHTML(c, http.StatusOK, "recurring/index.html", gin.H{
"title": "繰り返し設定一覧",
"recurrings": recurrings,
"isAdmin": role == "admin",
"userName": name,
})
}
func (h *AssignmentHandler) EditRecurring(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
}
recurring, err := h.recurringService.GetByID(userID, uint(id))
if err != nil {
c.Redirect(http.StatusFound, "/assignments")
return
}
role, _ := c.Get(middleware.UserRoleKey)
name, _ := c.Get(middleware.UserNameKey)
RenderHTML(c, http.StatusOK, "recurring/edit.html", gin.H{
"title": "繰り返し課題の編集",
"recurring": recurring,
"isAdmin": role == "admin",
"userName": name,
})
}
func (h *AssignmentHandler) UpdateRecurring(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
}
title := c.PostForm("title")
description := c.PostForm("description")
subject := c.PostForm("subject")
priority := c.PostForm("priority")
recurrenceType := c.PostForm("recurrence_type")
dueTime := c.PostForm("due_time")
editBehavior := c.PostForm("edit_behavior")
recurrenceInterval := 1
if v, err := strconv.Atoi(c.PostForm("recurrence_interval")); err == nil && v > 0 {
recurrenceInterval = v
}
var recurrenceWeekday *int
if wd := c.PostForm("recurrence_weekday"); wd != "" {
if v, err := strconv.Atoi(wd); err == nil && v >= 0 && v <= 6 {
recurrenceWeekday = &v
}
}
var recurrenceDay *int
if d := c.PostForm("recurrence_day"); d != "" {
if v, err := strconv.Atoi(d); err == nil && v >= 1 && v <= 31 {
recurrenceDay = &v
}
}
endType := c.PostForm("end_type")
var endCount *int
if ec := c.PostForm("end_count"); ec != "" {
if v, err := strconv.Atoi(ec); err == nil && v > 0 {
endCount = &v
}
}
var endDate *time.Time
if ed := c.PostForm("end_date"); ed != "" {
if v, err := time.ParseInLocation("2006-01-02", ed, time.Local); err == nil {
endDate = &v
}
}
input := service.UpdateRecurringInput{
Title: &title,
Description: &description,
Subject: &subject,
Priority: &priority,
RecurrenceType: &recurrenceType,
RecurrenceInterval: &recurrenceInterval,
RecurrenceWeekday: recurrenceWeekday,
RecurrenceDay: recurrenceDay,
DueTime: &dueTime,
EndType: &endType,
EndCount: endCount,
EndDate: endDate,
EditBehavior: editBehavior,
}
_, err = h.recurringService.Update(userID, uint(id), input)
if err != nil {
c.Redirect(http.StatusFound, "/recurring/"+c.Param("id")+"/edit")
return
}
c.Redirect(http.StatusFound, "/assignments")
}
func (h *AssignmentHandler) DeleteRecurring(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
}
h.recurringService.Delete(userID, uint(id), false)
c.Redirect(http.StatusFound, "/assignments")
}

View File

@@ -346,3 +346,59 @@ func (r *AssignmentRepository) GetSubjectsByUserIDWithArchived(userID uint, incl
err := query.Distinct("subject").Pluck("subject", &subjects).Error
return subjects, err
}
func (r *AssignmentRepository) SearchWithPreload(userID uint, queryStr, priority, filter string, page, pageSize int) ([]models.Assignment, int64, error) {
var assignments []models.Assignment
var totalCount int64
dbQuery := r.db.Model(&models.Assignment{}).Where("user_id = ?", userID)
if queryStr != "" {
dbQuery = dbQuery.Where("title LIKE ? OR description LIKE ?", "%"+queryStr+"%", "%"+queryStr+"%")
}
if priority != "" {
dbQuery = dbQuery.Where("priority = ?", priority)
}
now := time.Now()
startOfDay := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
endOfDay := startOfDay.AddDate(0, 0, 1)
weekLater := startOfDay.AddDate(0, 0, 7)
switch filter {
case "completed":
dbQuery = dbQuery.Where("is_completed = ?", true)
case "overdue":
dbQuery = dbQuery.Where("is_completed = ? AND due_date < ?", false, now)
case "due_today":
dbQuery = dbQuery.Where("is_completed = ? AND due_date >= ? AND due_date < ?", false, startOfDay, endOfDay)
case "due_this_week":
dbQuery = dbQuery.Where("is_completed = ? AND due_date >= ? AND due_date < ?", false, startOfDay, weekLater)
case "recurring":
dbQuery = dbQuery.Where("recurring_assignment_id IS NOT NULL")
default:
dbQuery = dbQuery.Where("is_completed = ?", false)
}
if err := dbQuery.Count(&totalCount).Error; err != nil {
return nil, 0, err
}
if filter == "completed" {
dbQuery = dbQuery.Order("completed_at DESC")
} else {
dbQuery = dbQuery.Order("due_date ASC")
}
if page < 1 {
page = 1
}
if pageSize < 1 {
pageSize = 10
}
offset := (page - 1) * pageSize
err := dbQuery.Preload("RecurringAssignment").Limit(pageSize).Offset(offset).Find(&assignments).Error
return assignments, totalCount, err
}

View File

@@ -48,6 +48,19 @@ func getFuncMap() template.FuncMap {
"recurringLabel": service.GetRecurrenceTypeLabel,
"endTypeLabel": service.GetEndTypeLabel,
"recurringSummary": service.FormatRecurringSummary,
"derefInt": func(i *int) int {
if i == nil {
return 0
}
return *i
},
"seq": func(start, end int) []int {
var result []int
for i := start; i <= end; i++ {
result = append(result, i)
}
return result
},
}
}
@@ -67,6 +80,7 @@ func loadTemplates() (*template.Template, error) {
{"web/templates/auth/*.html", ""},
{"web/templates/pages/*.html", ""},
{"web/templates/assignments/*.html", "assignments/"},
{"web/templates/recurring/*.html", "recurring/"},
{"web/templates/admin/*.html", "admin/"},
}
@@ -187,6 +201,7 @@ func Setup(cfg *config.Config) *gin.Engine {
adminHandler := handler.NewAdminHandler()
profileHandler := handler.NewProfileHandler(notificationService)
apiHandler := handler.NewAPIHandler()
apiRecurringHandler := handler.NewAPIRecurringHandler()
guest := r.Group("/")
guest.Use(middleware.GuestOnly())
@@ -226,6 +241,13 @@ func Setup(cfg *config.Config) *gin.Engine {
auth.POST("/statistics/archive-subject", assignmentHandler.ArchiveSubject)
auth.POST("/statistics/unarchive-subject", assignmentHandler.UnarchiveSubject)
auth.POST("/recurring/:id/stop", assignmentHandler.StopRecurring)
auth.POST("/recurring/:id/resume", assignmentHandler.ResumeRecurring)
auth.POST("/recurring/:id/delete", assignmentHandler.DeleteRecurring)
auth.GET("/recurring", assignmentHandler.ListRecurring)
auth.GET("/recurring/:id/edit", assignmentHandler.EditRecurring)
auth.POST("/recurring/:id", assignmentHandler.UpdateRecurring)
auth.GET("/profile", profileHandler.Show)
auth.POST("/profile", profileHandler.Update)
auth.POST("/profile/password", profileHandler.ChangePassword)
@@ -258,7 +280,13 @@ func Setup(cfg *config.Config) *gin.Engine {
api.PUT("/assignments/:id", apiHandler.UpdateAssignment)
api.DELETE("/assignments/:id", apiHandler.DeleteAssignment)
api.PATCH("/assignments/:id/toggle", apiHandler.ToggleAssignment)
api.GET("/statistics", apiHandler.GetStatistics)
api.GET("/recurring", apiRecurringHandler.ListRecurring)
api.GET("/recurring/:id", apiRecurringHandler.GetRecurring)
api.PUT("/recurring/:id", apiRecurringHandler.UpdateRecurring)
api.DELETE("/recurring/:id", apiRecurringHandler.DeleteRecurring)
}
return r

View File

@@ -179,7 +179,7 @@ func (s *AssignmentService) SearchAssignments(userID uint, query, priority, filt
pageSize = 10
}
assignments, totalCount, err := s.assignmentRepo.Search(userID, query, priority, filter, page, pageSize)
assignments, totalCount, err := s.assignmentRepo.SearchWithPreload(userID, query, priority, filter, page, pageSize)
if err != nil {
return nil, err
}
@@ -255,11 +255,11 @@ func (s *AssignmentService) GetSubjectsByUser(userID uint) ([]string, error) {
}
type DashboardStats struct {
TotalPending int64
DueToday int
DueThisWeek int
Overdue int
Subjects []string
TotalPending int64
DueToday int
DueThisWeek int
Overdue int
Subjects []string
}
func (s *AssignmentService) GetDashboardStats(userID uint) (*DashboardStats, error) {
@@ -270,11 +270,11 @@ func (s *AssignmentService) GetDashboardStats(userID uint) (*DashboardStats, err
subjects, _ := s.assignmentRepo.GetSubjectsByUserID(userID)
return &DashboardStats{
TotalPending: pending,
DueToday: len(dueToday),
DueThisWeek: len(dueThisWeek),
Overdue: int(overdueCount),
Subjects: subjects,
TotalPending: pending,
DueToday: len(dueToday),
DueThisWeek: len(dueThisWeek),
Overdue: int(overdueCount),
Subjects: subjects,
}, nil
}
@@ -392,4 +392,3 @@ func (s *AssignmentService) GetSubjectsWithArchived(userID uint, includeArchived
func (s *AssignmentService) GetArchivedSubjects(userID uint) ([]string, error) {
return s.assignmentRepo.GetArchivedSubjects(userID)
}

View File

@@ -30,7 +30,7 @@ func NewRecurringAssignmentService() *RecurringAssignmentService {
}
}
type CreateRecurringInput struct {
type CreateRecurringAssignmentInput struct {
Title string
Description string
Subject string
@@ -50,7 +50,7 @@ type CreateRecurringInput struct {
FirstDueDate time.Time
}
func (s *RecurringAssignmentService) Create(userID uint, input CreateRecurringInput) (*models.RecurringAssignment, error) {
func (s *RecurringAssignmentService) Create(userID uint, input CreateRecurringAssignmentInput) (*models.RecurringAssignment, error) {
if !isValidRecurrenceType(input.RecurrenceType) {
return nil, ErrInvalidRecurrenceType
}
@@ -121,15 +121,22 @@ func (s *RecurringAssignmentService) GetActiveByUser(userID uint) ([]models.Recu
}
type UpdateRecurringInput struct {
Title string
Description string
Subject string
Priority string
DueTime string
Title *string
Description *string
Subject *string
Priority *string
RecurrenceType *string
RecurrenceInterval *int
RecurrenceWeekday *int
RecurrenceDay *int
DueTime *string
EndType *string
EndCount *int
EndDate *time.Time
EditBehavior string
ReminderEnabled bool
ReminderEnabled *bool
ReminderOffset *int
UrgentReminderEnabled bool
UrgentReminderEnabled *bool
}
func (s *RecurringAssignmentService) Update(userID, recurringID uint, input UpdateRecurringInput) (*models.RecurringAssignment, error) {
@@ -138,19 +145,56 @@ func (s *RecurringAssignmentService) Update(userID, recurringID uint, input Upda
return nil, err
}
recurring.Title = input.Title
recurring.Description = input.Description
recurring.Subject = input.Subject
recurring.Priority = input.Priority
if input.DueTime != "" {
recurring.DueTime = input.DueTime
if input.Title != nil {
recurring.Title = *input.Title
}
if input.Description != nil {
recurring.Description = *input.Description
}
if input.Subject != nil {
recurring.Subject = *input.Subject
}
if input.Priority != nil {
recurring.Priority = *input.Priority
}
if input.DueTime != nil {
recurring.DueTime = *input.DueTime
}
if input.EditBehavior != "" {
recurring.EditBehavior = input.EditBehavior
}
recurring.ReminderEnabled = input.ReminderEnabled
recurring.ReminderOffset = input.ReminderOffset
recurring.UrgentReminderEnabled = input.UrgentReminderEnabled
if input.ReminderEnabled != nil {
recurring.ReminderEnabled = *input.ReminderEnabled
}
if input.ReminderOffset != nil {
recurring.ReminderOffset = input.ReminderOffset
}
if input.UrgentReminderEnabled != nil {
recurring.UrgentReminderEnabled = *input.UrgentReminderEnabled
}
if input.RecurrenceType != nil && *input.RecurrenceType != "" && isValidRecurrenceType(*input.RecurrenceType) {
recurring.RecurrenceType = *input.RecurrenceType
}
if input.RecurrenceInterval != nil && *input.RecurrenceInterval > 0 {
recurring.RecurrenceInterval = *input.RecurrenceInterval
}
if input.RecurrenceWeekday != nil {
recurring.RecurrenceWeekday = input.RecurrenceWeekday
}
if input.RecurrenceDay != nil {
recurring.RecurrenceDay = input.RecurrenceDay
}
if input.EndType != nil && isValidEndType(*input.EndType) {
recurring.EndType = *input.EndType
}
if input.EndCount != nil {
recurring.EndCount = input.EndCount
}
if input.EndDate != nil {
recurring.EndDate = input.EndDate
}
if err := s.recurringRepo.Update(recurring); err != nil {
return nil, err
@@ -159,6 +203,16 @@ func (s *RecurringAssignmentService) Update(userID, recurringID uint, input Upda
return recurring, nil
}
func (s *RecurringAssignmentService) SetActive(userID, recurringID uint, isActive bool) error {
recurring, err := s.GetByID(userID, recurringID)
if err != nil {
return err
}
recurring.IsActive = isActive
return s.recurringRepo.Update(recurring)
}
func (s *RecurringAssignmentService) UpdateAssignmentWithBehavior(
userID uint,
assignment *models.Assignment,