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

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"` 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

View File

@@ -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"),

View File

@@ -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)
} }

View File

@@ -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)

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 == "" { 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

View File

@@ -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
} }

View File

@@ -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,

View File

@@ -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>

View File

@@ -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}}

View File

@@ -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}}

View File

@@ -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}}">