- Categories: regular, deposits, credits, recurring, multi-user, accounts - Transactions: deposits and withdrawals with user tracking - Recurring plans: monthly payment obligations per user - Stats: overdues calculation with allocation algorithm - Excludes is_account categories from total sums - Documentation: docs/SAVINGS.md
239 lines
6.1 KiB
Go
239 lines
6.1 KiB
Go
package service
|
|
|
|
import (
|
|
"database/sql"
|
|
"time"
|
|
|
|
"github.com/daniil/homelab-api/internal/model"
|
|
"github.com/daniil/homelab-api/internal/repository"
|
|
)
|
|
|
|
type TaskService struct {
|
|
taskRepo *repository.TaskRepository
|
|
}
|
|
|
|
func NewTaskService(taskRepo *repository.TaskRepository) *TaskService {
|
|
return &TaskService{taskRepo: taskRepo}
|
|
}
|
|
|
|
func (s *TaskService) Create(userID int64, req *model.CreateTaskRequest) (*model.Task, error) {
|
|
task := &model.Task{
|
|
UserID: userID,
|
|
Title: req.Title,
|
|
Description: req.Description,
|
|
Icon: defaultString(req.Icon, "📋"),
|
|
Color: defaultString(req.Color, "#6B7280"),
|
|
Priority: req.Priority,
|
|
IsRecurring: req.IsRecurring,
|
|
RecurrenceInterval: defaultInt(req.RecurrenceInterval, 1),
|
|
}
|
|
|
|
if req.DueDate != nil && *req.DueDate != "" {
|
|
parsed, err := time.Parse("2006-01-02", *req.DueDate)
|
|
if err == nil {
|
|
task.DueDate = sql.NullTime{Time: parsed, Valid: true}
|
|
}
|
|
}
|
|
|
|
if req.ReminderTime != nil && *req.ReminderTime != "" {
|
|
task.ReminderTime = sql.NullString{String: *req.ReminderTime, Valid: true}
|
|
}
|
|
|
|
if req.RecurrenceType != nil && *req.RecurrenceType != "" {
|
|
task.RecurrenceType = sql.NullString{String: *req.RecurrenceType, Valid: true}
|
|
}
|
|
|
|
if req.RecurrenceEndDate != nil && *req.RecurrenceEndDate != "" {
|
|
parsed, err := time.Parse("2006-01-02", *req.RecurrenceEndDate)
|
|
if err == nil {
|
|
task.RecurrenceEndDate = sql.NullTime{Time: parsed, Valid: true}
|
|
}
|
|
}
|
|
|
|
if err := s.taskRepo.Create(task); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
task.ProcessForJSON()
|
|
return task, nil
|
|
}
|
|
|
|
func (s *TaskService) Get(id, userID int64) (*model.Task, error) {
|
|
return s.taskRepo.GetByID(id, userID)
|
|
}
|
|
|
|
func (s *TaskService) List(userID int64, completed *bool) ([]model.Task, error) {
|
|
tasks, err := s.taskRepo.ListByUser(userID, completed)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if tasks == nil {
|
|
tasks = []model.Task{}
|
|
}
|
|
return tasks, nil
|
|
}
|
|
|
|
func (s *TaskService) GetTodayTasks(userID int64) ([]model.Task, error) {
|
|
tasks, err := s.taskRepo.GetTodayTasks(userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if tasks == nil {
|
|
tasks = []model.Task{}
|
|
}
|
|
return tasks, nil
|
|
}
|
|
|
|
func (s *TaskService) Update(id, userID int64, req *model.UpdateTaskRequest) (*model.Task, error) {
|
|
task, err := s.taskRepo.GetByID(id, userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if req.Title != nil {
|
|
task.Title = *req.Title
|
|
}
|
|
if req.Description != nil {
|
|
task.Description = *req.Description
|
|
}
|
|
if req.Icon != nil {
|
|
task.Icon = *req.Icon
|
|
}
|
|
if req.Color != nil {
|
|
task.Color = *req.Color
|
|
}
|
|
if req.Priority != nil {
|
|
task.Priority = *req.Priority
|
|
}
|
|
if req.DueDate != nil {
|
|
if *req.DueDate == "" {
|
|
task.DueDate = sql.NullTime{Valid: false}
|
|
} else {
|
|
parsed, err := time.Parse("2006-01-02", *req.DueDate)
|
|
if err == nil {
|
|
task.DueDate = sql.NullTime{Time: parsed, Valid: true}
|
|
}
|
|
}
|
|
}
|
|
if req.ReminderTime != nil {
|
|
if *req.ReminderTime == "" {
|
|
task.ReminderTime = sql.NullString{Valid: false}
|
|
} else {
|
|
task.ReminderTime = sql.NullString{String: *req.ReminderTime, Valid: true}
|
|
}
|
|
}
|
|
|
|
// Handle recurring fields
|
|
if req.IsRecurring != nil {
|
|
task.IsRecurring = *req.IsRecurring
|
|
}
|
|
if req.RecurrenceType != nil {
|
|
if *req.RecurrenceType == "" {
|
|
task.RecurrenceType = sql.NullString{Valid: false}
|
|
} else {
|
|
task.RecurrenceType = sql.NullString{String: *req.RecurrenceType, Valid: true}
|
|
}
|
|
}
|
|
if req.RecurrenceInterval != nil {
|
|
task.RecurrenceInterval = *req.RecurrenceInterval
|
|
}
|
|
if req.RecurrenceEndDate != nil {
|
|
if *req.RecurrenceEndDate == "" {
|
|
task.RecurrenceEndDate = sql.NullTime{Valid: false}
|
|
} else {
|
|
parsed, err := time.Parse("2006-01-02", *req.RecurrenceEndDate)
|
|
if err == nil {
|
|
task.RecurrenceEndDate = sql.NullTime{Time: parsed, Valid: true}
|
|
}
|
|
}
|
|
}
|
|
|
|
if err := s.taskRepo.Update(task); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
task.ProcessForJSON()
|
|
return task, nil
|
|
}
|
|
|
|
func (s *TaskService) Delete(id, userID int64) error {
|
|
return s.taskRepo.Delete(id, userID)
|
|
}
|
|
|
|
func (s *TaskService) Complete(id, userID int64) (*model.Task, error) {
|
|
// First, get the task to check if it's recurring
|
|
task, err := s.taskRepo.GetByID(id, userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Complete the current task
|
|
if err := s.taskRepo.Complete(id, userID); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// If task is recurring, create the next occurrence
|
|
if task.IsRecurring && task.RecurrenceType.Valid && task.DueDate.Valid {
|
|
s.createNextRecurrence(task)
|
|
}
|
|
|
|
return s.taskRepo.GetByID(id, userID)
|
|
}
|
|
|
|
func (s *TaskService) createNextRecurrence(task *model.Task) {
|
|
// Calculate next due date based on recurrence type
|
|
var nextDueDate time.Time
|
|
interval := task.RecurrenceInterval
|
|
if interval < 1 {
|
|
interval = 1
|
|
}
|
|
|
|
currentDue := task.DueDate.Time
|
|
|
|
switch task.RecurrenceType.String {
|
|
case "daily":
|
|
nextDueDate = currentDue.AddDate(0, 0, interval)
|
|
case "weekly":
|
|
nextDueDate = currentDue.AddDate(0, 0, 7*interval)
|
|
case "monthly":
|
|
nextDueDate = currentDue.AddDate(0, interval, 0)
|
|
case "custom":
|
|
nextDueDate = currentDue.AddDate(0, 0, interval)
|
|
default:
|
|
return // Unknown recurrence type, don't create
|
|
}
|
|
|
|
// Check if next date is past the end date
|
|
if task.RecurrenceEndDate.Valid && nextDueDate.After(task.RecurrenceEndDate.Time) {
|
|
return // Don't create task past end date
|
|
}
|
|
|
|
// Create the next task
|
|
nextTask := &model.Task{
|
|
UserID: task.UserID,
|
|
Title: task.Title,
|
|
Description: task.Description,
|
|
Icon: task.Icon,
|
|
Color: task.Color,
|
|
Priority: task.Priority,
|
|
DueDate: sql.NullTime{Time: nextDueDate, Valid: true},
|
|
ReminderTime: task.ReminderTime,
|
|
IsRecurring: true,
|
|
RecurrenceType: task.RecurrenceType,
|
|
RecurrenceInterval: task.RecurrenceInterval,
|
|
RecurrenceEndDate: task.RecurrenceEndDate,
|
|
ParentTaskID: sql.NullInt64{Int64: task.ID, Valid: true},
|
|
}
|
|
|
|
// Silently create, ignore errors
|
|
s.taskRepo.Create(nextTask)
|
|
}
|
|
|
|
func (s *TaskService) Uncomplete(id, userID int64) (*model.Task, error) {
|
|
if err := s.taskRepo.Uncomplete(id, userID); err != nil {
|
|
return nil, err
|
|
}
|
|
return s.taskRepo.GetByID(id, userID)
|
|
}
|
|
|