Files
Super-HomeworkManager/internal/handler/assignment_handler.go
2026-04-26 15:38:38 +09:00

807 lines
23 KiB
Go

package handler
import (
"encoding/csv"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"homework-manager/internal/middleware"
"homework-manager/internal/models"
"homework-manager/internal/service"
"homework-manager/internal/validation"
"github.com/gin-gonic/gin"
)
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(),
}
}
func (h *AssignmentHandler) getUserID(c *gin.Context) uint {
userID, _ := c.Get(middleware.UserIDKey)
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)
dueToday, _ := h.assignmentService.GetDueTodayByUser(userID)
overdue, _ := h.assignmentService.GetOverdueByUser(userID)
upcoming, _ := h.assignmentService.GetDueThisWeekByUser(userID)
role, _ := c.Get(middleware.UserRoleKey)
name, _ := c.Get(middleware.UserNameKey)
RenderHTML(c, http.StatusOK, "dashboard.html", gin.H{
"title": "ダッシュボード",
"stats": stats,
"dueToday": dueToday,
"overdue": overdue,
"upcoming": upcoming,
"isAdmin": role == "admin",
"userName": name,
})
}
func (h *AssignmentHandler) Index(c *gin.Context) {
userID := h.getUserID(c)
filter := c.Query("filter")
filter = strings.TrimSpace(filter)
if filter == "" {
filter = "pending"
}
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 {
page = 1
}
const pageSize = 10
result, err := h.assignmentService.SearchAssignments(userID, query, priority, filter, sort, subject, page, pageSize)
var assignments []models.Assignment
var totalPages, currentPage int
if err != nil || result == nil {
assignments = []models.Assignment{}
totalPages = 1
currentPage = 1
} else {
assignments = result.Assignments
totalPages = result.TotalPages
currentPage = result.CurrentPage
}
subjects, _ := h.assignmentService.GetSubjectsByUser(userID)
tabCounts := h.assignmentService.GetTabCounts(userID)
role, _ := c.Get(middleware.UserRoleKey)
name, _ := c.Get(middleware.UserNameKey)
RenderHTML(c, http.StatusOK, "assignments/index.html", gin.H{
"title": "課題一覧",
"assignments": assignments,
"filter": filter,
"query": query,
"priority": priority,
"subject": subject,
"sort": sort,
"subjects": subjects,
"tabCounts": tabCounts,
"isAdmin": role == "admin",
"userName": name,
"currentPage": currentPage,
"totalPages": totalPages,
"hasPrev": currentPage > 1,
"hasNext": currentPage < totalPages,
"prevPage": currentPage - 1,
"nextPage": currentPage + 1,
})
}
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)
now := time.Now()
tomorrow := now.AddDate(0, 0, 1)
defaultDue := time.Date(tomorrow.Year(), tomorrow.Month(), tomorrow.Day(), 23, 59, 0, 0, now.Location())
defaultSoftDue := defaultDue.Add(-48 * time.Hour)
RenderHTML(c, http.StatusOK, "assignments/new.html", gin.H{
"title": "課題登録",
"isAdmin": role == "admin",
"userName": name,
"currentWeekday": int(now.Weekday()),
"currentDay": now.Day(),
"defaultDueDate": defaultDue.Format("2006-01-02T15:04"),
"defaultSoftDueDate": defaultSoftDue.Format("2006-01-02T15:04"),
})
}
func (h *AssignmentHandler) Create(c *gin.Context) {
userID := h.getUserID(c)
title := c.PostForm("title")
description := c.PostForm("description")
subject := c.PostForm("subject")
priority := c.PostForm("priority")
dueDateStr := c.PostForm("due_date")
if err := validation.ValidateAssignmentInput(title, description, subject, priority); err != nil {
role, _ := c.Get(middleware.UserRoleKey)
name, _ := c.Get(middleware.UserNameKey)
RenderHTML(c, http.StatusOK, "assignments/new.html", gin.H{
"title": "課題登録",
"error": err.Error(),
"formTitle": title,
"description": description,
"subject": subject,
"priority": priority,
"isAdmin": role == "admin",
"userName": name,
})
return
}
reminderEnabled := c.PostForm("reminder_enabled") == "on"
reminderAtStr := c.PostForm("reminder_at")
var reminderAt *time.Time
if reminderEnabled && reminderAtStr != "" {
if parsed, err := time.ParseInLocation("2006-01-02T15:04", reminderAtStr, time.Local); err == nil {
reminderAt = &parsed
}
}
urgentReminderEnabled := c.PostForm("urgent_reminder_enabled") == "on"
dueDate, err := time.ParseInLocation("2006-01-02T15:04", dueDateStr, time.Local)
if err != nil {
dueDate, err = time.ParseInLocation("2006-01-02", dueDateStr, time.Local)
if err != nil {
role, _ := c.Get(middleware.UserRoleKey)
name, _ := c.Get(middleware.UserNameKey)
RenderHTML(c, http.StatusOK, "assignments/new.html", gin.H{
"title": "課題登録",
"error": "提出期限の形式が正しくありません",
"formTitle": title,
"description": description,
"subject": subject,
"priority": priority,
"isAdmin": role == "admin",
"userName": name,
})
return
}
dueDate = dueDate.Add(23*time.Hour + 59*time.Minute)
}
var softDueDate *time.Time
if softDueDateStr := c.PostForm("soft_due_date"); softDueDateStr != "" {
if parsed, err := time.ParseInLocation("2006-01-02T15:04", softDueDateStr, time.Local); err == nil {
softDueDate = &parsed
} else if parsed, err := time.ParseInLocation("2006-01-02", softDueDateStr, time.Local); err == nil {
p := parsed.Add(23*time.Hour + 59*time.Minute)
softDueDate = &p
}
}
recurrenceType := c.PostForm("recurrence_type")
if recurrenceType != "" && recurrenceType != "none" {
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")
if endType == "" {
endType = models.EndTypeNever
}
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
}
}
dueTime := dueDate.Format("15:04")
generationLeadDays := 0
if v, err := strconv.Atoi(c.PostForm("generation_lead_days")); err == nil && v >= 0 {
generationLeadDays = v
}
generationLeadTime := c.PostForm("generation_lead_time")
recurringService := service.NewRecurringAssignmentService()
input := service.CreateRecurringAssignmentInput{
Title: title,
Description: description,
Subject: subject,
Priority: priority,
RecurrenceType: recurrenceType,
RecurrenceInterval: recurrenceInterval,
RecurrenceWeekday: recurrenceWeekday,
RecurrenceDay: recurrenceDay,
DueTime: dueTime,
EndType: endType,
EndCount: endCount,
EndDate: endDate,
ReminderEnabled: reminderEnabled,
UrgentReminderEnabled: urgentReminderEnabled,
GenerationLeadDays: generationLeadDays,
GenerationLeadTime: generationLeadTime,
FirstDueDate: dueDate,
}
_, err = recurringService.Create(userID, input)
if err != nil {
role, _ := c.Get(middleware.UserRoleKey)
name, _ := c.Get(middleware.UserNameKey)
RenderHTML(c, http.StatusOK, "assignments/new.html", gin.H{
"title": "課題登録",
"error": "繰り返し課題の登録に失敗しました: " + err.Error(),
"formTitle": title,
"description": description,
"subject": subject,
"priority": priority,
"isAdmin": role == "admin",
"userName": name,
})
return
}
} else {
assignment, err := h.assignmentService.Create(userID, title, description, subject, priority, dueDate, softDueDate, reminderEnabled, reminderAt, urgentReminderEnabled)
if err != nil {
role, _ := c.Get(middleware.UserRoleKey)
name, _ := c.Get(middleware.UserNameKey)
RenderHTML(c, http.StatusOK, "assignments/new.html", gin.H{
"title": "課題登録",
"error": "課題の登録に失敗しました",
"formTitle": title,
"description": description,
"subject": subject,
"priority": priority,
"isAdmin": role == "admin",
"userName": name,
})
return
}
if h.notificationService != nil {
go h.notificationService.SendAssignmentCreatedNotification(userID, assignment)
}
}
c.Redirect(http.StatusFound, "/assignments")
}
func (h *AssignmentHandler) Edit(c *gin.Context) {
userID := h.getUserID(c)
id, _ := strconv.ParseUint(c.Param("id"), 10, 32)
assignment, err := h.assignmentService.GetByID(userID, uint(id))
if err != nil {
c.Redirect(http.StatusFound, "/assignments")
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,
})
}
func (h *AssignmentHandler) Update(c *gin.Context) {
userID := h.getUserID(c)
id, _ := strconv.ParseUint(c.Param("id"), 10, 32)
title := c.PostForm("title")
description := c.PostForm("description")
subject := c.PostForm("subject")
priority := c.PostForm("priority")
dueDateStr := c.PostForm("due_date")
if err := validation.ValidateAssignmentInput(title, description, subject, priority); err != nil {
c.Redirect(http.StatusFound, "/assignments")
return
}
reminderEnabled := c.PostForm("reminder_enabled") == "on"
reminderAtStr := c.PostForm("reminder_at")
var reminderAt *time.Time
if reminderEnabled && reminderAtStr != "" {
if parsed, err := time.ParseInLocation("2006-01-02T15:04", reminderAtStr, time.Local); err == nil {
reminderAt = &parsed
}
}
urgentReminderEnabled := c.PostForm("urgent_reminder_enabled") == "on"
dueDate, err := time.ParseInLocation("2006-01-02T15:04", dueDateStr, time.Local)
if err != nil {
dueDate, err = time.ParseInLocation("2006-01-02", dueDateStr, time.Local)
if err != nil {
c.Redirect(http.StatusFound, "/assignments")
return
}
dueDate = dueDate.Add(23*time.Hour + 59*time.Minute)
}
var softDueDate *time.Time
if softDueDateStr := c.PostForm("soft_due_date"); softDueDateStr != "" {
if parsed, err := time.ParseInLocation("2006-01-02T15:04", softDueDateStr, time.Local); err == nil {
softDueDate = &parsed
} else if parsed, err := time.ParseInLocation("2006-01-02", softDueDateStr, time.Local); err == nil {
p := parsed.Add(23*time.Hour + 59*time.Minute)
softDueDate = &p
}
}
_, err = h.assignmentService.Update(userID, uint(id), title, description, subject, priority, dueDate, softDueDate, reminderEnabled, reminderAt, urgentReminderEnabled)
if err != nil {
c.Redirect(http.StatusFound, "/assignments")
return
}
c.Redirect(http.StatusFound, "/assignments")
}
func (h *AssignmentHandler) Toggle(c *gin.Context) {
userID := h.getUserID(c)
id, _ := strconv.ParseUint(c.Param("id"), 10, 32)
assignment, err := h.assignmentService.ToggleComplete(userID, uint(id))
if err == nil && assignment.IsCompleted && assignment.RecurringAssignmentID != nil {
h.recurringService.TriggerForRecurring(*assignment.RecurringAssignmentID)
}
referer := c.Request.Referer()
if referer == "" {
referer = "/assignments"
}
c.Redirect(http.StatusFound, referer)
}
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")
}
func (h *AssignmentHandler) Statistics(c *gin.Context) {
userID := h.getUserID(c)
role, _ := c.Get(middleware.UserRoleKey)
name, _ := c.Get(middleware.UserNameKey)
filter := service.StatisticsFilter{
Subject: c.Query("subject"),
IncludeArchived: c.Query("include_archived") == "true",
}
fromStr := c.Query("from")
toStr := c.Query("to")
if fromStr != "" {
fromDate, err := time.ParseInLocation("2006-01-02", fromStr, time.Local)
if err == nil {
filter.From = &fromDate
}
}
if toStr != "" {
toDate, err := time.ParseInLocation("2006-01-02", toStr, time.Local)
if err == nil {
filter.To = &toDate
}
}
stats, err := h.assignmentService.GetStatistics(userID, filter)
if err != nil {
RenderHTML(c, http.StatusInternalServerError, "error.html", gin.H{
"title": "エラー",
"message": "統計情報の取得に失敗しました",
})
return
}
subjects, _ := h.assignmentService.GetSubjectsWithArchived(userID, false)
archivedSubjects, _ := h.assignmentService.GetArchivedSubjects(userID)
archivedMap := make(map[string]bool)
for _, s := range archivedSubjects {
archivedMap[s] = true
}
RenderHTML(c, http.StatusOK, "assignments/statistics.html", gin.H{
"title": "統計",
"stats": stats,
"subjects": subjects,
"archivedSubjects": archivedMap,
"selectedSubject": filter.Subject,
"fromDate": fromStr,
"toDate": toStr,
"includeArchived": filter.IncludeArchived,
"isAdmin": role == "admin",
"userName": name,
})
}
func (h *AssignmentHandler) ArchiveSubject(c *gin.Context) {
userID := h.getUserID(c)
subject := c.PostForm("subject")
if subject != "" {
h.assignmentService.ArchiveSubject(userID, subject)
}
c.Redirect(http.StatusFound, "/statistics")
}
func (h *AssignmentHandler) UnarchiveSubject(c *gin.Context) {
userID := h.getUserID(c)
subject := c.PostForm("subject")
if subject != "" {
h.assignmentService.UnarchiveSubject(userID, subject)
}
c.Redirect(http.StatusFound, "/statistics?include_archived=true")
}
func (h *AssignmentHandler) ExportCSV(c *gin.Context) {
userID := h.getUserID(c)
var from, to *time.Time
if fromStr := c.Query("from"); fromStr != "" {
if t, err := time.ParseInLocation("2006-01-02", fromStr, time.Local); err == nil {
from = &t
}
}
if toStr := c.Query("to"); toStr != "" {
if t, err := time.ParseInLocation("2006-01-02", toStr, time.Local); err == nil {
to = &t
}
}
subject := c.Query("subject")
assignments, err := h.assignmentService.GetForExport(userID, from, to, subject)
if err != nil {
c.String(http.StatusInternalServerError, "エクスポートに失敗しました")
return
}
filename := "assignments_" + time.Now().Format("20060102") + ".csv"
c.Header("Content-Type", "text/csv; charset=utf-8")
c.Header("Content-Disposition", "attachment; filename=\""+filename+"\"")
w := csv.NewWriter(c.Writer)
c.Writer.Write([]byte("\xef\xbb\xbf"))
headers := []string{"ID", "タイトル", "科目", "説明", "重要度", "提出期限", "自分の期限", "完了", "完了日時", "登録日時"}
w.Write(headers)
priorityLabel := map[string]string{"low": "低", "medium": "中", "high": "高"}
for _, a := range assignments {
completed := "未完了"
if a.IsCompleted {
completed = "完了"
}
completedAt := ""
if a.CompletedAt != nil {
completedAt = a.CompletedAt.Format("2006/01/02 15:04")
}
softDueDateStr := ""
if a.SoftDueDate != nil {
softDueDateStr = a.SoftDueDate.Format("2006/01/02 15:04")
}
label := priorityLabel[a.Priority]
if label == "" {
label = a.Priority
}
w.Write([]string{
strconv.FormatUint(uint64(a.ID), 10),
a.Title,
a.Subject,
a.Description,
label,
a.DueDate.Format("2006/01/02 15:04"),
softDueDateStr,
completed,
completedAt,
a.CreatedAt.Format("2006/01/02 15:04"),
})
}
w.Flush()
}
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
}
}
generationLeadDays := 0
if v, err := strconv.Atoi(c.PostForm("generation_lead_days")); err == nil && v >= 0 {
generationLeadDays = v
}
generationLeadTime := c.PostForm("generation_lead_time")
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,
GenerationLeadDays: &generationLeadDays,
GenerationLeadTime: &generationLeadTime,
}
_, 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")
}