本来の期限に加えて自分の中での目標期限を設定できるよう仕様追加
This commit is contained in:
@@ -239,6 +239,7 @@ type CreateAssignmentInput struct {
|
|||||||
Subject string `json:"subject"`
|
Subject string `json:"subject"`
|
||||||
Priority string `json:"priority"`
|
Priority string `json:"priority"`
|
||||||
DueDate string `json:"due_date" binding:"required"`
|
DueDate string `json:"due_date" binding:"required"`
|
||||||
|
SoftDueDate string `json:"soft_due_date"`
|
||||||
|
|
||||||
ReminderEnabled bool `json:"reminder_enabled"`
|
ReminderEnabled bool `json:"reminder_enabled"`
|
||||||
ReminderAt string `json:"reminder_at"`
|
ReminderAt string `json:"reminder_at"`
|
||||||
@@ -352,7 +353,17 @@ func (h *APIHandler) CreateAssignment(c *gin.Context) {
|
|||||||
return
|
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 {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create assignment"})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create assignment"})
|
||||||
return
|
return
|
||||||
@@ -367,6 +378,7 @@ type UpdateAssignmentInput struct {
|
|||||||
Subject string `json:"subject"`
|
Subject string `json:"subject"`
|
||||||
Priority string `json:"priority"`
|
Priority string `json:"priority"`
|
||||||
DueDate string `json:"due_date"`
|
DueDate string `json:"due_date"`
|
||||||
|
SoftDueDate string `json:"soft_due_date"`
|
||||||
ReminderEnabled *bool `json:"reminder_enabled"`
|
ReminderEnabled *bool `json:"reminder_enabled"`
|
||||||
ReminderAt string `json:"reminder_at"`
|
ReminderAt string `json:"reminder_at"`
|
||||||
UrgentReminderEnabled *bool `json:"urgent_reminder_enabled"`
|
UrgentReminderEnabled *bool `json:"urgent_reminder_enabled"`
|
||||||
@@ -448,7 +460,17 @@ func (h *APIHandler) UpdateAssignment(c *gin.Context) {
|
|||||||
urgentReminderEnabled = *input.UrgentReminderEnabled
|
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 {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update assignment"})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update assignment"})
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -111,14 +111,16 @@ func (h *AssignmentHandler) New(c *gin.Context) {
|
|||||||
now := time.Now()
|
now := time.Now()
|
||||||
tomorrow := now.AddDate(0, 0, 1)
|
tomorrow := now.AddDate(0, 0, 1)
|
||||||
defaultDue := time.Date(tomorrow.Year(), tomorrow.Month(), tomorrow.Day(), 23, 59, 0, 0, now.Location())
|
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{
|
RenderHTML(c, http.StatusOK, "assignments/new.html", gin.H{
|
||||||
"title": "課題登録",
|
"title": "課題登録",
|
||||||
"isAdmin": role == "admin",
|
"isAdmin": role == "admin",
|
||||||
"userName": name,
|
"userName": name,
|
||||||
"currentWeekday": int(now.Weekday()),
|
"currentWeekday": int(now.Weekday()),
|
||||||
"currentDay": now.Day(),
|
"currentDay": now.Day(),
|
||||||
"defaultDueDate": defaultDue.Format("2006-01-02T15:04"),
|
"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)
|
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")
|
recurrenceType := c.PostForm("recurrence_type")
|
||||||
if recurrenceType != "" && recurrenceType != "none" {
|
if recurrenceType != "" && recurrenceType != "none" {
|
||||||
|
|
||||||
@@ -265,7 +277,7 @@ func (h *AssignmentHandler) Create(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else {
|
} 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 {
|
if err != nil {
|
||||||
role, _ := c.Get(middleware.UserRoleKey)
|
role, _ := c.Get(middleware.UserRoleKey)
|
||||||
name, _ := c.Get(middleware.UserNameKey)
|
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)
|
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 {
|
if err != nil {
|
||||||
c.Redirect(http.StatusFound, "/assignments")
|
c.Redirect(http.StatusFound, "/assignments")
|
||||||
return
|
return
|
||||||
@@ -501,10 +523,9 @@ func (h *AssignmentHandler) ExportCSV(c *gin.Context) {
|
|||||||
c.Header("Content-Disposition", "attachment; filename=\""+filename+"\"")
|
c.Header("Content-Disposition", "attachment; filename=\""+filename+"\"")
|
||||||
|
|
||||||
w := csv.NewWriter(c.Writer)
|
w := csv.NewWriter(c.Writer)
|
||||||
// UTF-8 BOM for Excel compatibility
|
|
||||||
c.Writer.Write([]byte("\xef\xbb\xbf"))
|
c.Writer.Write([]byte("\xef\xbb\xbf"))
|
||||||
|
|
||||||
headers := []string{"ID", "タイトル", "科目", "説明", "重要度", "提出期限", "完了", "完了日時", "登録日時"}
|
headers := []string{"ID", "タイトル", "科目", "説明", "重要度", "提出期限", "自分の期限", "完了", "完了日時", "登録日時"}
|
||||||
w.Write(headers)
|
w.Write(headers)
|
||||||
|
|
||||||
priorityLabel := map[string]string{"low": "低", "medium": "中", "high": "高"}
|
priorityLabel := map[string]string{"low": "低", "medium": "中", "high": "高"}
|
||||||
@@ -517,6 +538,10 @@ func (h *AssignmentHandler) ExportCSV(c *gin.Context) {
|
|||||||
if a.CompletedAt != nil {
|
if a.CompletedAt != nil {
|
||||||
completedAt = a.CompletedAt.Format("2006/01/02 15:04")
|
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]
|
label := priorityLabel[a.Priority]
|
||||||
if label == "" {
|
if label == "" {
|
||||||
label = a.Priority
|
label = a.Priority
|
||||||
@@ -528,6 +553,7 @@ func (h *AssignmentHandler) ExportCSV(c *gin.Context) {
|
|||||||
a.Description,
|
a.Description,
|
||||||
label,
|
label,
|
||||||
a.DueDate.Format("2006/01/02 15:04"),
|
a.DueDate.Format("2006/01/02 15:04"),
|
||||||
|
softDueDateStr,
|
||||||
completed,
|
completed,
|
||||||
completedAt,
|
completedAt,
|
||||||
a.CreatedAt.Format("2006/01/02 15:04"),
|
a.CreatedAt.Format("2006/01/02 15:04"),
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ type Assignment struct {
|
|||||||
Subject string `json:"subject"`
|
Subject string `json:"subject"`
|
||||||
Priority string `gorm:"not null;default:medium" json:"priority"`
|
Priority string `gorm:"not null;default:medium" json:"priority"`
|
||||||
DueDate time.Time `gorm:"not null" json:"due_date"`
|
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"`
|
IsCompleted bool `gorm:"default:false" json:"is_completed"`
|
||||||
IsArchived bool `gorm:"default:false;index" json:"is_archived"`
|
IsArchived bool `gorm:"default:false;index" json:"is_archived"`
|
||||||
CompletedAt *time.Time `json:"completed_at,omitempty"`
|
CompletedAt *time.Time `json:"completed_at,omitempty"`
|
||||||
@@ -23,7 +24,6 @@ type Assignment struct {
|
|||||||
UrgentReminderEnabled bool `gorm:"default:true" json:"urgent_reminder_enabled"`
|
UrgentReminderEnabled bool `gorm:"default:true" json:"urgent_reminder_enabled"`
|
||||||
LastUrgentReminderSent *time.Time `json:"last_urgent_reminder_sent,omitempty"`
|
LastUrgentReminderSent *time.Time `json:"last_urgent_reminder_sent,omitempty"`
|
||||||
|
|
||||||
// Recurring assignment reference
|
|
||||||
RecurringAssignmentID *uint `gorm:"index" json:"recurring_assignment_id,omitempty"`
|
RecurringAssignmentID *uint `gorm:"index" json:"recurring_assignment_id,omitempty"`
|
||||||
RecurringAssignment *RecurringAssignment `gorm:"foreignKey:RecurringAssignmentID" json:"-"`
|
RecurringAssignment *RecurringAssignment `gorm:"foreignKey:RecurringAssignmentID" json:"-"`
|
||||||
|
|
||||||
@@ -34,6 +34,13 @@ type Assignment struct {
|
|||||||
User *User `gorm:"foreignKey:UserID" json:"user,omitempty"`
|
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 {
|
func (a *Assignment) IsOverdue() bool {
|
||||||
return !a.IsCompleted && time.Now().After(a.DueDate)
|
return !a.IsCompleted && time.Now().After(a.DueDate)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,11 +25,27 @@ func getFuncMap() template.FuncMap {
|
|||||||
"formatDate": func(t time.Time) string {
|
"formatDate": func(t time.Time) string {
|
||||||
return t.Format("2006/01/02")
|
return t.Format("2006/01/02")
|
||||||
},
|
},
|
||||||
"formatDateTime": func(t time.Time) string {
|
"formatDateTime": func(t interface{}) string {
|
||||||
return t.Format("2006/01/02 15:04")
|
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 {
|
"formatDateInput": func(t interface{}) string {
|
||||||
return t.Format("2006-01-02T15:04")
|
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 {
|
"isOverdue": func(t time.Time, completed bool) bool {
|
||||||
return !completed && time.Now().After(t)
|
return !completed && time.Now().After(t)
|
||||||
|
|||||||
@@ -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 == "" {
|
if priority == "" {
|
||||||
priority = "medium"
|
priority = "medium"
|
||||||
}
|
}
|
||||||
@@ -42,6 +42,7 @@ func (s *AssignmentService) Create(userID uint, title, description, subject, pri
|
|||||||
Subject: subject,
|
Subject: subject,
|
||||||
Priority: priority,
|
Priority: priority,
|
||||||
DueDate: dueDate,
|
DueDate: dueDate,
|
||||||
|
SoftDueDate: softDueDate,
|
||||||
IsCompleted: false,
|
IsCompleted: false,
|
||||||
ReminderEnabled: reminderEnabled,
|
ReminderEnabled: reminderEnabled,
|
||||||
ReminderAt: reminderAt,
|
ReminderAt: reminderAt,
|
||||||
@@ -195,7 +196,7 @@ func (s *AssignmentService) SearchAssignments(userID uint, query, priority, filt
|
|||||||
}, nil
|
}, 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)
|
assignment, err := s.GetByID(userID, assignmentID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -206,6 +207,7 @@ func (s *AssignmentService) Update(userID, assignmentID uint, title, description
|
|||||||
assignment.Subject = subject
|
assignment.Subject = subject
|
||||||
assignment.Priority = priority
|
assignment.Priority = priority
|
||||||
assignment.DueDate = dueDate
|
assignment.DueDate = dueDate
|
||||||
|
assignment.SoftDueDate = softDueDate
|
||||||
assignment.ReminderEnabled = reminderEnabled
|
assignment.ReminderEnabled = reminderEnabled
|
||||||
assignment.ReminderAt = reminderAt
|
assignment.ReminderAt = reminderAt
|
||||||
assignment.UrgentReminderEnabled = urgentReminderEnabled
|
assignment.UrgentReminderEnabled = urgentReminderEnabled
|
||||||
|
|||||||
@@ -170,10 +170,11 @@ func (s *NotificationService) SendUrgentReminder(userID uint, assignment *models
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
timeRemaining := time.Until(assignment.DueDate)
|
softDue := assignment.GetEffectiveSoftDueDate()
|
||||||
|
timeRemaining := time.Until(softDue)
|
||||||
var timeStr string
|
var timeStr string
|
||||||
if timeRemaining < 0 {
|
if timeRemaining < 0 {
|
||||||
timeStr = "期限切れ!"
|
timeStr = "自分の期限切れ!"
|
||||||
} else if timeRemaining < time.Hour {
|
} else if timeRemaining < time.Hour {
|
||||||
timeStr = fmt.Sprintf("あと%d分", int(timeRemaining.Minutes()))
|
timeStr = fmt.Sprintf("あと%d分", int(timeRemaining.Minutes()))
|
||||||
} else {
|
} else {
|
||||||
@@ -191,12 +192,13 @@ func (s *NotificationService) SendUrgentReminder(userID uint, assignment *models
|
|||||||
}
|
}
|
||||||
|
|
||||||
message := fmt.Sprintf(
|
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,
|
priorityEmoji,
|
||||||
assignment.Title,
|
assignment.Title,
|
||||||
assignment.Subject,
|
assignment.Subject,
|
||||||
assignment.DueDate.Format("2006/01/02 15:04"),
|
softDue.Format("2006/01/02 15:04"),
|
||||||
timeStr,
|
timeStr,
|
||||||
|
assignment.DueDate.Format("2006/01/02 15:04"),
|
||||||
)
|
)
|
||||||
|
|
||||||
var errors []string
|
var errors []string
|
||||||
@@ -268,9 +270,10 @@ func (s *NotificationService) ProcessUrgentReminders() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, assignment := range assignments {
|
for _, assignment := range assignments {
|
||||||
timeUntilDue := assignment.DueDate.Sub(now)
|
softDue := assignment.GetEffectiveSoftDueDate()
|
||||||
|
timeUntilSoftDue := softDue.Sub(now)
|
||||||
|
|
||||||
if timeUntilDue > urgentStartTime {
|
if timeUntilSoftDue > urgentStartTime {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -244,26 +244,27 @@ func (s *RecurringAssignmentService) UpdateAssignmentWithBehavior(
|
|||||||
assignment *models.Assignment,
|
assignment *models.Assignment,
|
||||||
title, description, subject, priority string,
|
title, description, subject, priority string,
|
||||||
dueDate time.Time,
|
dueDate time.Time,
|
||||||
|
softDueDate *time.Time,
|
||||||
reminderEnabled bool,
|
reminderEnabled bool,
|
||||||
reminderAt *time.Time,
|
reminderAt *time.Time,
|
||||||
urgentReminderEnabled bool,
|
urgentReminderEnabled bool,
|
||||||
editBehavior string,
|
editBehavior string,
|
||||||
) error {
|
) error {
|
||||||
if assignment.RecurringAssignmentID == nil {
|
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)
|
recurring, err := s.GetByID(userID, *assignment.RecurringAssignmentID)
|
||||||
if err != nil {
|
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 {
|
switch editBehavior {
|
||||||
case models.EditBehaviorThisOnly:
|
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:
|
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
|
return err
|
||||||
}
|
}
|
||||||
recurring.Title = title
|
recurring.Title = title
|
||||||
@@ -288,7 +289,7 @@ func (s *RecurringAssignmentService) UpdateAssignmentWithBehavior(
|
|||||||
return s.updateAllPendingAssignments(recurring.ID, title, description, subject, priority, urgentReminderEnabled)
|
return s.updateAllPendingAssignments(recurring.ID, title, description, subject, priority, urgentReminderEnabled)
|
||||||
|
|
||||||
default:
|
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,
|
assignment *models.Assignment,
|
||||||
title, description, subject, priority string,
|
title, description, subject, priority string,
|
||||||
dueDate time.Time,
|
dueDate time.Time,
|
||||||
|
softDueDate *time.Time,
|
||||||
reminderEnabled bool,
|
reminderEnabled bool,
|
||||||
reminderAt *time.Time,
|
reminderAt *time.Time,
|
||||||
urgentReminderEnabled bool,
|
urgentReminderEnabled bool,
|
||||||
@@ -305,6 +307,7 @@ func (s *RecurringAssignmentService) updateSingleAssignment(
|
|||||||
assignment.Subject = subject
|
assignment.Subject = subject
|
||||||
assignment.Priority = priority
|
assignment.Priority = priority
|
||||||
assignment.DueDate = dueDate
|
assignment.DueDate = dueDate
|
||||||
|
assignment.SoftDueDate = softDueDate
|
||||||
assignment.ReminderEnabled = reminderEnabled
|
assignment.ReminderEnabled = reminderEnabled
|
||||||
assignment.ReminderAt = reminderAt
|
assignment.ReminderAt = reminderAt
|
||||||
assignment.UrgentReminderEnabled = urgentReminderEnabled
|
assignment.UrgentReminderEnabled = urgentReminderEnabled
|
||||||
@@ -471,6 +474,8 @@ func (s *RecurringAssignmentService) generateAssignment(recurring *models.Recurr
|
|||||||
reminderAt = &t
|
reminderAt = &t
|
||||||
}
|
}
|
||||||
|
|
||||||
|
softDue := dueDate.Add(-48 * time.Hour)
|
||||||
|
|
||||||
assignment := &models.Assignment{
|
assignment := &models.Assignment{
|
||||||
UserID: recurring.UserID,
|
UserID: recurring.UserID,
|
||||||
Title: recurring.Title,
|
Title: recurring.Title,
|
||||||
@@ -478,6 +483,7 @@ func (s *RecurringAssignmentService) generateAssignment(recurring *models.Recurr
|
|||||||
Subject: recurring.Subject,
|
Subject: recurring.Subject,
|
||||||
Priority: recurring.Priority,
|
Priority: recurring.Priority,
|
||||||
DueDate: dueDate,
|
DueDate: dueDate,
|
||||||
|
SoftDueDate: &softDue,
|
||||||
ReminderEnabled: recurring.ReminderEnabled,
|
ReminderEnabled: recurring.ReminderEnabled,
|
||||||
ReminderAt: reminderAt,
|
ReminderAt: reminderAt,
|
||||||
UrgentReminderEnabled: recurring.UrgentReminderEnabled,
|
UrgentReminderEnabled: recurring.UrgentReminderEnabled,
|
||||||
|
|||||||
@@ -28,9 +28,14 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="due_date" class="form-label">提出期限 <span class="text-danger" aria-hidden="true">*</span><span class="visually-hidden">(必須)</span></label>
|
<label for="due_date" class="form-label">提出期限(ガチ期限) <span class="text-danger" aria-hidden="true">*</span><span class="visually-hidden">(必須)</span></label>
|
||||||
<input type="datetime-local" class="form-control" id="due_date" name="due_date" value="{{formatDateInput .assignment.DueDate}}" required>
|
<input type="datetime-local" class="form-control" id="due_date" name="due_date" value="{{formatDateInput .assignment.DueDate}}" required>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="soft_due_date" class="form-label">自分の期限 <span class="badge bg-secondary">任意</span></label>
|
||||||
|
<input type="datetime-local" class="form-control" id="soft_due_date" name="soft_due_date" value="{{if .assignment.SoftDueDate}}{{formatDateInput .assignment.SoftDueDate}}{{end}}">
|
||||||
|
<div class="form-text small text-muted">鬼督促はこの期限を基準に通知します。未指定の場合は提出期限の2日前になります。</div>
|
||||||
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="description" class="form-label">説明</label>
|
<label for="description" class="form-label">説明</label>
|
||||||
<textarea class="form-control" id="description" name="description" rows="3">{{.assignment.Description}}</textarea>
|
<textarea class="form-control" id="description" name="description" rows="3">{{.assignment.Description}}</textarea>
|
||||||
|
|||||||
@@ -129,6 +129,7 @@
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="small fw-bold text-dark user-select-all">{{.DueDate.Format "2006/01/02 15:04"}}</div>
|
<div class="small fw-bold text-dark user-select-all">{{.DueDate.Format "2006/01/02 15:04"}}</div>
|
||||||
|
{{if .SoftDueDate}}<div class="small text-info" title="自分の期限"><i class="bi bi-clock-history me-1" aria-hidden="true"></i>{{.SoftDueDate.Format "01/02 15:04"}}</div>{{end}}
|
||||||
</td>
|
</td>
|
||||||
<td class="countdown-col">
|
<td class="countdown-col">
|
||||||
{{if not .IsCompleted}}
|
{{if not .IsCompleted}}
|
||||||
|
|||||||
@@ -28,9 +28,14 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="due_date" class="form-label">提出期限 <span class="text-danger" aria-hidden="true">*</span><span class="visually-hidden">(必須)</span></label>
|
<label for="due_date" class="form-label">提出期限(ガチ期限) <span class="text-danger" aria-hidden="true">*</span><span class="visually-hidden">(必須)</span></label>
|
||||||
<input type="datetime-local" class="form-control" id="due_date" name="due_date" value="{{.defaultDueDate}}" required>
|
<input type="datetime-local" class="form-control" id="due_date" name="due_date" value="{{.defaultDueDate}}" required>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="soft_due_date" class="form-label">自分の期限 <span class="badge bg-secondary">任意</span> <i class="bi bi-question-circle-fill text-muted" data-bs-toggle="tooltip" data-bs-placement="top" title="自分の期限とは、余裕を持って自分でこの期間までに提出するといった目標期限です。デフォルトでは2日前に設定されます。" aria-label="自分の期限の説明"></i></label>
|
||||||
|
<input type="datetime-local" class="form-control" id="soft_due_date" name="soft_due_date" value="{{.defaultSoftDueDate}}">
|
||||||
|
<div class="form-text small text-muted">鬼督促はこの期限を基準に通知します。</div>
|
||||||
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="description" class="form-label">説明</label>
|
<label for="description" class="form-label">説明</label>
|
||||||
<textarea class="form-control" id="description" name="description" rows="3">{{.description}}</textarea>
|
<textarea class="form-control" id="description" name="description" rows="3">{{.description}}</textarea>
|
||||||
@@ -156,6 +161,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
|
// Bootstrap tooltip初期化
|
||||||
|
document.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(function(el) { new bootstrap.Tooltip(el); });
|
||||||
|
|
||||||
function toggleReminderDate(checkbox) {
|
function toggleReminderDate(checkbox) {
|
||||||
document.getElementById('reminder_at_group').style.display = checkbox.checked ? 'block' : 'none';
|
document.getElementById('reminder_at_group').style.display = checkbox.checked ? 'block' : 'none';
|
||||||
}
|
}
|
||||||
@@ -204,5 +212,25 @@
|
|||||||
document.getElementById('recurringSettings').addEventListener('show.bs.collapse', function () {
|
document.getElementById('recurringSettings').addEventListener('show.bs.collapse', function () {
|
||||||
updateLeadDaysMax();
|
updateLeadDaysMax();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Auto-sync soft_due_date when due_date changes
|
||||||
|
(function() {
|
||||||
|
var dueDateInput = document.getElementById('due_date');
|
||||||
|
var softDueDateInput = document.getElementById('soft_due_date');
|
||||||
|
var userEditedSoftDue = false;
|
||||||
|
softDueDateInput.addEventListener('input', function() { userEditedSoftDue = true; });
|
||||||
|
dueDateInput.addEventListener('change', function() {
|
||||||
|
if (userEditedSoftDue) return;
|
||||||
|
if (!dueDateInput.value) return;
|
||||||
|
var dueDate = new Date(dueDateInput.value);
|
||||||
|
dueDate.setDate(dueDate.getDate() - 2);
|
||||||
|
var y = dueDate.getFullYear();
|
||||||
|
var m = String(dueDate.getMonth() + 1).padStart(2, '0');
|
||||||
|
var d = String(dueDate.getDate()).padStart(2, '0');
|
||||||
|
var h = String(dueDate.getHours()).padStart(2, '0');
|
||||||
|
var min = String(dueDate.getMinutes()).padStart(2, '0');
|
||||||
|
softDueDateInput.value = y + '-' + m + '-' + d + 'T' + h + ':' + min;
|
||||||
|
});
|
||||||
|
})();
|
||||||
</script>
|
</script>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|||||||
@@ -115,6 +115,7 @@
|
|||||||
{{if eq .Priority "high"}}<span class="badge bg-danger me-1">高<span class="visually-hidden">(重要度:高)</span></span>{{end}}
|
{{if eq .Priority "high"}}<span class="badge bg-danger me-1">高<span class="visually-hidden">(重要度:高)</span></span>{{end}}
|
||||||
<strong>{{.Title}}</strong>
|
<strong>{{.Title}}</strong>
|
||||||
<br><small class="text-danger">{{formatDateTime .DueDate}}</small>
|
<br><small class="text-danger">{{formatDateTime .DueDate}}</small>
|
||||||
|
{{if .SoftDueDate}}<br><small class="text-info"><i class="bi bi-clock-history" aria-hidden="true"></i> 自分の期限: {{formatDateTime .SoftDueDate}}</small>{{end}}
|
||||||
</div>
|
</div>
|
||||||
<form action="/assignments/{{.ID}}/toggle" method="POST">
|
<form action="/assignments/{{.ID}}/toggle" method="POST">
|
||||||
<input type="hidden" name="_csrf" value="{{$.csrfToken}}">
|
<input type="hidden" name="_csrf" value="{{$.csrfToken}}">
|
||||||
@@ -140,6 +141,7 @@
|
|||||||
{{if eq .Priority "high"}}<span class="badge bg-danger me-1">高<span class="visually-hidden">(重要度:高)</span></span>{{end}}
|
{{if eq .Priority "high"}}<span class="badge bg-danger me-1">高<span class="visually-hidden">(重要度:高)</span></span>{{end}}
|
||||||
<strong>{{.Title}}</strong>
|
<strong>{{.Title}}</strong>
|
||||||
<br><small class="text-muted">{{formatDateTime .DueDate}}</small>
|
<br><small class="text-muted">{{formatDateTime .DueDate}}</small>
|
||||||
|
{{if .SoftDueDate}}<br><small class="text-info"><i class="bi bi-clock-history" aria-hidden="true"></i> 自分の期限: {{formatDateTime .SoftDueDate}}</small>{{end}}
|
||||||
</div>
|
</div>
|
||||||
<form action="/assignments/{{.ID}}/toggle" method="POST">
|
<form action="/assignments/{{.ID}}/toggle" method="POST">
|
||||||
<input type="hidden" name="_csrf" value="{{$.csrfToken}}">
|
<input type="hidden" name="_csrf" value="{{$.csrfToken}}">
|
||||||
@@ -165,6 +167,7 @@
|
|||||||
{{if eq .Priority "high"}}<span class="badge bg-danger me-1">高<span class="visually-hidden">(重要度:高)</span></span>{{end}}
|
{{if eq .Priority "high"}}<span class="badge bg-danger me-1">高<span class="visually-hidden">(重要度:高)</span></span>{{end}}
|
||||||
<strong>{{.Title}}</strong>
|
<strong>{{.Title}}</strong>
|
||||||
<br><small class="text-muted">{{formatDateTime .DueDate}}</small>
|
<br><small class="text-muted">{{formatDateTime .DueDate}}</small>
|
||||||
|
{{if .SoftDueDate}}<br><small class="text-info"><i class="bi bi-clock-history" aria-hidden="true"></i> 自分の期限: {{formatDateTime .SoftDueDate}}</small>{{end}}
|
||||||
</div>
|
</div>
|
||||||
<form action="/assignments/{{.ID}}/toggle" method="POST">
|
<form action="/assignments/{{.ID}}/toggle" method="POST">
|
||||||
<input type="hidden" name="_csrf" value="{{$.csrfToken}}">
|
<input type="hidden" name="_csrf" value="{{$.csrfToken}}">
|
||||||
|
|||||||
Reference in New Issue
Block a user