217 lines
9.8 KiB
Go
217 lines
9.8 KiB
Go
|
package chore
|
||
|
|
||
|
import (
|
||
|
"context"
|
||
|
"fmt"
|
||
|
"time"
|
||
|
|
||
|
config "donetick.com/core/config"
|
||
|
chModel "donetick.com/core/internal/chore/model"
|
||
|
"gorm.io/gorm"
|
||
|
)
|
||
|
|
||
|
type ChoreRepository struct {
|
||
|
db *gorm.DB
|
||
|
dbType string
|
||
|
}
|
||
|
|
||
|
func NewChoreRepository(db *gorm.DB, cfg *config.Config) *ChoreRepository {
|
||
|
return &ChoreRepository{db: db, dbType: cfg.Database.Type}
|
||
|
}
|
||
|
|
||
|
func (r *ChoreRepository) UpsertChore(c context.Context, chore *chModel.Chore) error {
|
||
|
return r.db.WithContext(c).Model(&chore).Save(chore).Error
|
||
|
}
|
||
|
|
||
|
func (r *ChoreRepository) UpdateChores(c context.Context, chores []*chModel.Chore) error {
|
||
|
return r.db.WithContext(c).Save(&chores).Error
|
||
|
}
|
||
|
func (r *ChoreRepository) CreateChore(c context.Context, chore *chModel.Chore) (int, error) {
|
||
|
if err := r.db.WithContext(c).Create(chore).Error; err != nil {
|
||
|
return 0, err
|
||
|
}
|
||
|
return chore.ID, nil
|
||
|
}
|
||
|
|
||
|
func (r *ChoreRepository) GetChore(c context.Context, choreID int) (*chModel.Chore, error) {
|
||
|
var chore chModel.Chore
|
||
|
if err := r.db.Debug().WithContext(c).Model(&chModel.Chore{}).Preload("Assignees").Preload("ThingChore").First(&chore, choreID).Error; err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
return &chore, nil
|
||
|
}
|
||
|
|
||
|
func (r *ChoreRepository) GetChores(c context.Context, circleID int, userID int) ([]*chModel.Chore, error) {
|
||
|
var chores []*chModel.Chore
|
||
|
// if err := r.db.WithContext(c).Preload("Assignees").Where("is_active = ?", true).Order("next_due_date asc").Find(&chores, "circle_id = ?", circleID).Error; err != nil {
|
||
|
if err := r.db.WithContext(c).Preload("Assignees").Joins("left join chore_assignees on chores.id = chore_assignees.chore_id").Where("chores.circle_id = ? AND (chores.created_by = ? OR chore_assignees.user_id = ?)", circleID, userID, userID).Group("chores.id").Order("next_due_date asc").Find(&chores, "circle_id = ?", circleID).Error; err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
return chores, nil
|
||
|
}
|
||
|
func (r *ChoreRepository) DeleteChore(c context.Context, id int) error {
|
||
|
r.db.WithContext(c).Where("chore_id = ?", id).Delete(&chModel.ChoreAssignees{})
|
||
|
return r.db.WithContext(c).Delete(&chModel.Chore{}, id).Error
|
||
|
}
|
||
|
|
||
|
func (r *ChoreRepository) SoftDelete(c context.Context, id int, userID int) error {
|
||
|
return r.db.WithContext(c).Model(&chModel.Chore{}).Where("id = ?", id).Where("created_by = ? ", userID).Update("is_active", false).Error
|
||
|
|
||
|
}
|
||
|
|
||
|
func (r *ChoreRepository) IsChoreOwner(c context.Context, choreID int, userID int) error {
|
||
|
var chore chModel.Chore
|
||
|
err := r.db.WithContext(c).Model(&chModel.Chore{}).Where("id = ? AND created_by = ?", choreID, userID).First(&chore).Error
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
// func (r *ChoreRepository) ListChores(circleID int) ([]*chModel.Chore, error) {
|
||
|
// var chores []*Chore
|
||
|
// if err := r.db.WithContext(c).Find(&chores).Where("is_active = ?", true).Order("next_due_date").Error; err != nil {
|
||
|
// return nil, err
|
||
|
// }
|
||
|
// return chores, nil
|
||
|
// }
|
||
|
|
||
|
func (r *ChoreRepository) CompleteChore(c context.Context, chore *chModel.Chore, note *string, userID int, dueDate *time.Time, completedDate time.Time, nextAssignedTo int) error {
|
||
|
err := r.db.WithContext(c).Transaction(func(tx *gorm.DB) error {
|
||
|
ch := &chModel.ChoreHistory{
|
||
|
ChoreID: chore.ID,
|
||
|
CompletedAt: completedDate,
|
||
|
CompletedBy: userID,
|
||
|
AssignedTo: chore.AssignedTo,
|
||
|
DueDate: chore.NextDueDate,
|
||
|
Note: note,
|
||
|
}
|
||
|
if err := tx.Create(ch).Error; err != nil {
|
||
|
return err
|
||
|
}
|
||
|
updates := map[string]interface{}{}
|
||
|
updates["next_due_date"] = dueDate
|
||
|
|
||
|
if dueDate != nil {
|
||
|
updates["assigned_to"] = nextAssignedTo
|
||
|
} else {
|
||
|
updates["is_active"] = false
|
||
|
}
|
||
|
// Perform the update operation once, using the prepared updates map.
|
||
|
if err := tx.Model(&chModel.Chore{}).Where("id = ?", chore.ID).Updates(updates).Error; err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
})
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
func (r *ChoreRepository) GetChoreHistory(c context.Context, choreID int) ([]*chModel.ChoreHistory, error) {
|
||
|
var histories []*chModel.ChoreHistory
|
||
|
if err := r.db.WithContext(c).Where("chore_id = ?", choreID).Order("completed_at desc").Find(&histories).Error; err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
return histories, nil
|
||
|
}
|
||
|
|
||
|
func (r *ChoreRepository) UpdateChoreAssignees(c context.Context, assignees []*chModel.ChoreAssignees) error {
|
||
|
return r.db.WithContext(c).Save(&assignees).Error
|
||
|
}
|
||
|
|
||
|
func (r *ChoreRepository) DeleteChoreAssignees(c context.Context, choreAssignees []*chModel.ChoreAssignees) error {
|
||
|
return r.db.WithContext(c).Delete(&choreAssignees).Error
|
||
|
}
|
||
|
|
||
|
func (r *ChoreRepository) GetChoreAssignees(c context.Context, choreID int) ([]*chModel.ChoreAssignees, error) {
|
||
|
var assignees []*chModel.ChoreAssignees
|
||
|
if err := r.db.WithContext(c).Find(&assignees, "chore_id = ?", choreID).Error; err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
return assignees, nil
|
||
|
}
|
||
|
|
||
|
func (r *ChoreRepository) RemoveChoreAssigneeByCircleID(c context.Context, userID int, circleID int) error {
|
||
|
return r.db.WithContext(c).Where("user_id = ? AND chore_id IN (SELECT id FROM chores WHERE circle_id = ? and created_by != ?)", userID, circleID, userID).Delete(&chModel.ChoreAssignees{}).Error
|
||
|
}
|
||
|
|
||
|
// func (r *ChoreRepository) getChoreDueToday(circleID int) ([]*chModel.Chore, error) {
|
||
|
// var chores []*Chore
|
||
|
// if err := r.db.WithContext(c).Where("next_due_date <= ?", time.Now().UTC()).Find(&chores).Error; err != nil {
|
||
|
// return nil, err
|
||
|
// }
|
||
|
// return chores, nil
|
||
|
// }
|
||
|
|
||
|
func (r *ChoreRepository) GetAllActiveChores(c context.Context) ([]*chModel.Chore, error) {
|
||
|
var chores []*chModel.Chore
|
||
|
// query := r.db.WithContext(c).Table("chores").Joins("left join notifications n on n.chore_id = chores.id and n.scheduled_for < chores.next_due_date")
|
||
|
// if err := query.Where("chores.is_active = ? and chores.notification = ? and (n.is_sent = ? or n.is_sent is null)", true, true, false).Find(&chores).Error; err != nil {
|
||
|
// return nil, err
|
||
|
// }
|
||
|
return chores, nil
|
||
|
}
|
||
|
|
||
|
func (r *ChoreRepository) GetChoresForNotification(c context.Context) ([]*chModel.Chore, error) {
|
||
|
var chores []*chModel.Chore
|
||
|
query := r.db.WithContext(c).Table("chores").Joins("left join notifications n on n.chore_id = chores.id and n.scheduled_for = chores.next_due_date and n.type = 1")
|
||
|
if err := query.Where("chores.is_active = ? and chores.notification = ? and n.id is null", true, true).Find(&chores).Error; err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
return chores, nil
|
||
|
}
|
||
|
|
||
|
// func (r *ChoreReposity) GetOverdueChoresForNotification(c context.Context, overdueDuration time.Duration, everyDuration time.Duration, untilDuration time.Duration) ([]*chModel.Chore, error) {
|
||
|
// var chores []*chModel.Chore
|
||
|
// query := r.db.Debug().WithContext(c).Table("chores").Select("chores.*, MAX(n.created_at) as max_notification_created_at").Joins("left join notifications n on n.chore_id = chores.id and n.scheduled_for = chores.next_due_date and n.type = 2")
|
||
|
// if err := query.Where("chores.is_active = ? and chores.notification = ? and chores.next_due_date < ? and chores.next_due_date > ?", true, true, time.Now().Add(overdueDuration).UTC(), time.Now().Add(untilDuration).UTC()).Where(readJSONBooleanField(r.dbType, "chores.notification_meta", "nagging")).Having("MAX(n.created_at) is null or MAX(n.created_at) < ?", time.Now().Add(everyDuration).UTC()).Group("chores.id").Find(&chores).Error; err != nil {
|
||
|
// return nil, err
|
||
|
// }
|
||
|
// return chores, nil
|
||
|
// }
|
||
|
|
||
|
func (r *ChoreRepository) GetOverdueChoresForNotification(c context.Context, overdueFor time.Duration, everyDuration time.Duration, untilDuration time.Duration) ([]*chModel.Chore, error) {
|
||
|
var chores []*chModel.Chore
|
||
|
now := time.Now().UTC()
|
||
|
overdueTime := now.Add(-overdueFor)
|
||
|
everyTime := now.Add(-everyDuration)
|
||
|
untilTime := now.Add(-untilDuration)
|
||
|
|
||
|
query := r.db.Debug().WithContext(c).
|
||
|
Table("chores").
|
||
|
Select("chores.*, MAX(n.created_at) as max_notification_created_at").
|
||
|
Joins("left join notifications n on n.chore_id = chores.id and n.type = 2").
|
||
|
Where("chores.is_active = ? AND chores.notification = ? AND chores.next_due_date < ? AND chores.next_due_date > ?", true, true, overdueTime, untilTime).
|
||
|
Where(readJSONBooleanField(r.dbType, "chores.notification_meta", "nagging")).
|
||
|
Group("chores.id").
|
||
|
Having("MAX(n.created_at) IS NULL OR MAX(n.created_at) < ?", everyTime)
|
||
|
|
||
|
if err := query.Find(&chores).Error; err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
return chores, nil
|
||
|
}
|
||
|
|
||
|
// a predue notfication is a notification send before the due date in 6 hours, 3 hours :
|
||
|
func (r *ChoreRepository) GetPreDueChoresForNotification(c context.Context, preDueDuration time.Duration, everyDuration time.Duration) ([]*chModel.Chore, error) {
|
||
|
var chores []*chModel.Chore
|
||
|
query := r.db.WithContext(c).Table("chores").Select("chores.*, MAX(n.created_at) as max_notification_created_at").Joins("left join notifications n on n.chore_id = chores.id and n.scheduled_for = chores.next_due_date and n.type = 3")
|
||
|
if err := query.Where("chores.is_active = ? and chores.notification = ? and chores.next_due_date > ? and chores.next_due_date < ?", true, true, time.Now().UTC(), time.Now().Add(everyDuration*2).UTC()).Where(readJSONBooleanField(r.dbType, "chores.notification_meta", "predue")).Having("MAX(n.created_at) is null or MAX(n.created_at) < ?", time.Now().Add(everyDuration).UTC()).Group("chores.id").Find(&chores).Error; err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
return chores, nil
|
||
|
}
|
||
|
|
||
|
func readJSONBooleanField(dbType string, columnName string, fieldName string) string {
|
||
|
if dbType == "postgres" {
|
||
|
return fmt.Sprintf("(%s::json->>'%s')::boolean", columnName, fieldName)
|
||
|
}
|
||
|
return fmt.Sprintf("JSON_EXTRACT(%s, '$.%s')", columnName, fieldName)
|
||
|
}
|
||
|
|
||
|
func (r *ChoreRepository) SetDueDate(c context.Context, choreID int, dueDate time.Time) error {
|
||
|
return r.db.WithContext(c).Model(&chModel.Chore{}).Where("id = ?", choreID).Update("next_due_date", dueDate).Error
|
||
|
}
|
||
|
|
||
|
func (r *ChoreRepository) SetDueDateIfNotExisted(c context.Context, choreID int, dueDate time.Time) error {
|
||
|
return r.db.WithContext(c).Model(&chModel.Chore{}).Where("id = ? and next_due_date is null", choreID).Update("next_due_date", dueDate).Error
|
||
|
}
|