繰り返し追加機能の追加
This commit is contained in:
@@ -62,19 +62,13 @@ func Connect(dbConfig config.DatabaseConfig, debug bool) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Set connection pool settings
|
||||
sqlDB, err := db.DB()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// SetMaxIdleConns sets the maximum number of connections in the idle connection pool.
|
||||
sqlDB.SetMaxIdleConns(10)
|
||||
|
||||
// SetMaxOpenConns sets the maximum number of open connections to the database.
|
||||
sqlDB.SetMaxOpenConns(100)
|
||||
|
||||
// SetConnMaxLifetime sets the maximum amount of time a connection may be reused.
|
||||
sqlDB.SetConnMaxLifetime(time.Hour)
|
||||
|
||||
DB = db
|
||||
@@ -85,6 +79,7 @@ func Migrate() error {
|
||||
return DB.AutoMigrate(
|
||||
&models.User{},
|
||||
&models.Assignment{},
|
||||
&models.RecurringAssignment{},
|
||||
&models.APIKey{},
|
||||
&models.UserNotificationSettings{},
|
||||
)
|
||||
|
||||
@@ -119,7 +119,6 @@ func (h *AssignmentHandler) Create(c *gin.Context) {
|
||||
priority := c.PostForm("priority")
|
||||
dueDateStr := c.PostForm("due_date")
|
||||
|
||||
// Parse reminder settings
|
||||
reminderEnabled := c.PostForm("reminder_enabled") == "on"
|
||||
reminderAtStr := c.PostForm("reminder_at")
|
||||
var reminderAt *time.Time
|
||||
@@ -151,21 +150,101 @@ func (h *AssignmentHandler) Create(c *gin.Context) {
|
||||
dueDate = dueDate.Add(23*time.Hour + 59*time.Minute)
|
||||
}
|
||||
|
||||
_, err = h.assignmentService.Create(userID, title, description, subject, priority, dueDate, 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
|
||||
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")
|
||||
|
||||
recurringService := service.NewRecurringAssignmentService()
|
||||
input := service.CreateRecurringInput{
|
||||
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,
|
||||
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 {
|
||||
_, err = h.assignmentService.Create(userID, title, description, subject, priority, dueDate, 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
|
||||
}
|
||||
}
|
||||
|
||||
c.Redirect(http.StatusFound, "/assignments")
|
||||
@@ -202,7 +281,6 @@ func (h *AssignmentHandler) Update(c *gin.Context) {
|
||||
priority := c.PostForm("priority")
|
||||
dueDateStr := c.PostForm("due_date")
|
||||
|
||||
// Parse reminder settings
|
||||
reminderEnabled := c.PostForm("reminder_enabled") == "on"
|
||||
reminderAtStr := c.PostForm("reminder_at")
|
||||
var reminderAt *time.Time
|
||||
@@ -259,7 +337,6 @@ func (h *AssignmentHandler) Statistics(c *gin.Context) {
|
||||
role, _ := c.Get(middleware.UserRoleKey)
|
||||
name, _ := c.Get(middleware.UserNameKey)
|
||||
|
||||
// Parse filter parameters
|
||||
filter := service.StatisticsFilter{
|
||||
Subject: c.Query("subject"),
|
||||
IncludeArchived: c.Query("include_archived") == "true",
|
||||
@@ -268,7 +345,6 @@ func (h *AssignmentHandler) Statistics(c *gin.Context) {
|
||||
fromStr := c.Query("from")
|
||||
toStr := c.Query("to")
|
||||
|
||||
// Parse from date
|
||||
if fromStr != "" {
|
||||
fromDate, err := time.ParseInLocation("2006-01-02", fromStr, time.Local)
|
||||
if err == nil {
|
||||
@@ -276,7 +352,6 @@ func (h *AssignmentHandler) Statistics(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// Parse to date
|
||||
if toStr != "" {
|
||||
toDate, err := time.ParseInLocation("2006-01-02", toStr, time.Local)
|
||||
if err == nil {
|
||||
@@ -293,11 +368,9 @@ func (h *AssignmentHandler) Statistics(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Get available subjects for filter dropdown (exclude archived)
|
||||
subjects, _ := h.assignmentService.GetSubjectsWithArchived(userID, false)
|
||||
archivedSubjects, _ := h.assignmentService.GetArchivedSubjects(userID)
|
||||
|
||||
// Create a map for quick lookup of archived subjects
|
||||
archivedMap := make(map[string]bool)
|
||||
for _, s := range archivedSubjects {
|
||||
archivedMap[s] = true
|
||||
@@ -338,5 +411,3 @@ func (h *AssignmentHandler) UnarchiveSubject(c *gin.Context) {
|
||||
|
||||
c.Redirect(http.StatusFound, "/statistics?include_archived=true")
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -7,26 +7,29 @@ import (
|
||||
)
|
||||
|
||||
type Assignment struct {
|
||||
ID uint `gorm:"primarykey" json:"id"`
|
||||
UserID uint `gorm:"not null;index" json:"user_id"`
|
||||
Title string `gorm:"not null" json:"title"`
|
||||
Description string `json:"description"`
|
||||
Subject string `json:"subject"`
|
||||
Priority string `gorm:"not null;default:medium" json:"priority"` // low, medium, high
|
||||
DueDate time.Time `gorm:"not null" json:"due_date"`
|
||||
IsCompleted bool `gorm:"default:false" json:"is_completed"`
|
||||
IsArchived bool `gorm:"default:false;index" json:"is_archived"`
|
||||
CompletedAt *time.Time `json:"completed_at,omitempty"`
|
||||
// Reminder notification settings (one-time)
|
||||
ReminderEnabled bool `gorm:"default:false" json:"reminder_enabled"`
|
||||
ReminderAt *time.Time `json:"reminder_at,omitempty"`
|
||||
ReminderSent bool `gorm:"default:false;index" json:"reminder_sent"`
|
||||
// Urgent reminder settings (repeating until completed)
|
||||
UrgentReminderEnabled bool `gorm:"default:true" json:"urgent_reminder_enabled"`
|
||||
LastUrgentReminderSent *time.Time `json:"last_urgent_reminder_sent,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
ID uint `gorm:"primarykey" json:"id"`
|
||||
UserID uint `gorm:"not null;index" json:"user_id"`
|
||||
Title string `gorm:"not null" json:"title"`
|
||||
Description string `json:"description"`
|
||||
Subject string `json:"subject"`
|
||||
Priority string `gorm:"not null;default:medium" json:"priority"`
|
||||
DueDate time.Time `gorm:"not null" json:"due_date"`
|
||||
IsCompleted bool `gorm:"default:false" json:"is_completed"`
|
||||
IsArchived bool `gorm:"default:false;index" json:"is_archived"`
|
||||
CompletedAt *time.Time `json:"completed_at,omitempty"`
|
||||
ReminderEnabled bool `gorm:"default:false" json:"reminder_enabled"`
|
||||
ReminderAt *time.Time `json:"reminder_at,omitempty"`
|
||||
ReminderSent bool `gorm:"default:false;index" json:"reminder_sent"`
|
||||
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:"-"`
|
||||
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
|
||||
User *User `gorm:"foreignKey:UserID" json:"user,omitempty"`
|
||||
}
|
||||
|
||||
@@ -6,14 +6,13 @@ import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// UserNotificationSettings stores user's notification preferences
|
||||
type UserNotificationSettings struct {
|
||||
ID uint `gorm:"primarykey" json:"id"`
|
||||
UserID uint `gorm:"uniqueIndex;not null" json:"user_id"`
|
||||
TelegramEnabled bool `gorm:"default:false" json:"telegram_enabled"`
|
||||
TelegramChatID string `json:"telegram_chat_id"`
|
||||
LineEnabled bool `gorm:"default:false" json:"line_enabled"`
|
||||
LineNotifyToken string `json:"-"` // Hide token from JSON
|
||||
LineNotifyToken string `json:"-"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
|
||||
102
internal/models/recurring_assignment.go
Normal file
102
internal/models/recurring_assignment.go
Normal file
@@ -0,0 +1,102 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const (
|
||||
RecurrenceNone = "none"
|
||||
RecurrenceDaily = "daily"
|
||||
RecurrenceWeekly = "weekly"
|
||||
RecurrenceMonthly = "monthly"
|
||||
)
|
||||
|
||||
const (
|
||||
EndTypeNever = "never"
|
||||
EndTypeCount = "count"
|
||||
EndTypeDate = "date"
|
||||
)
|
||||
|
||||
const (
|
||||
EditBehaviorThisOnly = "this_only"
|
||||
EditBehaviorThisAndFuture = "this_and_future"
|
||||
EditBehaviorAll = "all"
|
||||
)
|
||||
|
||||
type RecurringAssignment struct {
|
||||
ID uint `gorm:"primarykey" json:"id"`
|
||||
UserID uint `gorm:"not null;index" json:"user_id"`
|
||||
Title string `gorm:"not null" json:"title"`
|
||||
Description string `json:"description"`
|
||||
Subject string `json:"subject"`
|
||||
Priority string `gorm:"not null;default:medium" json:"priority"`
|
||||
|
||||
RecurrenceType string `gorm:"not null;default:none" json:"recurrence_type"`
|
||||
RecurrenceInterval int `gorm:"not null;default:1" json:"recurrence_interval"`
|
||||
RecurrenceWeekday *int `json:"recurrence_weekday,omitempty"`
|
||||
RecurrenceDay *int `json:"recurrence_day,omitempty"`
|
||||
DueTime string `gorm:"not null" json:"due_time"`
|
||||
|
||||
EndType string `gorm:"not null;default:never" json:"end_type"`
|
||||
EndCount *int `json:"end_count,omitempty"`
|
||||
EndDate *time.Time `json:"end_date,omitempty"`
|
||||
GeneratedCount int `gorm:"default:0" json:"generated_count"`
|
||||
EditBehavior string `gorm:"not null;default:this_only" json:"edit_behavior"`
|
||||
|
||||
ReminderEnabled bool `gorm:"default:false" json:"reminder_enabled"`
|
||||
ReminderOffset *int `json:"reminder_offset,omitempty"`
|
||||
UrgentReminderEnabled bool `gorm:"default:true" json:"urgent_reminder_enabled"`
|
||||
IsActive bool `gorm:"default:true" json:"is_active"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
|
||||
User *User `gorm:"foreignKey:UserID" json:"user,omitempty"`
|
||||
Assignments []Assignment `gorm:"foreignKey:RecurringAssignmentID" json:"assignments,omitempty"`
|
||||
}
|
||||
|
||||
func (r *RecurringAssignment) ShouldGenerateNext() bool {
|
||||
if !r.IsActive || r.RecurrenceType == RecurrenceNone {
|
||||
return false
|
||||
}
|
||||
|
||||
switch r.EndType {
|
||||
case EndTypeCount:
|
||||
if r.EndCount != nil && r.GeneratedCount >= *r.EndCount {
|
||||
return false
|
||||
}
|
||||
case EndTypeDate:
|
||||
if r.EndDate != nil && time.Now().After(*r.EndDate) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (r *RecurringAssignment) CalculateNextDueDate(lastDueDate time.Time) time.Time {
|
||||
var nextDate time.Time
|
||||
|
||||
switch r.RecurrenceType {
|
||||
case RecurrenceDaily:
|
||||
nextDate = lastDueDate.AddDate(0, 0, r.RecurrenceInterval)
|
||||
case RecurrenceWeekly:
|
||||
nextDate = lastDueDate.AddDate(0, 0, 7*r.RecurrenceInterval)
|
||||
case RecurrenceMonthly:
|
||||
nextDate = lastDueDate.AddDate(0, r.RecurrenceInterval, 0)
|
||||
if r.RecurrenceDay != nil {
|
||||
day := *r.RecurrenceDay
|
||||
lastDayOfMonth := time.Date(nextDate.Year(), nextDate.Month()+1, 0, 0, 0, 0, 0, nextDate.Location()).Day()
|
||||
if day > lastDayOfMonth {
|
||||
day = lastDayOfMonth
|
||||
}
|
||||
nextDate = time.Date(nextDate.Year(), nextDate.Month(), day, nextDate.Hour(), nextDate.Minute(), 0, 0, nextDate.Location())
|
||||
}
|
||||
default:
|
||||
return lastDueDate
|
||||
}
|
||||
|
||||
return nextDate
|
||||
}
|
||||
@@ -187,7 +187,6 @@ func (r *AssignmentRepository) CountOverdueByUserID(userID uint) (int64, error)
|
||||
return count, err
|
||||
}
|
||||
|
||||
// StatisticsFilter holds filter parameters for statistics queries
|
||||
type StatisticsFilter struct {
|
||||
Subject string
|
||||
From *time.Time
|
||||
@@ -195,7 +194,6 @@ type StatisticsFilter struct {
|
||||
IncludeArchived bool
|
||||
}
|
||||
|
||||
// AssignmentStatistics holds statistics data
|
||||
type AssignmentStatistics struct {
|
||||
Total int64
|
||||
Completed int64
|
||||
@@ -205,7 +203,6 @@ type AssignmentStatistics struct {
|
||||
OnTimeCompletionRate float64
|
||||
}
|
||||
|
||||
// SubjectStatistics holds statistics for a specific subject
|
||||
type SubjectStatistics struct {
|
||||
Subject string
|
||||
Total int64
|
||||
@@ -216,64 +213,48 @@ type SubjectStatistics struct {
|
||||
OnTimeCompletionRate float64
|
||||
}
|
||||
|
||||
// GetStatistics returns statistics for a user with optional filters
|
||||
func (r *AssignmentRepository) GetStatistics(userID uint, filter StatisticsFilter) (*AssignmentStatistics, error) {
|
||||
now := time.Now()
|
||||
stats := &AssignmentStatistics{}
|
||||
|
||||
// Base query
|
||||
baseQuery := r.db.Model(&models.Assignment{}).Where("user_id = ?", userID)
|
||||
|
||||
// Apply subject filter
|
||||
if filter.Subject != "" {
|
||||
baseQuery = baseQuery.Where("subject = ?", filter.Subject)
|
||||
}
|
||||
|
||||
// Apply date range filter (by created_at)
|
||||
if filter.From != nil {
|
||||
baseQuery = baseQuery.Where("created_at >= ?", *filter.From)
|
||||
}
|
||||
if filter.To != nil {
|
||||
// Add 1 day to include the entire "to" date
|
||||
toEnd := filter.To.AddDate(0, 0, 1)
|
||||
baseQuery = baseQuery.Where("created_at < ?", toEnd)
|
||||
}
|
||||
|
||||
// Apply archived filter
|
||||
if !filter.IncludeArchived {
|
||||
baseQuery = baseQuery.Where("is_archived = ?", false)
|
||||
}
|
||||
|
||||
// Total count
|
||||
if err := baseQuery.Count(&stats.Total).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Completed count
|
||||
completedQuery := baseQuery.Session(&gorm.Session{})
|
||||
if err := completedQuery.Where("is_completed = ?", true).Count(&stats.Completed).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Pending count
|
||||
pendingQuery := baseQuery.Session(&gorm.Session{})
|
||||
if err := pendingQuery.Where("is_completed = ?", false).Count(&stats.Pending).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Overdue count
|
||||
overdueQuery := baseQuery.Session(&gorm.Session{})
|
||||
if err := overdueQuery.Where("is_completed = ? AND due_date < ?", false, now).Count(&stats.Overdue).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Completed on time count (completed_at <= due_date)
|
||||
onTimeQuery := baseQuery.Session(&gorm.Session{})
|
||||
if err := onTimeQuery.Where("is_completed = ? AND completed_at <= due_date", true).Count(&stats.CompletedOnTime).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Calculate on-time completion rate
|
||||
if stats.Completed > 0 {
|
||||
stats.OnTimeCompletionRate = float64(stats.CompletedOnTime) / float64(stats.Completed) * 100
|
||||
}
|
||||
@@ -281,7 +262,6 @@ func (r *AssignmentRepository) GetStatistics(userID uint, filter StatisticsFilte
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
// GetStatisticsBySubjects returns statistics grouped by subject
|
||||
func (r *AssignmentRepository) GetStatisticsBySubjects(userID uint, filter StatisticsFilter) ([]SubjectStatistics, error) {
|
||||
now := time.Now()
|
||||
subjects, err := r.GetSubjectsByUserID(userID)
|
||||
@@ -301,7 +281,6 @@ func (r *AssignmentRepository) GetStatisticsBySubjects(userID uint, filter Stati
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Count overdue for this subject
|
||||
overdueQuery := r.db.Model(&models.Assignment{}).
|
||||
Where("user_id = ? AND subject = ? AND is_completed = ? AND due_date < ?", userID, subject, false, now)
|
||||
if filter.From != nil {
|
||||
@@ -328,21 +307,18 @@ func (r *AssignmentRepository) GetStatisticsBySubjects(userID uint, filter Stati
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// ArchiveBySubject archives all assignments for a subject
|
||||
func (r *AssignmentRepository) ArchiveBySubject(userID uint, subject string) error {
|
||||
return r.db.Model(&models.Assignment{}).
|
||||
Where("user_id = ? AND subject = ?", userID, subject).
|
||||
Update("is_archived", true).Error
|
||||
}
|
||||
|
||||
// UnarchiveBySubject unarchives all assignments for a subject
|
||||
func (r *AssignmentRepository) UnarchiveBySubject(userID uint, subject string) error {
|
||||
return r.db.Model(&models.Assignment{}).
|
||||
Where("user_id = ? AND subject = ?", userID, subject).
|
||||
Update("is_archived", false).Error
|
||||
}
|
||||
|
||||
// GetArchivedSubjects returns a list of archived subjects for a user
|
||||
func (r *AssignmentRepository) GetArchivedSubjects(userID uint) ([]string, error) {
|
||||
var subjects []string
|
||||
err := r.db.Model(&models.Assignment{}).
|
||||
@@ -352,7 +328,6 @@ func (r *AssignmentRepository) GetArchivedSubjects(userID uint) ([]string, error
|
||||
return subjects, err
|
||||
}
|
||||
|
||||
// GetSubjectsByUserIDWithArchived returns subjects optionally including archived
|
||||
func (r *AssignmentRepository) GetSubjectsByUserIDWithArchived(userID uint, includeArchived bool) ([]string, error) {
|
||||
var subjects []string
|
||||
query := r.db.Model(&models.Assignment{}).
|
||||
|
||||
123
internal/repository/recurring_assignment_repository.go
Normal file
123
internal/repository/recurring_assignment_repository.go
Normal file
@@ -0,0 +1,123 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"homework-manager/internal/database"
|
||||
"homework-manager/internal/models"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type RecurringAssignmentRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewRecurringAssignmentRepository() *RecurringAssignmentRepository {
|
||||
return &RecurringAssignmentRepository{db: database.GetDB()}
|
||||
}
|
||||
|
||||
func (r *RecurringAssignmentRepository) Create(recurring *models.RecurringAssignment) error {
|
||||
return r.db.Create(recurring).Error
|
||||
}
|
||||
|
||||
func (r *RecurringAssignmentRepository) FindByID(id uint) (*models.RecurringAssignment, error) {
|
||||
var recurring models.RecurringAssignment
|
||||
err := r.db.First(&recurring, id).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &recurring, nil
|
||||
}
|
||||
|
||||
func (r *RecurringAssignmentRepository) FindByUserID(userID uint) ([]models.RecurringAssignment, error) {
|
||||
var recurrings []models.RecurringAssignment
|
||||
err := r.db.Where("user_id = ?", userID).Order("created_at DESC").Find(&recurrings).Error
|
||||
return recurrings, err
|
||||
}
|
||||
|
||||
func (r *RecurringAssignmentRepository) FindActiveByUserID(userID uint) ([]models.RecurringAssignment, error) {
|
||||
var recurrings []models.RecurringAssignment
|
||||
err := r.db.Where("user_id = ? AND is_active = ?", userID, true).Order("created_at DESC").Find(&recurrings).Error
|
||||
return recurrings, err
|
||||
}
|
||||
|
||||
func (r *RecurringAssignmentRepository) Update(recurring *models.RecurringAssignment) error {
|
||||
return r.db.Save(recurring).Error
|
||||
}
|
||||
|
||||
func (r *RecurringAssignmentRepository) Delete(id uint) error {
|
||||
return r.db.Delete(&models.RecurringAssignment{}, id).Error
|
||||
}
|
||||
|
||||
func (r *RecurringAssignmentRepository) FindDueForGeneration() ([]models.RecurringAssignment, error) {
|
||||
var recurrings []models.RecurringAssignment
|
||||
|
||||
err := r.db.Where("is_active = ? AND recurrence_type != ?", true, models.RecurrenceNone).
|
||||
Find(&recurrings).Error
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result []models.RecurringAssignment
|
||||
now := time.Now()
|
||||
for _, rec := range recurrings {
|
||||
shouldGenerate := true
|
||||
|
||||
switch rec.EndType {
|
||||
case models.EndTypeCount:
|
||||
if rec.EndCount != nil && rec.GeneratedCount >= *rec.EndCount {
|
||||
shouldGenerate = false
|
||||
}
|
||||
case models.EndTypeDate:
|
||||
if rec.EndDate != nil && now.After(*rec.EndDate) {
|
||||
shouldGenerate = false
|
||||
}
|
||||
}
|
||||
|
||||
if shouldGenerate {
|
||||
result = append(result, rec)
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *RecurringAssignmentRepository) GetLatestAssignmentByRecurringID(recurringID uint) (*models.Assignment, error) {
|
||||
var assignment models.Assignment
|
||||
err := r.db.Where("recurring_assignment_id = ?", recurringID).
|
||||
Order("due_date DESC").
|
||||
First(&assignment).Error
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &assignment, nil
|
||||
}
|
||||
|
||||
func (r *RecurringAssignmentRepository) GetAssignmentsByRecurringID(recurringID uint) ([]models.Assignment, error) {
|
||||
var assignments []models.Assignment
|
||||
err := r.db.Where("recurring_assignment_id = ?", recurringID).
|
||||
Order("due_date ASC").
|
||||
Find(&assignments).Error
|
||||
return assignments, err
|
||||
}
|
||||
|
||||
func (r *RecurringAssignmentRepository) GetFutureAssignmentsByRecurringID(recurringID uint, fromDate time.Time) ([]models.Assignment, error) {
|
||||
var assignments []models.Assignment
|
||||
err := r.db.Where("recurring_assignment_id = ? AND due_date >= ?", recurringID, fromDate).
|
||||
Order("due_date ASC").
|
||||
Find(&assignments).Error
|
||||
return assignments, err
|
||||
}
|
||||
|
||||
func (r *RecurringAssignmentRepository) CountPendingByRecurringID(recurringID uint) (int64, error) {
|
||||
var count int64
|
||||
err := r.db.Model(&models.Assignment{}).
|
||||
Where("recurring_assignment_id = ? AND is_completed = ?", recurringID, false).
|
||||
Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
@@ -45,6 +45,9 @@ func getFuncMap() template.FuncMap {
|
||||
"multiplyFloat": func(a float64, b float64) float64 {
|
||||
return a * b
|
||||
},
|
||||
"recurringLabel": service.GetRecurrenceTypeLabel,
|
||||
"endTypeLabel": service.GetEndTypeLabel,
|
||||
"recurringSummary": service.FormatRecurringSummary,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,6 +62,8 @@ func loadTemplates() (*template.Template, error) {
|
||||
pattern string
|
||||
prefix string
|
||||
}{
|
||||
{"web/templates/auth/*.html", ""},
|
||||
{"web/templates/pages/*.html", ""},
|
||||
{"web/templates/auth/*.html", ""},
|
||||
{"web/templates/pages/*.html", ""},
|
||||
{"web/templates/assignments/*.html", "assignments/"},
|
||||
@@ -175,7 +180,6 @@ func Setup(cfg *config.Config) *gin.Engine {
|
||||
apiKeyService := service.NewAPIKeyService()
|
||||
notificationService := service.NewNotificationService(cfg.Notification.TelegramBotToken)
|
||||
|
||||
// Start notification reminder scheduler
|
||||
notificationService.StartReminderScheduler()
|
||||
|
||||
authHandler := handler.NewAuthHandler()
|
||||
|
||||
@@ -209,7 +209,6 @@ func (s *AssignmentService) Update(userID, assignmentID uint, title, description
|
||||
assignment.ReminderEnabled = reminderEnabled
|
||||
assignment.ReminderAt = reminderAt
|
||||
assignment.UrgentReminderEnabled = urgentReminderEnabled
|
||||
// Reset reminder sent flag if reminder settings changed
|
||||
if reminderEnabled && reminderAt != nil {
|
||||
assignment.ReminderSent = false
|
||||
}
|
||||
@@ -279,7 +278,6 @@ func (s *AssignmentService) GetDashboardStats(userID uint) (*DashboardStats, err
|
||||
}, nil
|
||||
}
|
||||
|
||||
// StatisticsFilter holds filter parameters for statistics
|
||||
type StatisticsFilter struct {
|
||||
Subject string
|
||||
From *time.Time
|
||||
@@ -287,7 +285,6 @@ type StatisticsFilter struct {
|
||||
IncludeArchived bool
|
||||
}
|
||||
|
||||
// SubjectStats holds statistics for a subject
|
||||
type SubjectStats struct {
|
||||
Subject string `json:"subject"`
|
||||
Total int64 `json:"total"`
|
||||
@@ -298,7 +295,6 @@ type SubjectStats struct {
|
||||
IsArchived bool `json:"is_archived,omitempty"`
|
||||
}
|
||||
|
||||
// StatisticsSummary holds overall statistics
|
||||
type StatisticsSummary struct {
|
||||
TotalAssignments int64 `json:"total_assignments"`
|
||||
CompletedAssignments int64 `json:"completed_assignments"`
|
||||
@@ -309,7 +305,6 @@ type StatisticsSummary struct {
|
||||
Subjects []SubjectStats `json:"subjects,omitempty"`
|
||||
}
|
||||
|
||||
// FilterInfo shows applied filters in response
|
||||
type FilterInfo struct {
|
||||
Subject *string `json:"subject"`
|
||||
From *string `json:"from"`
|
||||
@@ -317,9 +312,7 @@ type FilterInfo struct {
|
||||
IncludeArchived bool `json:"include_archived"`
|
||||
}
|
||||
|
||||
// GetStatistics returns statistics for a user with optional filters
|
||||
func (s *AssignmentService) GetStatistics(userID uint, filter StatisticsFilter) (*StatisticsSummary, error) {
|
||||
// Convert filter to repository filter
|
||||
repoFilter := repository.StatisticsFilter{
|
||||
Subject: filter.Subject,
|
||||
From: filter.From,
|
||||
@@ -327,7 +320,6 @@ func (s *AssignmentService) GetStatistics(userID uint, filter StatisticsFilter)
|
||||
IncludeArchived: filter.IncludeArchived,
|
||||
}
|
||||
|
||||
// Get overall statistics
|
||||
stats, err := s.assignmentRepo.GetStatistics(userID, repoFilter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -341,7 +333,6 @@ func (s *AssignmentService) GetStatistics(userID uint, filter StatisticsFilter)
|
||||
OnTimeCompletionRate: stats.OnTimeCompletionRate,
|
||||
}
|
||||
|
||||
// Build filter info
|
||||
filterInfo := &FilterInfo{}
|
||||
hasFilter := false
|
||||
if filter.Subject != "" {
|
||||
@@ -366,7 +357,6 @@ func (s *AssignmentService) GetStatistics(userID uint, filter StatisticsFilter)
|
||||
summary.Filter = filterInfo
|
||||
}
|
||||
|
||||
// If no specific subject filter, get per-subject statistics
|
||||
if filter.Subject == "" {
|
||||
subjectStats, err := s.assignmentRepo.GetStatisticsBySubjects(userID, repoFilter)
|
||||
if err != nil {
|
||||
@@ -388,22 +378,17 @@ func (s *AssignmentService) GetStatistics(userID uint, filter StatisticsFilter)
|
||||
return summary, nil
|
||||
}
|
||||
|
||||
// ArchiveSubject archives all assignments for a subject
|
||||
func (s *AssignmentService) ArchiveSubject(userID uint, subject string) error {
|
||||
return s.assignmentRepo.ArchiveBySubject(userID, subject)
|
||||
}
|
||||
|
||||
// UnarchiveSubject unarchives all assignments for a subject
|
||||
func (s *AssignmentService) UnarchiveSubject(userID uint, subject string) error {
|
||||
return s.assignmentRepo.UnarchiveBySubject(userID, subject)
|
||||
}
|
||||
|
||||
// GetSubjectsWithArchived returns subjects optionally including archived
|
||||
func (s *AssignmentService) GetSubjectsWithArchived(userID uint, includeArchived bool) ([]string, error) {
|
||||
return s.assignmentRepo.GetSubjectsByUserIDWithArchived(userID, includeArchived)
|
||||
}
|
||||
|
||||
// GetArchivedSubjects returns archived subjects only
|
||||
func (s *AssignmentService) GetArchivedSubjects(userID uint) ([]string, error) {
|
||||
return s.assignmentRepo.GetArchivedSubjects(userID)
|
||||
}
|
||||
|
||||
@@ -14,24 +14,21 @@ import (
|
||||
"homework-manager/internal/models"
|
||||
)
|
||||
|
||||
// NotificationService handles Telegram and LINE notifications
|
||||
type NotificationService struct {
|
||||
telegramBotToken string
|
||||
}
|
||||
|
||||
// NewNotificationService creates a new notification service
|
||||
func NewNotificationService(telegramBotToken string) *NotificationService {
|
||||
return &NotificationService{
|
||||
telegramBotToken: telegramBotToken,
|
||||
}
|
||||
}
|
||||
|
||||
// GetUserSettings retrieves notification settings for a user
|
||||
|
||||
func (s *NotificationService) GetUserSettings(userID uint) (*models.UserNotificationSettings, error) {
|
||||
var settings models.UserNotificationSettings
|
||||
result := database.GetDB().Where("user_id = ?", userID).First(&settings)
|
||||
if result.Error != nil {
|
||||
// If not found, return a new empty settings object
|
||||
if result.RowsAffected == 0 {
|
||||
return &models.UserNotificationSettings{
|
||||
UserID: userID,
|
||||
@@ -42,7 +39,6 @@ func (s *NotificationService) GetUserSettings(userID uint) (*models.UserNotifica
|
||||
return &settings, nil
|
||||
}
|
||||
|
||||
// UpdateUserSettings updates notification settings for a user
|
||||
func (s *NotificationService) UpdateUserSettings(userID uint, settings *models.UserNotificationSettings) error {
|
||||
settings.UserID = userID
|
||||
|
||||
@@ -50,16 +46,12 @@ func (s *NotificationService) UpdateUserSettings(userID uint, settings *models.U
|
||||
result := database.GetDB().Where("user_id = ?", userID).First(&existing)
|
||||
|
||||
if result.RowsAffected == 0 {
|
||||
// Create new
|
||||
return database.GetDB().Create(settings).Error
|
||||
}
|
||||
|
||||
// Update existing
|
||||
settings.ID = existing.ID
|
||||
return database.GetDB().Save(settings).Error
|
||||
}
|
||||
|
||||
// SendTelegramNotification sends a message via Telegram Bot API
|
||||
func (s *NotificationService) SendTelegramNotification(chatID, message string) error {
|
||||
if s.telegramBotToken == "" {
|
||||
return fmt.Errorf("telegram bot token is not configured")
|
||||
@@ -94,7 +86,6 @@ func (s *NotificationService) SendTelegramNotification(chatID, message string) e
|
||||
return nil
|
||||
}
|
||||
|
||||
// SendLineNotification sends a message via LINE Notify API
|
||||
func (s *NotificationService) SendLineNotification(token, message string) error {
|
||||
if token == "" {
|
||||
return fmt.Errorf("LINE Notify token is empty")
|
||||
@@ -127,7 +118,6 @@ func (s *NotificationService) SendLineNotification(token, message string) error
|
||||
return nil
|
||||
}
|
||||
|
||||
// SendAssignmentReminder sends a reminder notification for an assignment
|
||||
func (s *NotificationService) SendAssignmentReminder(userID uint, assignment *models.Assignment) error {
|
||||
settings, err := s.GetUserSettings(userID)
|
||||
if err != nil {
|
||||
@@ -144,14 +134,12 @@ func (s *NotificationService) SendAssignmentReminder(userID uint, assignment *mo
|
||||
|
||||
var errors []string
|
||||
|
||||
// Send to Telegram if enabled
|
||||
if settings.TelegramEnabled && settings.TelegramChatID != "" {
|
||||
if err := s.SendTelegramNotification(settings.TelegramChatID, message); err != nil {
|
||||
errors = append(errors, fmt.Sprintf("Telegram: %v", err))
|
||||
}
|
||||
}
|
||||
|
||||
// Send to LINE if enabled
|
||||
if settings.LineEnabled && settings.LineNotifyToken != "" {
|
||||
if err := s.SendLineNotification(settings.LineNotifyToken, message); err != nil {
|
||||
errors = append(errors, fmt.Sprintf("LINE: %v", err))
|
||||
@@ -165,7 +153,6 @@ func (s *NotificationService) SendAssignmentReminder(userID uint, assignment *mo
|
||||
return nil
|
||||
}
|
||||
|
||||
// SendUrgentReminder sends an urgent reminder notification for an assignment
|
||||
func (s *NotificationService) SendUrgentReminder(userID uint, assignment *models.Assignment) error {
|
||||
settings, err := s.GetUserSettings(userID)
|
||||
if err != nil {
|
||||
@@ -203,14 +190,12 @@ func (s *NotificationService) SendUrgentReminder(userID uint, assignment *models
|
||||
|
||||
var errors []string
|
||||
|
||||
// Send to Telegram if enabled
|
||||
if settings.TelegramEnabled && settings.TelegramChatID != "" {
|
||||
if err := s.SendTelegramNotification(settings.TelegramChatID, message); err != nil {
|
||||
errors = append(errors, fmt.Sprintf("Telegram: %v", err))
|
||||
}
|
||||
}
|
||||
|
||||
// Send to LINE if enabled
|
||||
if settings.LineEnabled && settings.LineNotifyToken != "" {
|
||||
if err := s.SendLineNotification(settings.LineNotifyToken, message); err != nil {
|
||||
errors = append(errors, fmt.Sprintf("LINE: %v", err))
|
||||
@@ -224,8 +209,6 @@ func (s *NotificationService) SendUrgentReminder(userID uint, assignment *models
|
||||
return nil
|
||||
}
|
||||
|
||||
// getUrgentReminderInterval returns the reminder interval based on priority
|
||||
// high=10min, medium=30min, low=60min
|
||||
func getUrgentReminderInterval(priority string) time.Duration {
|
||||
switch priority {
|
||||
case "high":
|
||||
@@ -239,7 +222,6 @@ func getUrgentReminderInterval(priority string) time.Duration {
|
||||
}
|
||||
}
|
||||
|
||||
// ProcessPendingReminders checks and sends pending one-time reminders
|
||||
func (s *NotificationService) ProcessPendingReminders() {
|
||||
now := time.Now()
|
||||
|
||||
@@ -260,17 +242,14 @@ func (s *NotificationService) ProcessPendingReminders() {
|
||||
continue
|
||||
}
|
||||
|
||||
// Mark as sent
|
||||
database.GetDB().Model(&assignment).Update("reminder_sent", true)
|
||||
log.Printf("Sent reminder for assignment %d to user %d", assignment.ID, assignment.UserID)
|
||||
}
|
||||
}
|
||||
|
||||
// ProcessUrgentReminders checks and sends urgent (repeating) reminders
|
||||
// Starts 3 hours before deadline, repeats at interval based on priority
|
||||
func (s *NotificationService) ProcessUrgentReminders() {
|
||||
now := time.Now()
|
||||
urgentStartTime := 3 * time.Hour // Start 3 hours before deadline
|
||||
urgentStartTime := 3 * time.Hour
|
||||
|
||||
var assignments []models.Assignment
|
||||
result := database.GetDB().Where(
|
||||
@@ -286,12 +265,10 @@ func (s *NotificationService) ProcessUrgentReminders() {
|
||||
for _, assignment := range assignments {
|
||||
timeUntilDue := assignment.DueDate.Sub(now)
|
||||
|
||||
// Only send if within 3 hours of deadline
|
||||
if timeUntilDue > urgentStartTime {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if enough time has passed since last urgent reminder
|
||||
interval := getUrgentReminderInterval(assignment.Priority)
|
||||
|
||||
if assignment.LastUrgentReminderSent != nil {
|
||||
@@ -301,20 +278,17 @@ func (s *NotificationService) ProcessUrgentReminders() {
|
||||
}
|
||||
}
|
||||
|
||||
// Send urgent reminder
|
||||
if err := s.SendUrgentReminder(assignment.UserID, &assignment); err != nil {
|
||||
log.Printf("Error sending urgent reminder for assignment %d: %v", assignment.ID, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Update last sent time
|
||||
database.GetDB().Model(&assignment).Update("last_urgent_reminder_sent", now)
|
||||
log.Printf("Sent urgent reminder for assignment %d (priority: %s) to user %d",
|
||||
assignment.ID, assignment.Priority, assignment.UserID)
|
||||
}
|
||||
}
|
||||
|
||||
// StartReminderScheduler starts a background goroutine to process reminders
|
||||
func (s *NotificationService) StartReminderScheduler() {
|
||||
go func() {
|
||||
ticker := time.NewTicker(1 * time.Minute)
|
||||
|
||||
467
internal/service/recurring_assignment_service.go
Normal file
467
internal/service/recurring_assignment_service.go
Normal file
@@ -0,0 +1,467 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"homework-manager/internal/models"
|
||||
"homework-manager/internal/repository"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrRecurringAssignmentNotFound = errors.New("recurring assignment not found")
|
||||
ErrRecurringUnauthorized = errors.New("unauthorized")
|
||||
ErrInvalidRecurrenceType = errors.New("invalid recurrence type")
|
||||
ErrInvalidEndType = errors.New("invalid end type")
|
||||
)
|
||||
|
||||
type RecurringAssignmentService struct {
|
||||
recurringRepo *repository.RecurringAssignmentRepository
|
||||
assignmentRepo *repository.AssignmentRepository
|
||||
}
|
||||
|
||||
func NewRecurringAssignmentService() *RecurringAssignmentService {
|
||||
return &RecurringAssignmentService{
|
||||
recurringRepo: repository.NewRecurringAssignmentRepository(),
|
||||
assignmentRepo: repository.NewAssignmentRepository(),
|
||||
}
|
||||
}
|
||||
|
||||
type CreateRecurringInput struct {
|
||||
Title string
|
||||
Description string
|
||||
Subject string
|
||||
Priority string
|
||||
RecurrenceType string
|
||||
RecurrenceInterval int
|
||||
RecurrenceWeekday *int
|
||||
RecurrenceDay *int
|
||||
DueTime string
|
||||
EndType string
|
||||
EndCount *int
|
||||
EndDate *time.Time
|
||||
EditBehavior string
|
||||
ReminderEnabled bool
|
||||
ReminderOffset *int
|
||||
UrgentReminderEnabled bool
|
||||
FirstDueDate time.Time
|
||||
}
|
||||
|
||||
func (s *RecurringAssignmentService) Create(userID uint, input CreateRecurringInput) (*models.RecurringAssignment, error) {
|
||||
if !isValidRecurrenceType(input.RecurrenceType) {
|
||||
return nil, ErrInvalidRecurrenceType
|
||||
}
|
||||
|
||||
if !isValidEndType(input.EndType) {
|
||||
return nil, ErrInvalidEndType
|
||||
}
|
||||
|
||||
if input.RecurrenceInterval < 1 {
|
||||
input.RecurrenceInterval = 1
|
||||
}
|
||||
if input.EditBehavior == "" {
|
||||
input.EditBehavior = models.EditBehaviorThisOnly
|
||||
}
|
||||
|
||||
recurring := &models.RecurringAssignment{
|
||||
UserID: userID,
|
||||
Title: input.Title,
|
||||
Description: input.Description,
|
||||
Subject: input.Subject,
|
||||
Priority: input.Priority,
|
||||
RecurrenceType: input.RecurrenceType,
|
||||
RecurrenceInterval: input.RecurrenceInterval,
|
||||
RecurrenceWeekday: input.RecurrenceWeekday,
|
||||
RecurrenceDay: input.RecurrenceDay,
|
||||
DueTime: input.DueTime,
|
||||
EndType: input.EndType,
|
||||
EndCount: input.EndCount,
|
||||
EndDate: input.EndDate,
|
||||
EditBehavior: input.EditBehavior,
|
||||
ReminderEnabled: input.ReminderEnabled,
|
||||
ReminderOffset: input.ReminderOffset,
|
||||
UrgentReminderEnabled: input.UrgentReminderEnabled,
|
||||
IsActive: true,
|
||||
GeneratedCount: 0,
|
||||
}
|
||||
|
||||
if err := s.recurringRepo.Create(recurring); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := s.generateAssignment(recurring, input.FirstDueDate); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return recurring, nil
|
||||
}
|
||||
|
||||
func (s *RecurringAssignmentService) GetByID(userID, recurringID uint) (*models.RecurringAssignment, error) {
|
||||
recurring, err := s.recurringRepo.FindByID(recurringID)
|
||||
if err != nil {
|
||||
return nil, ErrRecurringAssignmentNotFound
|
||||
}
|
||||
|
||||
if recurring.UserID != userID {
|
||||
return nil, ErrRecurringUnauthorized
|
||||
}
|
||||
|
||||
return recurring, nil
|
||||
}
|
||||
|
||||
func (s *RecurringAssignmentService) GetAllByUser(userID uint) ([]models.RecurringAssignment, error) {
|
||||
return s.recurringRepo.FindByUserID(userID)
|
||||
}
|
||||
|
||||
func (s *RecurringAssignmentService) GetActiveByUser(userID uint) ([]models.RecurringAssignment, error) {
|
||||
return s.recurringRepo.FindActiveByUserID(userID)
|
||||
}
|
||||
|
||||
type UpdateRecurringInput struct {
|
||||
Title string
|
||||
Description string
|
||||
Subject string
|
||||
Priority string
|
||||
DueTime string
|
||||
EditBehavior string
|
||||
ReminderEnabled bool
|
||||
ReminderOffset *int
|
||||
UrgentReminderEnabled bool
|
||||
}
|
||||
|
||||
func (s *RecurringAssignmentService) Update(userID, recurringID uint, input UpdateRecurringInput) (*models.RecurringAssignment, error) {
|
||||
recurring, err := s.GetByID(userID, recurringID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
recurring.Title = input.Title
|
||||
recurring.Description = input.Description
|
||||
recurring.Subject = input.Subject
|
||||
recurring.Priority = input.Priority
|
||||
if input.DueTime != "" {
|
||||
recurring.DueTime = input.DueTime
|
||||
}
|
||||
if input.EditBehavior != "" {
|
||||
recurring.EditBehavior = input.EditBehavior
|
||||
}
|
||||
recurring.ReminderEnabled = input.ReminderEnabled
|
||||
recurring.ReminderOffset = input.ReminderOffset
|
||||
recurring.UrgentReminderEnabled = input.UrgentReminderEnabled
|
||||
|
||||
if err := s.recurringRepo.Update(recurring); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return recurring, nil
|
||||
}
|
||||
|
||||
func (s *RecurringAssignmentService) UpdateAssignmentWithBehavior(
|
||||
userID uint,
|
||||
assignment *models.Assignment,
|
||||
title, description, subject, priority string,
|
||||
dueDate 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)
|
||||
}
|
||||
|
||||
recurring, err := s.GetByID(userID, *assignment.RecurringAssignmentID)
|
||||
if err != nil {
|
||||
return s.updateSingleAssignment(assignment, title, description, subject, priority, dueDate, reminderEnabled, reminderAt, urgentReminderEnabled)
|
||||
}
|
||||
|
||||
switch editBehavior {
|
||||
case models.EditBehaviorThisOnly:
|
||||
return s.updateSingleAssignment(assignment, title, description, subject, priority, dueDate, reminderEnabled, reminderAt, urgentReminderEnabled)
|
||||
|
||||
case models.EditBehaviorThisAndFuture:
|
||||
if err := s.updateSingleAssignment(assignment, title, description, subject, priority, dueDate, reminderEnabled, reminderAt, urgentReminderEnabled); err != nil {
|
||||
return err
|
||||
}
|
||||
recurring.Title = title
|
||||
recurring.Description = description
|
||||
recurring.Subject = subject
|
||||
recurring.Priority = priority
|
||||
recurring.UrgentReminderEnabled = urgentReminderEnabled
|
||||
if err := s.recurringRepo.Update(recurring); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.updateFutureAssignments(recurring.ID, assignment.DueDate, title, description, subject, priority, urgentReminderEnabled)
|
||||
|
||||
case models.EditBehaviorAll:
|
||||
recurring.Title = title
|
||||
recurring.Description = description
|
||||
recurring.Subject = subject
|
||||
recurring.Priority = priority
|
||||
recurring.UrgentReminderEnabled = urgentReminderEnabled
|
||||
if err := s.recurringRepo.Update(recurring); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.updateAllPendingAssignments(recurring.ID, title, description, subject, priority, urgentReminderEnabled)
|
||||
|
||||
default:
|
||||
return s.updateSingleAssignment(assignment, title, description, subject, priority, dueDate, reminderEnabled, reminderAt, urgentReminderEnabled)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *RecurringAssignmentService) updateSingleAssignment(
|
||||
assignment *models.Assignment,
|
||||
title, description, subject, priority string,
|
||||
dueDate time.Time,
|
||||
reminderEnabled bool,
|
||||
reminderAt *time.Time,
|
||||
urgentReminderEnabled bool,
|
||||
) error {
|
||||
assignment.Title = title
|
||||
assignment.Description = description
|
||||
assignment.Subject = subject
|
||||
assignment.Priority = priority
|
||||
assignment.DueDate = dueDate
|
||||
assignment.ReminderEnabled = reminderEnabled
|
||||
assignment.ReminderAt = reminderAt
|
||||
assignment.UrgentReminderEnabled = urgentReminderEnabled
|
||||
return s.assignmentRepo.Update(assignment)
|
||||
}
|
||||
|
||||
func (s *RecurringAssignmentService) updateFutureAssignments(
|
||||
recurringID uint,
|
||||
fromDate time.Time,
|
||||
title, description, subject, priority string,
|
||||
urgentReminderEnabled bool,
|
||||
) error {
|
||||
assignments, err := s.recurringRepo.GetFutureAssignmentsByRecurringID(recurringID, fromDate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, a := range assignments {
|
||||
if a.IsCompleted {
|
||||
continue
|
||||
}
|
||||
a.Title = title
|
||||
a.Description = description
|
||||
a.Subject = subject
|
||||
a.Priority = priority
|
||||
a.UrgentReminderEnabled = urgentReminderEnabled
|
||||
if err := s.assignmentRepo.Update(&a); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *RecurringAssignmentService) updateAllPendingAssignments(
|
||||
recurringID uint,
|
||||
title, description, subject, priority string,
|
||||
urgentReminderEnabled bool,
|
||||
) error {
|
||||
assignments, err := s.recurringRepo.GetAssignmentsByRecurringID(recurringID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, a := range assignments {
|
||||
if a.IsCompleted {
|
||||
continue
|
||||
}
|
||||
a.Title = title
|
||||
a.Description = description
|
||||
a.Subject = subject
|
||||
a.Priority = priority
|
||||
a.UrgentReminderEnabled = urgentReminderEnabled
|
||||
if err := s.assignmentRepo.Update(&a); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *RecurringAssignmentService) Delete(userID, recurringID uint, deleteFutureAssignments bool) error {
|
||||
recurring, err := s.GetByID(userID, recurringID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if deleteFutureAssignments {
|
||||
assignments, err := s.recurringRepo.GetFutureAssignmentsByRecurringID(recurringID, time.Now())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, a := range assignments {
|
||||
if !a.IsCompleted {
|
||||
s.assignmentRepo.Delete(a.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return s.recurringRepo.Delete(recurring.ID)
|
||||
}
|
||||
|
||||
func (s *RecurringAssignmentService) GenerateNextAssignments() error {
|
||||
recurrings, err := s.recurringRepo.FindDueForGeneration()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, recurring := range recurrings {
|
||||
pendingCount, err := s.recurringRepo.CountPendingByRecurringID(recurring.ID)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if pendingCount == 0 {
|
||||
latest, err := s.recurringRepo.GetLatestAssignmentByRecurringID(recurring.ID)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var nextDueDate time.Time
|
||||
if latest != nil {
|
||||
nextDueDate = recurring.CalculateNextDueDate(latest.DueDate)
|
||||
} else {
|
||||
nextDueDate = time.Now()
|
||||
}
|
||||
|
||||
if nextDueDate.After(time.Now()) {
|
||||
s.generateAssignment(&recurring, nextDueDate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *RecurringAssignmentService) generateAssignment(recurring *models.RecurringAssignment, dueDate time.Time) error {
|
||||
if recurring.DueTime != "" {
|
||||
parts := strings.Split(recurring.DueTime, ":")
|
||||
if len(parts) == 2 {
|
||||
hour, _ := strconv.Atoi(parts[0])
|
||||
minute, _ := strconv.Atoi(parts[1])
|
||||
dueDate = time.Date(dueDate.Year(), dueDate.Month(), dueDate.Day(), hour, minute, 0, 0, dueDate.Location())
|
||||
}
|
||||
}
|
||||
|
||||
var reminderAt *time.Time
|
||||
if recurring.ReminderEnabled && recurring.ReminderOffset != nil {
|
||||
t := dueDate.Add(-time.Duration(*recurring.ReminderOffset) * time.Minute)
|
||||
reminderAt = &t
|
||||
}
|
||||
|
||||
assignment := &models.Assignment{
|
||||
UserID: userID(recurring.UserID),
|
||||
Title: recurring.Title,
|
||||
Description: recurring.Description,
|
||||
Subject: recurring.Subject,
|
||||
Priority: recurring.Priority,
|
||||
DueDate: dueDate,
|
||||
ReminderEnabled: recurring.ReminderEnabled,
|
||||
ReminderAt: reminderAt,
|
||||
UrgentReminderEnabled: recurring.UrgentReminderEnabled,
|
||||
RecurringAssignmentID: &recurring.ID,
|
||||
}
|
||||
|
||||
if err := s.assignmentRepo.Create(assignment); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
recurring.GeneratedCount++
|
||||
return s.recurringRepo.Update(recurring)
|
||||
}
|
||||
|
||||
func userID(id uint) uint {
|
||||
return id
|
||||
}
|
||||
|
||||
func isValidRecurrenceType(t string) bool {
|
||||
switch t {
|
||||
case models.RecurrenceNone, models.RecurrenceDaily, models.RecurrenceWeekly, models.RecurrenceMonthly:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isValidEndType(t string) bool {
|
||||
switch t {
|
||||
case models.EndTypeNever, models.EndTypeCount, models.EndTypeDate:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func GetRecurrenceTypeLabel(t string) string {
|
||||
switch t {
|
||||
case models.RecurrenceDaily:
|
||||
return "毎日"
|
||||
case models.RecurrenceWeekly:
|
||||
return "毎週"
|
||||
case models.RecurrenceMonthly:
|
||||
return "毎月"
|
||||
default:
|
||||
return "なし"
|
||||
}
|
||||
}
|
||||
|
||||
func GetEndTypeLabel(t string) string {
|
||||
switch t {
|
||||
case models.EndTypeCount:
|
||||
return "回数指定"
|
||||
case models.EndTypeDate:
|
||||
return "終了日指定"
|
||||
default:
|
||||
return "無期限"
|
||||
}
|
||||
}
|
||||
|
||||
func FormatRecurringSummary(recurring *models.RecurringAssignment) string {
|
||||
if recurring.RecurrenceType == models.RecurrenceNone {
|
||||
return ""
|
||||
}
|
||||
|
||||
var parts []string
|
||||
|
||||
typeLabel := GetRecurrenceTypeLabel(recurring.RecurrenceType)
|
||||
if recurring.RecurrenceInterval > 1 {
|
||||
switch recurring.RecurrenceType {
|
||||
case models.RecurrenceDaily:
|
||||
parts = append(parts, fmt.Sprintf("%d日ごと", recurring.RecurrenceInterval))
|
||||
case models.RecurrenceWeekly:
|
||||
parts = append(parts, fmt.Sprintf("%d週間ごと", recurring.RecurrenceInterval))
|
||||
case models.RecurrenceMonthly:
|
||||
parts = append(parts, fmt.Sprintf("%dヶ月ごと", recurring.RecurrenceInterval))
|
||||
}
|
||||
} else {
|
||||
parts = append(parts, typeLabel)
|
||||
}
|
||||
|
||||
if recurring.RecurrenceType == models.RecurrenceWeekly && recurring.RecurrenceWeekday != nil {
|
||||
weekdays := []string{"日", "月", "火", "水", "木", "金", "土"}
|
||||
if *recurring.RecurrenceWeekday >= 0 && *recurring.RecurrenceWeekday < 7 {
|
||||
parts = append(parts, fmt.Sprintf("(%s曜日)", weekdays[*recurring.RecurrenceWeekday]))
|
||||
}
|
||||
}
|
||||
|
||||
if recurring.RecurrenceType == models.RecurrenceMonthly && recurring.RecurrenceDay != nil {
|
||||
parts = append(parts, fmt.Sprintf("(%d日)", *recurring.RecurrenceDay))
|
||||
}
|
||||
|
||||
switch recurring.EndType {
|
||||
case models.EndTypeCount:
|
||||
if recurring.EndCount != nil {
|
||||
parts = append(parts, fmt.Sprintf("/ %d回まで", *recurring.EndCount))
|
||||
}
|
||||
case models.EndTypeDate:
|
||||
if recurring.EndDate != nil {
|
||||
parts = append(parts, fmt.Sprintf("/ %sまで", recurring.EndDate.Format("2006/01/02")))
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(parts, " ")
|
||||
}
|
||||
Reference in New Issue
Block a user