# План: Создать общие функции расчета и форматирования срока разблокировки

## Цель

Создать универсальные функции для расчета и форматирования срока разблокировки проекта, которые будут использоваться везде где необходимо считать остаточный срок.

## Изменения

### 1. Создать функцию расчета срока разблокировки (бэкенд)

**Файл:** `play-life-backend/main.go`

Создать функцию `calculateProjectUnlockWeeks`:

```go
// calculateProjectUnlockWeeks рассчитывает срок разблокировки проекта в неделях
// projectID - ID проекта
// requiredPoints - необходимое количество баллов
// startDate - дата начала подсчета (может быть nil - за всё время)
// userID - ID пользователя (владельца условия)
// Возвращает количество недель (float64):
//   - > 0: условие не выполнено, возвращает количество недель
//   - 0: условие уже выполнено (remaining <= 0)
//   - 99999: медиана отсутствует или равна 0 (нельзя рассчитать)
func (a *App) calculateProjectUnlockWeeks(projectID int, requiredPoints float64, startDate sql.NullTime, userID int) float64 {
    // 1. Получаем текущие баллы от startDate
    currentPoints, err := a.calculateProjectPointsFromDate(projectID, startDate, userID)
    if err != nil {
        log.Printf("Error calculating project points for project %d, user %d: %v", projectID, userID, err)
        return 99999 // Ошибка расчета - возвращаем 99999
    }
    
    // 2. Вычисляем остаток
    remaining := requiredPoints - currentPoints
    if remaining <= 0 {
        // Условие уже выполнено
        return 0
    }
    
    // 3. Получаем медиану проекта
    median, err := a.getProjectMedian(projectID)
    if err != nil || median <= 0 {
        // Если медиана отсутствует или равна 0, возвращаем 99999 (нельзя рассчитать)
        // Это нормальная ситуация, не логируем
        return 99999
    }
    
    // 4. Рассчитываем недели
    weeks := remaining / median
    return weeks
}
```

**Примечание:** Функция возвращает:

- `0`: условие уже выполнено (remaining <= 0)
- `> 0 && < 99999`: количество недель до выполнения условия
- `99999`: медиана отсутствует или равна 0 (нельзя рассчитать) или ошибка расчета

````

### 2. Создать функцию форматирования срока (бэкенд)

**Файл:** `play-life-backend/main.go`

Создать функцию `formatWeeksText`:

```go
// formatWeeksText форматирует количество недель в текстовый формат
// weeks - количество недель (float64)
// Возвращает строку: "2 недели", "<1 недели", "5 недель", "∞ недель" и т.д.
func formatWeeksText(weeks float64) string {
    // Если weeks == 0, условие уже выполнено - не показываем срок
    if weeks == 0 {
        return ""
    }
    
    // Если weeks >= 99999, это означает что медиана отсутствует или нельзя рассчитать
    if weeks >= 99999 {
        return "∞ недель"
    }
    
    if weeks < 0 {
        return ""
    }
    
    if weeks < 1 {
        return "<1 недели"
    }
    
    weeksRounded := math.Round(weeks)
    weeksInt := int(weeksRounded)
    
    // Правильное склонение для русского языка
    var weekWord string
    lastDigit := weeksInt % 10
    lastTwoDigits := weeksInt % 100
    
    if lastTwoDigits >= 11 && lastTwoDigits <= 14 {
        weekWord = "недель"
    } else if lastDigit == 1 {
        weekWord = "неделя"
    } else if lastDigit >= 2 && lastDigit <= 4 {
        weekWord = "недели"
    } else {
        weekWord = "недель"
    }
    
    return fmt.Sprintf("%d %s", weeksInt, weekWord)
}
```

**Примечание:**

- Форматирование на бэкенде, так как сортировка происходит на бэкенде. Фронтенд получает уже отформатированную строку.
- При `weeks == 0` (условие выполнено) возвращается пустая строка (не показываем срок)
- При `weeks >= 99999` (медиана отсутствует, нельзя рассчитать или ошибка расчета) возвращается "∞ недель"

### 3. Использовать функции в calculateLockedSortValue

**Файл:** `play-life-backend/main.go` (строки 12314-12337)

Обновить функцию для использования `calculateProjectUnlockWeeks`:

```go
func (a *App) calculateLockedSortValue(item WishlistItem, userID int) float64 {
    // Если нет условий, возвращаем большое значение (отсутствие условий = все выполнены)
    if len(item.UnlockConditions) == 0 {
        return 999999.0
    }
    
    maxWeeks := 0.0
    hasProjectConditions := false
    
    for _, condition := range item.UnlockConditions {
        if condition.Type == "project_points" {
            hasProjectConditions = true
            if condition.RequiredPoints != nil {
                var startDate sql.NullTime
                if condition.StartDate != nil {
                    date, err := time.Parse("2006-01-02", *condition.StartDate)
                    if err == nil {
                        startDate = sql.NullTime{Time: date, Valid: true}
                    }
                }
                
                // ВАЖНО: Используем владельца условия из condition.UserID
                // Если condition.UserID есть - это владелец условия
                // Если нет - получаем владельца желания из БД (для старых условий)
                // НЕ используем текущего пользователя (userID), так как условие может принадлежать другому пользователю
                conditionOwnerID := 0
                if condition.UserID != nil {
                    conditionOwnerID = *condition.UserID
                } else {
                    // Если нет владельца условия, получаем владельца желания из БД
                    var itemOwnerID int
                    err := a.DB.QueryRow(`SELECT user_id FROM wishlist_items WHERE id = $1`, item.ID).Scan(&itemOwnerID)
                    if err != nil {
                        log.Printf("Error getting wishlist item owner for item %d: %v", item.ID, err)
                        continue // Пропускаем условие, если не можем получить владельца
                    }
                    conditionOwnerID = itemOwnerID
                }
                
                // Получаем projectID из условия
                if condition.ProjectID != nil {
                    weeks := a.calculateProjectUnlockWeeks(
                        *condition.ProjectID,
                        *condition.RequiredPoints,
                        startDate,
                        conditionOwnerID, // Владелец условия, а не текущий пользователь
                    )
                    // weeks > 0 && < 99999 означает, что условие еще не выполнено и расчет успешен
                    // weeks == 0 означает условие выполнено
                    // weeks == 99999 означает медиана отсутствует (нельзя рассчитать) или ошибка расчета
                    if weeks > 0 && weeks < 99999 {
                        if weeks > maxWeeks {
                            maxWeeks = weeks
                        }
                    }
                }
            }
        }
    }
    
    // Если были условия по проектам, но все выполнены (maxWeeks = 0)
    if hasProjectConditions && maxWeeks == 0.0 {
        return 999999.0
    }
    
    // Если не было условий по проектам (только задачи или нет условий)
    if !hasProjectConditions {
        return 999999.0
    }
    
    return maxWeeks
}
```

### 4. Использовать функции в API endpoint для расчета недель

**Файл:** `play-life-backend/main.go`

Обновить endpoint `/api/wishlist/calculate-weeks` (из плана "добавить расчет недель в форму"):

**Важно:** Использовать владельца условия, а не текущего пользователя!

```go
func (a *App) calculateWeeksHandler(w http.ResponseWriter, r *http.Request) {
    // ... валидация и получение параметров ...
    
    // Определяем владельца условия:
    // 1. Если передан condition_user_id в запросе - используем его (для существующего условия)
    // 2. Иначе используем текущего пользователя (для нового условия)
    conditionOwnerID := userID // userID из контекста (текущий пользователь)
    if req.ConditionUserID != nil && *req.ConditionUserID > 0 {
        conditionOwnerID = *req.ConditionUserID
    }
    
    var startDate sql.NullTime
    if req.StartDate != "" {
        date, err := time.Parse("2006-01-02", req.StartDate)
        if err == nil {
            startDate = sql.NullTime{Time: date, Valid: true}
        }
    }
    
    // Используем владельца условия, а не текущего пользователя
    weeks := a.calculateProjectUnlockWeeks(req.ProjectID, req.RequiredPoints, startDate, conditionOwnerID)
    
    response := map[string]interface{}{
        "weeks_text": formatWeeksText(weeks), // Отформатированная строка для отображения
    }
    
    // weeks используется только для сортировки на бэкенде, на клиент не отправляется
    
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(response)
}
```

**Структура запроса:**

```go
type CalculateWeeksRequest struct {
    ProjectID        int     `json:"project_id"`
    RequiredPoints   float64 `json:"required_points"`
    StartDate        string  `json:"start_date,omitempty"`
    ConditionUserID  *int    `json:"condition_user_id,omitempty"` // Владелец условия (если условие существует)
}
```

### 5. Добавить weeks_text в UnlockConditionDisplay

**Файл:** `play-life-backend/main.go`

Добавить поле `WeeksText *string` в структуру `UnlockConditionDisplay`:

```go
type UnlockConditionDisplay struct {
    // ... существующие поля ...
    WeeksText *string `json:"weeks_text,omitempty"` // Отформатированный текст срока разблокировки
}
```

При загрузке условий типа `project_points` рассчитывать и форматировать срок:

```go
if condition.Type == "project_points" && condition.RequiredPoints != nil && condition.ProjectID != nil {
    var startDate sql.NullTime
    if condition.StartDate != nil {
        date, err := time.Parse("2006-01-02", *condition.StartDate)
        if err == nil {
            startDate = sql.NullTime{Time: date, Valid: true}
        }
    }
    
    // ВАЖНО: Используем владельца условия из condition.UserID, а не текущего пользователя
    // Если condition.UserID есть - это владелец условия
    // Если нет - используем владельца желания (itemOwnerID), но НЕ текущего пользователя (userID)
    conditionOwnerID := itemOwnerID // Владелец желания как fallback
    if condition.UserID != nil {
        conditionOwnerID = *condition.UserID // Владелец условия (приоритет)
    }
    
    weeks := a.calculateProjectUnlockWeeks(
        *condition.ProjectID,
        *condition.RequiredPoints,
        startDate,
        conditionOwnerID, // Владелец условия, а не текущий пользователь
    )
    
    // Форматируем всегда (при weeks == 0 вернет пустую строку, при weeks >= 99999 вернет "∞ недель")
    weeksText := formatWeeksText(weeks)
    condition.WeeksText = &weeksText
}
```

**Важно:**

- `condition.UserID` - это владелец условия (из `wishlist_conditions.user_id`)
- `itemOwnerID` - это владелец желания (fallback для старых условий)
- `userID` (текущий пользователь) НЕ используется, так как условие может принадлежать другому пользователю

### 6. Использовать weeks_text на фронтенде

**Файл:** `play-life-web/src/components/WishlistDetail.jsx`

Использовать готовый `weeks_text` из условия (приходит уже отформатированным из API):

```javascript
// В renderUnlockConditions:
{progress.remaining > 0 && condition.weeks_text && (
  <span className="progress-remaining">
    Осталось: {Math.round(progress.remaining)} ({condition.weeks_text})
  </span>
)}
```

**Файл:** `play-life-web/src/components/WishlistForm.jsx`

Использовать `weeks_text` из ответа API для отображения недель в форме редактирования условия. Форматирование уже выполнено на бэкенде.

### 7. Обновить загрузку медианы в условиях (опционально)

**Файл:** `play-life-backend/main.go`

При загрузке условий типа `project_points` медиана не нужна отдельно, так как `calculateProjectUnlockWeeks` сама получит её и вернет уже отформатированный `weeks_text`.

## Места использования функций

1. **calculateProjectUnlockWeeks** (бэкенд):

   - `calculateLockedSortValue` - для сортировки заблокированных желаний (использует числовое значение)
   - `calculateWeeksHandler` - API endpoint для расчета недель (использует для расчета, но на клиент отправляется только отформатированная строка)
   - При загрузке условий для расчета `weeks_text` (используется внутри, на клиент не отправляется)
   - Любые другие места, где нужно рассчитать срок разблокировки

2. **formatWeeksText** (бэкенд):

   - При загрузке условий в `UnlockConditionDisplay.WeeksText` (отправляется на клиент для отображения)
   - В API endpoint `/api/wishlist/calculate-weeks` (отправляется на клиент для отображения в форме)
   - Форматирование на бэкенде, так как сортировка происходит на бэкенде по числовому значению `weeks`

## Выявленные и исправленные проблемы

1. **Проблема с userID в calculateLockedSortValue**: 

   - **Проблема**: Использовался текущий пользователь (`userID`), но условие может принадлежать другому пользователю
   - **Исправление**: Используется `conditionOwnerID` из `condition.UserID` (владелец условия). Если `condition.UserID` отсутствует, условие пропускается (некорректное состояние)

2. **Обработка отсутствия медианы**:

   - **Решение**: При отсутствии медианы возвращается `99999` (нельзя рассчитать). В `formatWeeksText` это значение преобразуется в "∞ недель". Такие условия не учитываются при сортировке по времени разблокировки (проверка `weeks > 0 && weeks < 99999`)

3. **Форматирование и передача данных**:

   - **Решение**: Форматирование на бэкенде, так как сортировка происходит на бэкенде по числовому значению `weeks`
   - Числовое значение `weeks` используется только на бэкенде для сортировки, на клиент не отправляется
   - На клиент отправляется только отформатированная строка `weeks_text` для отображения
   - Фронтенд просто отображает готовую строку без дополнительного форматирования
   - Это исключает дублирование логики и обеспечивает единообразие форматирования

4. **Использование правильного userID (владельца условия)**:

   - **Проблема**: В функцию `calculateProjectUnlockWeeks` может передаваться текущий пользователь вместо владельца условия
   - **Решение**: 
     - В `calculateLockedSortValue`: используется `condition.UserID` (владелец условия)
     - В `calculateWeeksHandler`: используется `condition_user_id` из запроса (если передан) или текущий пользователь (для нового условия)
     - При загрузке условий: используется `condition.UserID` или `itemOwnerID` (владелец желания), но НЕ текущий пользователь
   - **Важно**: Условие может принадлежать другому пользователю (на общих досках), поэтому нужно использовать именно владельца условия

## Зависимости

- Функция `getProjectMedian` должна быть создана (из плана сортировки)
- Функция `calculateProjectPointsFromDate` уже существует

## Финальный шаг: Перезапуск приложения

**После выполнения всех изменений:**

Выполнить команду для перезапуска фронтенда и бэкенда:

```bash
./run.sh
```

Это пересоберет и перезапустит:

- Backend сервер (с пересборкой)
- Frontend приложение (с пересборкой)
- База данных