feat: замена period_type на start_date в wishlist, обновление UI формы условий
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m5s
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m5s
- Добавлена миграция 020 для замены period_type на start_date в score_conditions - Обновлена функция подсчёта баллов: calculateProjectPointsFromDate вместо calculateProjectPointsForPeriod - Добавлен компонент DateSelector для выбора даты начала подсчёта - По умолчанию выбран тип условия 'Баллы' - Переименованы опции: 'Баллы' и 'Задача' - Версия: 3.9.3
This commit is contained in:
@@ -293,7 +293,7 @@ type UnlockConditionDisplay struct {
|
||||
TaskName *string `json:"task_name,omitempty"`
|
||||
ProjectName *string `json:"project_name,omitempty"`
|
||||
RequiredPoints *float64 `json:"required_points,omitempty"`
|
||||
PeriodType *string `json:"period_type,omitempty"`
|
||||
StartDate *string `json:"start_date,omitempty"` // Дата начала подсчёта (YYYY-MM-DD), NULL = за всё время
|
||||
DisplayOrder int `json:"display_order"`
|
||||
// Прогресс выполнения
|
||||
CurrentPoints *float64 `json:"current_points,omitempty"` // Текущее количество баллов (для project_points)
|
||||
@@ -312,7 +312,7 @@ type UnlockConditionRequest struct {
|
||||
TaskID *int `json:"task_id,omitempty"`
|
||||
ProjectID *int `json:"project_id,omitempty"`
|
||||
RequiredPoints *float64 `json:"required_points,omitempty"`
|
||||
PeriodType *string `json:"period_type,omitempty"`
|
||||
StartDate *string `json:"start_date,omitempty"` // Дата начала подсчёта (YYYY-MM-DD), NULL = за всё время
|
||||
DisplayOrder *int `json:"display_order,omitempty"`
|
||||
}
|
||||
|
||||
@@ -2816,6 +2816,12 @@ func (a *App) initAuthDB() error {
|
||||
// Не возвращаем ошибку, чтобы приложение могло запуститься
|
||||
}
|
||||
|
||||
// Apply migration 020: Change period_type to start_date in score_conditions
|
||||
if err := a.applyMigration020(); err != nil {
|
||||
log.Printf("Warning: Failed to apply migration 020: %v", err)
|
||||
// Не возвращаем ошибку, чтобы приложение могло запуститься
|
||||
}
|
||||
|
||||
// Clean up expired refresh tokens (only those with expiration date set)
|
||||
a.DB.Exec("DELETE FROM refresh_tokens WHERE expires_at IS NOT NULL AND expires_at < NOW()")
|
||||
|
||||
@@ -2993,6 +2999,52 @@ func (a *App) applyMigration019() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// applyMigration020 применяет миграцию 020_change_period_to_start_date.sql
|
||||
func (a *App) applyMigration020() error {
|
||||
log.Printf("Applying migration 020: Change period_type to start_date in score_conditions")
|
||||
|
||||
// Проверяем, существует ли уже поле start_date
|
||||
var exists bool
|
||||
err := a.DB.QueryRow(`
|
||||
SELECT EXISTS (
|
||||
SELECT FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'score_conditions'
|
||||
AND column_name = 'start_date'
|
||||
)
|
||||
`).Scan(&exists)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check if start_date exists: %w", err)
|
||||
}
|
||||
if exists {
|
||||
log.Printf("Migration 020 already applied (start_date column exists), skipping")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Читаем SQL файл миграции
|
||||
migrationPath := "/migrations/020_change_period_to_start_date.sql"
|
||||
if _, err := os.Stat(migrationPath); os.IsNotExist(err) {
|
||||
// Пробуем альтернативный путь (для локальной разработки)
|
||||
migrationPath = "play-life-backend/migrations/020_change_period_to_start_date.sql"
|
||||
if _, err := os.Stat(migrationPath); os.IsNotExist(err) {
|
||||
migrationPath = "migrations/020_change_period_to_start_date.sql"
|
||||
}
|
||||
}
|
||||
|
||||
migrationSQL, err := os.ReadFile(migrationPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read migration file %s: %w", migrationPath, err)
|
||||
}
|
||||
|
||||
// Выполняем миграцию
|
||||
if _, err := a.DB.Exec(string(migrationSQL)); err != nil {
|
||||
return fmt.Errorf("failed to execute migration 020: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("Migration 020 applied successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) initPlayLifeDB() error {
|
||||
// Создаем таблицу projects
|
||||
createProjectsTable := `
|
||||
@@ -8427,10 +8479,10 @@ func (a *App) todoistDisconnectHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// Wishlist handlers
|
||||
// ============================================
|
||||
|
||||
// calculateProjectPointsForPeriod считает баллы проекта за указанный период
|
||||
func (a *App) calculateProjectPointsForPeriod(
|
||||
// calculateProjectPointsFromDate считает баллы проекта с указанной даты до текущего момента
|
||||
func (a *App) calculateProjectPointsFromDate(
|
||||
projectID int,
|
||||
periodType sql.NullString,
|
||||
startDate sql.NullTime,
|
||||
userID int,
|
||||
) (float64, error) {
|
||||
var totalScore float64
|
||||
@@ -8442,7 +8494,7 @@ func (a *App) calculateProjectPointsForPeriod(
|
||||
log.Printf("Warning: Failed to refresh materialized view: %v", err)
|
||||
}
|
||||
|
||||
if !periodType.Valid || periodType.String == "" {
|
||||
if !startDate.Valid {
|
||||
// За всё время
|
||||
err = a.DB.QueryRow(`
|
||||
SELECT COALESCE(SUM(wr.total_score), 0)
|
||||
@@ -8451,46 +8503,21 @@ func (a *App) calculateProjectPointsForPeriod(
|
||||
WHERE wr.project_id = $1 AND p.user_id = $2
|
||||
`, projectID, userID).Scan(&totalScore)
|
||||
} else {
|
||||
switch periodType.String {
|
||||
case "week":
|
||||
// Текущая неделя
|
||||
err = a.DB.QueryRow(`
|
||||
SELECT COALESCE(SUM(wr.total_score), 0)
|
||||
FROM weekly_report_mv wr
|
||||
JOIN projects p ON wr.project_id = p.id
|
||||
WHERE wr.project_id = $1
|
||||
AND p.user_id = $2
|
||||
AND wr.report_year = EXTRACT(ISOYEAR FROM CURRENT_DATE)::INTEGER
|
||||
AND wr.report_week = EXTRACT(WEEK FROM CURRENT_DATE)::INTEGER
|
||||
`, projectID, userID).Scan(&totalScore)
|
||||
|
||||
case "month":
|
||||
// Текущий месяц
|
||||
err = a.DB.QueryRow(`
|
||||
SELECT COALESCE(SUM(wr.total_score), 0)
|
||||
FROM weekly_report_mv wr
|
||||
JOIN projects p ON wr.project_id = p.id
|
||||
WHERE wr.project_id = $1
|
||||
AND p.user_id = $2
|
||||
AND wr.report_year = EXTRACT(ISOYEAR FROM CURRENT_DATE)::INTEGER
|
||||
AND wr.report_week >= EXTRACT(WEEK FROM DATE_TRUNC('month', CURRENT_DATE))::INTEGER
|
||||
AND wr.report_week <= EXTRACT(WEEK FROM (DATE_TRUNC('month', CURRENT_DATE) + INTERVAL '1 month' - INTERVAL '1 day'))::INTEGER
|
||||
`, projectID, userID).Scan(&totalScore)
|
||||
|
||||
case "year":
|
||||
// Текущий год
|
||||
err = a.DB.QueryRow(`
|
||||
SELECT COALESCE(SUM(wr.total_score), 0)
|
||||
FROM weekly_report_mv wr
|
||||
JOIN projects p ON wr.project_id = p.id
|
||||
WHERE wr.project_id = $1
|
||||
AND p.user_id = $2
|
||||
AND wr.report_year = EXTRACT(ISOYEAR FROM CURRENT_DATE)::INTEGER
|
||||
`, projectID, userID).Scan(&totalScore)
|
||||
|
||||
default:
|
||||
return 0, fmt.Errorf("unknown period_type: %s", periodType.String)
|
||||
}
|
||||
// С указанной даты до текущего момента
|
||||
// Нужно найти все недели, которые попадают в диапазон от startDate до CURRENT_DATE
|
||||
// Используем сравнение (year, week) >= (startDate_year, startDate_week)
|
||||
err = a.DB.QueryRow(`
|
||||
SELECT COALESCE(SUM(wr.total_score), 0)
|
||||
FROM weekly_report_mv wr
|
||||
JOIN projects p ON wr.project_id = p.id
|
||||
WHERE wr.project_id = $1
|
||||
AND p.user_id = $2
|
||||
AND (
|
||||
wr.report_year > EXTRACT(ISOYEAR FROM $3)::INTEGER
|
||||
OR (wr.report_year = EXTRACT(ISOYEAR FROM $3)::INTEGER
|
||||
AND wr.report_week >= EXTRACT(WEEK FROM $3)::INTEGER)
|
||||
)
|
||||
`, projectID, userID, startDate.Time).Scan(&totalScore)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
@@ -8513,7 +8540,7 @@ func (a *App) checkWishlistUnlock(itemID int, userID int) (bool, error) {
|
||||
tc.task_id,
|
||||
sc.project_id,
|
||||
sc.required_points,
|
||||
sc.period_type
|
||||
sc.start_date
|
||||
FROM wishlist_conditions wc
|
||||
LEFT JOIN task_conditions tc ON wc.task_condition_id = tc.id
|
||||
LEFT JOIN score_conditions sc ON wc.score_condition_id = sc.id
|
||||
@@ -8537,12 +8564,12 @@ func (a *App) checkWishlistUnlock(itemID int, userID int) (bool, error) {
|
||||
var taskID sql.NullInt64
|
||||
var projectID sql.NullInt64
|
||||
var requiredPoints sql.NullFloat64
|
||||
var periodType sql.NullString
|
||||
var startDate sql.NullTime
|
||||
|
||||
err := rows.Scan(
|
||||
&wcID, &displayOrder,
|
||||
&taskConditionID, &scoreConditionID,
|
||||
&taskID, &projectID, &requiredPoints, &periodType,
|
||||
&taskID, &projectID, &requiredPoints, &startDate,
|
||||
)
|
||||
if err != nil {
|
||||
return false, err
|
||||
@@ -8577,9 +8604,9 @@ func (a *App) checkWishlistUnlock(itemID int, userID int) (bool, error) {
|
||||
return false, fmt.Errorf("project_id or required_points missing for score_condition_id=%d", scoreConditionID.Int64)
|
||||
}
|
||||
|
||||
totalScore, err := a.calculateProjectPointsForPeriod(
|
||||
totalScore, err := a.calculateProjectPointsFromDate(
|
||||
int(projectID.Int64),
|
||||
periodType,
|
||||
startDate,
|
||||
userID,
|
||||
)
|
||||
if err != nil {
|
||||
@@ -8623,7 +8650,7 @@ func (a *App) getWishlistItemsWithConditions(userID int, includeCompleted bool)
|
||||
sc.project_id,
|
||||
p.name AS project_name,
|
||||
sc.required_points,
|
||||
sc.period_type
|
||||
sc.start_date
|
||||
FROM wishlist_items wi
|
||||
LEFT JOIN wishlist_conditions wc ON wi.id = wc.wishlist_item_id
|
||||
LEFT JOIN task_conditions tc ON wc.task_condition_id = tc.id
|
||||
@@ -8659,14 +8686,14 @@ func (a *App) getWishlistItemsWithConditions(userID int, includeCompleted bool)
|
||||
var projectID sql.NullInt64
|
||||
var projectName sql.NullString
|
||||
var requiredPoints sql.NullFloat64
|
||||
var periodType sql.NullString
|
||||
var startDate sql.NullTime
|
||||
|
||||
err := rows.Scan(
|
||||
&itemID, &name, &price, &imagePath, &link, &completed,
|
||||
&conditionID, &displayOrder,
|
||||
&taskConditionID, &scoreConditionID,
|
||||
&taskID, &taskName,
|
||||
&projectID, &projectName, &requiredPoints, &periodType,
|
||||
&projectID, &projectName, &requiredPoints, &startDate,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -8716,8 +8743,10 @@ func (a *App) getWishlistItemsWithConditions(userID int, includeCompleted bool)
|
||||
if requiredPoints.Valid {
|
||||
condition.RequiredPoints = &requiredPoints.Float64
|
||||
}
|
||||
if periodType.Valid {
|
||||
condition.PeriodType = &periodType.String
|
||||
if startDate.Valid {
|
||||
// Форматируем дату в YYYY-MM-DD
|
||||
dateStr := startDate.Time.Format("2006-01-02")
|
||||
condition.StartDate = &dateStr
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8767,15 +8796,15 @@ func (a *App) getWishlistItemsWithConditions(userID int, includeCompleted bool)
|
||||
// Находим project_id и required_points для этого условия
|
||||
var projectID int
|
||||
var requiredPoints float64
|
||||
var periodType sql.NullString
|
||||
var startDate sql.NullTime
|
||||
err = a.DB.QueryRow(`
|
||||
SELECT sc.project_id, sc.required_points, sc.period_type
|
||||
SELECT sc.project_id, sc.required_points, sc.start_date
|
||||
FROM wishlist_conditions wc
|
||||
JOIN score_conditions sc ON wc.score_condition_id = sc.id
|
||||
WHERE wc.id = $1
|
||||
`, condition.ID).Scan(&projectID, &requiredPoints, &periodType)
|
||||
`, condition.ID).Scan(&projectID, &requiredPoints, &startDate)
|
||||
if err == nil {
|
||||
totalScore, err := a.calculateProjectPointsForPeriod(projectID, periodType, userID)
|
||||
totalScore, err := a.calculateProjectPointsFromDate(projectID, startDate, userID)
|
||||
conditionMet = err == nil && totalScore >= requiredPoints
|
||||
if err == nil {
|
||||
condition.CurrentPoints = &totalScore
|
||||
@@ -8819,15 +8848,15 @@ func (a *App) getWishlistItemsWithConditions(userID int, includeCompleted bool)
|
||||
} else if condition.Type == "project_points" {
|
||||
var projectID int
|
||||
var requiredPoints float64
|
||||
var periodType sql.NullString
|
||||
var startDate sql.NullTime
|
||||
err := a.DB.QueryRow(`
|
||||
SELECT sc.project_id, sc.required_points, sc.period_type
|
||||
SELECT sc.project_id, sc.required_points, sc.start_date
|
||||
FROM wishlist_conditions wc
|
||||
JOIN score_conditions sc ON wc.score_condition_id = sc.id
|
||||
WHERE wc.id = $1
|
||||
`, condition.ID).Scan(&projectID, &requiredPoints, &periodType)
|
||||
`, condition.ID).Scan(&projectID, &requiredPoints, &startDate)
|
||||
if err == nil {
|
||||
totalScore, err := a.calculateProjectPointsForPeriod(projectID, periodType, userID)
|
||||
totalScore, err := a.calculateProjectPointsFromDate(projectID, startDate, userID)
|
||||
if err == nil {
|
||||
condition.CurrentPoints = &totalScore
|
||||
}
|
||||
@@ -8914,34 +8943,35 @@ func (a *App) saveWishlistConditions(
|
||||
return fmt.Errorf("project_id and required_points are required for project_points")
|
||||
}
|
||||
|
||||
periodType := condition.PeriodType
|
||||
startDateStr := condition.StartDate
|
||||
|
||||
// Получаем или создаём score_condition
|
||||
var scID int
|
||||
var periodTypeVal interface{}
|
||||
if periodType != nil && *periodType != "" {
|
||||
periodTypeVal = *periodType
|
||||
var startDateVal interface{}
|
||||
if startDateStr != nil && *startDateStr != "" {
|
||||
// Парсим дату из строки YYYY-MM-DD
|
||||
startDateVal = *startDateStr
|
||||
} else {
|
||||
// Пустая строка или nil = NULL для "за всё время"
|
||||
periodTypeVal = nil
|
||||
startDateVal = nil
|
||||
}
|
||||
|
||||
err := tx.QueryRow(`
|
||||
SELECT id FROM score_conditions
|
||||
WHERE project_id = $1
|
||||
AND required_points = $2
|
||||
AND (period_type = $3 OR (period_type IS NULL AND $3 IS NULL))
|
||||
`, *condition.ProjectID, *condition.RequiredPoints, periodTypeVal).Scan(&scID)
|
||||
AND (start_date = $3::DATE OR (start_date IS NULL AND $3 IS NULL))
|
||||
`, *condition.ProjectID, *condition.RequiredPoints, startDateVal).Scan(&scID)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
// Создаём новое условие
|
||||
err = tx.QueryRow(`
|
||||
INSERT INTO score_conditions (project_id, required_points, period_type)
|
||||
VALUES ($1, $2, $3)
|
||||
ON CONFLICT (project_id, required_points, period_type)
|
||||
INSERT INTO score_conditions (project_id, required_points, start_date)
|
||||
VALUES ($1, $2, $3::DATE)
|
||||
ON CONFLICT (project_id, required_points, start_date)
|
||||
DO UPDATE SET project_id = EXCLUDED.project_id
|
||||
RETURNING id
|
||||
`, *condition.ProjectID, *condition.RequiredPoints, periodTypeVal).Scan(&scID)
|
||||
`, *condition.ProjectID, *condition.RequiredPoints, startDateVal).Scan(&scID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user