本来の期限に加えて自分の中での目標期限を設定できるよう仕様追加

This commit is contained in:
2026-04-23 09:40:18 +09:00
parent b2dd70cf27
commit 098f636a65
11 changed files with 151 additions and 32 deletions

View File

@@ -239,6 +239,7 @@ type CreateAssignmentInput struct {
Subject string `json:"subject"`
Priority string `json:"priority"`
DueDate string `json:"due_date" binding:"required"`
SoftDueDate string `json:"soft_due_date"`
ReminderEnabled bool `json:"reminder_enabled"`
ReminderAt string `json:"reminder_at"`
@@ -352,7 +353,17 @@ func (h *APIHandler) CreateAssignment(c *gin.Context) {
return
}
assignment, err := h.assignmentService.Create(userID, input.Title, input.Description, input.Subject, input.Priority, dueDate, input.ReminderEnabled, reminderAt, urgentReminder)
var softDueDate *time.Time
if input.SoftDueDate != "" {
parsedSoft, err := parseDateString(input.SoftDueDate)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid soft_due_date format"})
return
}
softDueDate = &parsedSoft
}
assignment, err := h.assignmentService.Create(userID, input.Title, input.Description, input.Subject, input.Priority, dueDate, softDueDate, input.ReminderEnabled, reminderAt, urgentReminder)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create assignment"})
return
@@ -367,6 +378,7 @@ type UpdateAssignmentInput struct {
Subject string `json:"subject"`
Priority string `json:"priority"`
DueDate string `json:"due_date"`
SoftDueDate string `json:"soft_due_date"`
ReminderEnabled *bool `json:"reminder_enabled"`
ReminderAt string `json:"reminder_at"`
UrgentReminderEnabled *bool `json:"urgent_reminder_enabled"`
@@ -448,7 +460,17 @@ func (h *APIHandler) UpdateAssignment(c *gin.Context) {
urgentReminderEnabled = *input.UrgentReminderEnabled
}
assignment, err := h.assignmentService.Update(userID, uint(id), title, description, subject, priority, dueDate, reminderEnabled, reminderAt, urgentReminderEnabled)
softDueDate := existing.SoftDueDate
if input.SoftDueDate != "" {
parsedSoft, err := parseDateString(input.SoftDueDate)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid soft_due_date format"})
return
}
softDueDate = &parsedSoft
}
assignment, err := h.assignmentService.Update(userID, uint(id), title, description, subject, priority, dueDate, softDueDate, reminderEnabled, reminderAt, urgentReminderEnabled)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update assignment"})
return

View File

@@ -111,14 +111,16 @@ func (h *AssignmentHandler) New(c *gin.Context) {
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"),
"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"),
})
}
@@ -178,6 +180,16 @@ func (h *AssignmentHandler) Create(c *gin.Context) {
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" {
@@ -265,7 +277,7 @@ func (h *AssignmentHandler) Create(c *gin.Context) {
return
}
} else {
assignment, err := h.assignmentService.Create(userID, title, description, subject, priority, dueDate, reminderEnabled, reminderAt, urgentReminderEnabled)
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)
@@ -352,7 +364,17 @@ func (h *AssignmentHandler) Update(c *gin.Context) {
dueDate = dueDate.Add(23*time.Hour + 59*time.Minute)
}
_, err = h.assignmentService.Update(userID, uint(id), title, description, subject, priority, dueDate, reminderEnabled, reminderAt, urgentReminderEnabled)
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
@@ -501,10 +523,9 @@ func (h *AssignmentHandler) ExportCSV(c *gin.Context) {
c.Header("Content-Disposition", "attachment; filename=\""+filename+"\"")
w := csv.NewWriter(c.Writer)
// UTF-8 BOM for Excel compatibility
c.Writer.Write([]byte("\xef\xbb\xbf"))
headers := []string{"ID", "タイトル", "科目", "説明", "重要度", "提出期限", "完了", "完了日時", "登録日時"}
headers := []string{"ID", "タイトル", "科目", "説明", "重要度", "提出期限", "自分の期限", "完了", "完了日時", "登録日時"}
w.Write(headers)
priorityLabel := map[string]string{"low": "低", "medium": "中", "high": "高"}
@@ -517,6 +538,10 @@ func (h *AssignmentHandler) ExportCSV(c *gin.Context) {
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
@@ -528,6 +553,7 @@ func (h *AssignmentHandler) ExportCSV(c *gin.Context) {
a.Description,
label,
a.DueDate.Format("2006/01/02 15:04"),
softDueDateStr,
completed,
completedAt,
a.CreatedAt.Format("2006/01/02 15:04"),

View File

@@ -14,6 +14,7 @@ type Assignment struct {
Subject string `json:"subject"`
Priority string `gorm:"not null;default:medium" json:"priority"`
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"`
IsArchived bool `gorm:"default:false;index" json:"is_archived"`
CompletedAt *time.Time `json:"completed_at,omitempty"`
@@ -23,7 +24,6 @@ type Assignment struct {
UrgentReminderEnabled bool `gorm:"default:true" json:"urgent_reminder_enabled"`
LastUrgentReminderSent *time.Time `json:"last_urgent_reminder_sent,omitempty"`
// Recurring assignment reference
RecurringAssignmentID *uint `gorm:"index" json:"recurring_assignment_id,omitempty"`
RecurringAssignment *RecurringAssignment `gorm:"foreignKey:RecurringAssignmentID" json:"-"`
@@ -34,6 +34,13 @@ type Assignment struct {
User *User `gorm:"foreignKey:UserID" json:"user,omitempty"`
}
func (a *Assignment) GetEffectiveSoftDueDate() time.Time {
if a.SoftDueDate != nil {
return *a.SoftDueDate
}
return a.DueDate.Add(-48 * time.Hour)
}
func (a *Assignment) IsOverdue() bool {
return !a.IsCompleted && time.Now().After(a.DueDate)
}

View File

@@ -25,11 +25,27 @@ func getFuncMap() template.FuncMap {
"formatDate": func(t time.Time) string {
return t.Format("2006/01/02")
},
"formatDateTime": func(t time.Time) string {
return t.Format("2006/01/02 15:04")
"formatDateTime": func(t interface{}) string {
switch v := t.(type) {
case time.Time:
return v.Format("2006/01/02 15:04")
case *time.Time:
if v != nil {
return v.Format("2006/01/02 15:04")
}
}
return ""
},
"formatDateInput": func(t time.Time) string {
return t.Format("2006-01-02T15:04")
"formatDateInput": func(t interface{}) string {
switch v := t.(type) {
case time.Time:
return v.Format("2006-01-02T15:04")
case *time.Time:
if v != nil {
return v.Format("2006-01-02T15:04")
}
}
return ""
},
"isOverdue": func(t time.Time, completed bool) bool {
return !completed && time.Now().After(t)

View File

@@ -31,7 +31,7 @@ func NewAssignmentService() *AssignmentService {
}
}
func (s *AssignmentService) Create(userID uint, title, description, subject, priority string, dueDate time.Time, reminderEnabled bool, reminderAt *time.Time, urgentReminderEnabled bool) (*models.Assignment, error) {
func (s *AssignmentService) Create(userID uint, title, description, subject, priority string, dueDate time.Time, softDueDate *time.Time, reminderEnabled bool, reminderAt *time.Time, urgentReminderEnabled bool) (*models.Assignment, error) {
if priority == "" {
priority = "medium"
}
@@ -42,6 +42,7 @@ func (s *AssignmentService) Create(userID uint, title, description, subject, pri
Subject: subject,
Priority: priority,
DueDate: dueDate,
SoftDueDate: softDueDate,
IsCompleted: false,
ReminderEnabled: reminderEnabled,
ReminderAt: reminderAt,
@@ -195,7 +196,7 @@ func (s *AssignmentService) SearchAssignments(userID uint, query, priority, filt
}, nil
}
func (s *AssignmentService) Update(userID, assignmentID uint, title, description, subject, priority string, dueDate time.Time, reminderEnabled bool, reminderAt *time.Time, urgentReminderEnabled bool) (*models.Assignment, error) {
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 {
return nil, err
@@ -206,6 +207,7 @@ func (s *AssignmentService) Update(userID, assignmentID uint, title, description
assignment.Subject = subject
assignment.Priority = priority
assignment.DueDate = dueDate
assignment.SoftDueDate = softDueDate
assignment.ReminderEnabled = reminderEnabled
assignment.ReminderAt = reminderAt
assignment.UrgentReminderEnabled = urgentReminderEnabled

View File

@@ -170,10 +170,11 @@ func (s *NotificationService) SendUrgentReminder(userID uint, assignment *models
return err
}
timeRemaining := time.Until(assignment.DueDate)
softDue := assignment.GetEffectiveSoftDueDate()
timeRemaining := time.Until(softDue)
var timeStr string
if timeRemaining < 0 {
timeStr = "期限切れ!"
timeStr = "自分の期限切れ!"
} else if timeRemaining < time.Hour {
timeStr = fmt.Sprintf("あと%d分", int(timeRemaining.Minutes()))
} else {
@@ -191,12 +192,13 @@ func (s *NotificationService) SendUrgentReminder(userID uint, assignment *models
}
message := fmt.Sprintf(
"%s 督促通知!\n\n【%s】\n科目: %s\n期限: %s (%s)\n\n完了したらアプリで完了ボタンを押してください",
"%s 督促通知!\n\n【%s】\n科目: %s\n自分の期限: %s (%s)\nガチ期限: %s\n\n完了したらアプリで完了ボタンを押してください",
priorityEmoji,
assignment.Title,
assignment.Subject,
assignment.DueDate.Format("2006/01/02 15:04"),
softDue.Format("2006/01/02 15:04"),
timeStr,
assignment.DueDate.Format("2006/01/02 15:04"),
)
var errors []string
@@ -268,9 +270,10 @@ func (s *NotificationService) ProcessUrgentReminders() {
}
for _, assignment := range assignments {
timeUntilDue := assignment.DueDate.Sub(now)
softDue := assignment.GetEffectiveSoftDueDate()
timeUntilSoftDue := softDue.Sub(now)
if timeUntilDue > urgentStartTime {
if timeUntilSoftDue > urgentStartTime {
continue
}

View File

@@ -244,26 +244,27 @@ func (s *RecurringAssignmentService) UpdateAssignmentWithBehavior(
assignment *models.Assignment,
title, description, subject, priority string,
dueDate time.Time,
softDueDate *time.Time,
reminderEnabled bool,
reminderAt *time.Time,
urgentReminderEnabled bool,
editBehavior string,
) error {
if assignment.RecurringAssignmentID == nil {
return s.updateSingleAssignment(assignment, title, description, subject, priority, dueDate, reminderEnabled, reminderAt, urgentReminderEnabled)
return s.updateSingleAssignment(assignment, title, description, subject, priority, dueDate, softDueDate, reminderEnabled, reminderAt, urgentReminderEnabled)
}
recurring, err := s.GetByID(userID, *assignment.RecurringAssignmentID)
if err != nil {
return s.updateSingleAssignment(assignment, title, description, subject, priority, dueDate, reminderEnabled, reminderAt, urgentReminderEnabled)
return s.updateSingleAssignment(assignment, title, description, subject, priority, dueDate, softDueDate, reminderEnabled, reminderAt, urgentReminderEnabled)
}
switch editBehavior {
case models.EditBehaviorThisOnly:
return s.updateSingleAssignment(assignment, title, description, subject, priority, dueDate, reminderEnabled, reminderAt, urgentReminderEnabled)
return s.updateSingleAssignment(assignment, title, description, subject, priority, dueDate, softDueDate, reminderEnabled, reminderAt, urgentReminderEnabled)
case models.EditBehaviorThisAndFuture:
if err := s.updateSingleAssignment(assignment, title, description, subject, priority, dueDate, reminderEnabled, reminderAt, urgentReminderEnabled); err != nil {
if err := s.updateSingleAssignment(assignment, title, description, subject, priority, dueDate, softDueDate, reminderEnabled, reminderAt, urgentReminderEnabled); err != nil {
return err
}
recurring.Title = title
@@ -288,7 +289,7 @@ func (s *RecurringAssignmentService) UpdateAssignmentWithBehavior(
return s.updateAllPendingAssignments(recurring.ID, title, description, subject, priority, urgentReminderEnabled)
default:
return s.updateSingleAssignment(assignment, title, description, subject, priority, dueDate, reminderEnabled, reminderAt, urgentReminderEnabled)
return s.updateSingleAssignment(assignment, title, description, subject, priority, dueDate, softDueDate, reminderEnabled, reminderAt, urgentReminderEnabled)
}
}
@@ -296,6 +297,7 @@ func (s *RecurringAssignmentService) updateSingleAssignment(
assignment *models.Assignment,
title, description, subject, priority string,
dueDate time.Time,
softDueDate *time.Time,
reminderEnabled bool,
reminderAt *time.Time,
urgentReminderEnabled bool,
@@ -305,6 +307,7 @@ func (s *RecurringAssignmentService) updateSingleAssignment(
assignment.Subject = subject
assignment.Priority = priority
assignment.DueDate = dueDate
assignment.SoftDueDate = softDueDate
assignment.ReminderEnabled = reminderEnabled
assignment.ReminderAt = reminderAt
assignment.UrgentReminderEnabled = urgentReminderEnabled
@@ -471,6 +474,8 @@ func (s *RecurringAssignmentService) generateAssignment(recurring *models.Recurr
reminderAt = &t
}
softDue := dueDate.Add(-48 * time.Hour)
assignment := &models.Assignment{
UserID: recurring.UserID,
Title: recurring.Title,
@@ -478,6 +483,7 @@ func (s *RecurringAssignmentService) generateAssignment(recurring *models.Recurr
Subject: recurring.Subject,
Priority: recurring.Priority,
DueDate: dueDate,
SoftDueDate: &softDue,
ReminderEnabled: recurring.ReminderEnabled,
ReminderAt: reminderAt,
UrgentReminderEnabled: recurring.UrgentReminderEnabled,