2024-07-01 02:41:41 +01:00
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
}
2024-07-06 07:36:14 +01:00
func ( r * ChoreRepository ) GetChoreDetailByID ( c context . Context , choreID int , circleID int ) ( * chModel . ChoreDetail , error ) {
var choreDetail chModel . ChoreDetail
if err := r . db . WithContext ( c ) .
Table ( "chores" ) .
Select ( `
chores . id ,
chores . name ,
chores . frequency_type ,
chores . next_due_date ,
chores . assigned_to ,
chores . created_by ,
recent_history . last_completed_date ,
recent_history . last_assigned_to as last_completed_by ,
COUNT ( chore_histories . id ) as total_completed ` ) .
Joins ( "LEFT JOIN chore_histories ON chores.id = chore_histories.chore_id" ) .
Joins ( ` LEFT JOIN (
SELECT
chore_id ,
assigned_to AS last_assigned_to ,
completed_at AS last_completed_date
FROM chore_histories
WHERE ( chore_id , completed_at ) IN (
SELECT chore_id , MAX ( completed_at )
FROM chore_histories
GROUP BY chore_id
)
) AS recent_history ON chores . id = recent_history . chore_id ` ) .
Where ( "chores.id = ? and chores.circle_id = ?" , choreID , circleID ) .
Group ( "chores.id" ) .
First ( & choreDetail ) . Error ; err != nil {
return nil , err
}
return & choreDetail , nil
}