Первоначальный коммит

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
poignatov-home
2026-02-08 17:01:36 +03:00
commit bad198ce29
217 changed files with 57075 additions and 0 deletions

33
.cursor/commands.json Normal file
View File

@@ -0,0 +1,33 @@
{
"commands": [
{
"name": "init",
"description": "Инициализация Play Life: остановка контейнеров, поднятие сервисов, создание дампа с продакшена и восстановление в локальную базу",
"command": "./init.sh",
"type": "shell",
"cwd": "${workspaceFolder}"
},
{
"name": "run",
"description": "Перезапуск Play Life: перезапуск всех контейнеров",
"command": "./run.sh",
"type": "shell",
"cwd": "${workspaceFolder}"
},
{
"name": "backupFromProd",
"description": "Создание дампа базы данных с продакшена",
"command": "./dump-db.sh",
"type": "shell",
"cwd": "${workspaceFolder}"
},
{
"name": "restoreToLocal",
"description": "Восстановление базы данных из самого свежего дампа в локальную базу (автоматически выбирает последний дамп)",
"command": "./restore-db.sh",
"type": "shell",
"cwd": "${workspaceFolder}"
}
]
}

View File

@@ -0,0 +1,260 @@
# План: Изменить сортировку заблокированных желаний по времени разблокировки
## Цель
Изменить сортировку желаний:
1. Разблокированные - по цене от меньшего к большему
2. Заблокированные без целей-задач - по сроку разблокировки (максимальное время среди проектов)
3. Заблокированные с целями-задачами - по сроку разблокировки (максимальное время среди проектов)
## Статус реализации
**Уже реализовано:**
-`calculateProjectUnlockWeeks` - функция расчета недель разблокировки
-`calculateLockedSortValue` - использует `calculateProjectUnlockWeeks` и возвращает недели
-`getProjectMedian` - упрощенная версия без fallback (используется как есть)
**Требуется реализовать:**
- ⏳ Создать миграцию для `projects_median_mv` (миграции нет, но используется в коде)
-В `getWishlistHandler`: заменить `calculateUnlockedSortValue` на прямую сортировку по цене для разблокированных
-В `getWishlistHandler`: разделить заблокированные на группы (с задачами/без задач) и сортировать каждую группу
-В `getBoardItemsHandler`: заменить `calculateUnlockedSortValue` на прямую сортировку по цене для разблокированных
-В `getBoardItemsHandler`: разделить заблокированные на группы (с задачами/без задач) и сортировать каждую группу
## Изменения
### 1. Создать миграцию для projects_median_mv
**Статус:** `getProjectMedian` уже использует `projects_median_mv`, но миграции для неё нет в списке миграций. Нужно создать миграцию.
**Файл:** `play-life-backend/migrations/000007_add_projects_median_mv.up.sql`
Убедиться, что materialized view включает `user_id`:
```sql
CREATE MATERIALIZED VIEW projects_median_mv AS
SELECT
p.id AS project_id,
p.user_id,
PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY normalized_total_score) AS median_score
FROM (
SELECT
project_id,
normalized_total_score,
report_year,
report_week,
ROW_NUMBER() OVER (PARTITION BY project_id ORDER BY report_year DESC, report_week DESC) as rn
FROM weekly_report_mv
WHERE
(report_year < EXTRACT(ISOYEAR FROM CURRENT_DATE)::INTEGER)
OR (report_year = EXTRACT(ISOYEAR FROM CURRENT_DATE)::INTEGER
AND report_week < EXTRACT(WEEK FROM CURRENT_DATE)::INTEGER)
) sub
JOIN projects p ON p.id = sub.project_id
WHERE rn <= 12 AND p.deleted = FALSE
GROUP BY p.id, p.user_id
WITH DATA;
CREATE INDEX idx_projects_median_mv_project_id ON projects_median_mv(project_id);
CREATE INDEX idx_projects_median_mv_user_id ON projects_median_mv(user_id);
```
**Файл:** `play-life-backend/migrations/000007_add_projects_median_mv.down.sql`
```sql
DROP MATERIALIZED VIEW IF EXISTS projects_median_mv;
```
### 2. Изменить calculateLockedSortValue для расчета времени
**Файл:** `play-life-backend/main.go` (строки 12488-12561)
**Статус:** Функция уже реализована и использует `calculateProjectUnlockWeeks`. Проверить, что логика соответствует требованиям:
- Учитывает только условия типа `project_points`
- Использует правильного владельца условия (`conditionOwnerID`)
- Возвращает максимальное количество недель среди всех условий проектов
- Возвращает 999999.0 если нет условий по проектам или все выполнены
**Текущая реализация уже корректна**, изменения не требуются.
**Важно:**
- Функция уже использует `calculateProjectUnlockWeeks` для расчета (уже реализовано)
- Функция НЕ должна учитывать задачи, только проекты. Разделение на группы с задачами и без задач будет в сортировке.
- Функция уже правильно обрабатывает владельца условия через `conditionOwnerID` (не использует `userID` напрямую)
### 3. Обновить сортировку в getWishlistHandler
**Файл:** `play-life-backend/main.go` (строки 9933-9951)
**Текущее состояние:**
- Разблокированные: используют `calculateUnlockedSortValue` (сумма баллов) - **нужно заменить на цену**
- Заблокированные: сортируются по `calculateLockedSortValue` (недели) - **нужно разделить на группы**
**Изменить:**
1. Разблокированные: сортировка по цене от меньшего к большему (заменить `calculateUnlockedSortValue`)
2. Заблокированные: разделить на группы (с задачами/без задач) и сортировать каждую группу по времени
```go
// Сортируем разблокированные по цене от меньшего к большему
// ЗАМЕНА: было calculateUnlockedSortValue, стало прямая сортировка по цене
sort.Slice(unlocked, func(i, j int) bool {
priceI := 0.0
priceJ := 0.0
if unlocked[i].Price != nil {
priceI = *unlocked[i].Price
}
if unlocked[j].Price != nil {
priceJ = *unlocked[j].Price
}
if priceI == priceJ {
return unlocked[i].ID < unlocked[j].ID
}
return priceI < priceJ // Сортировка по цене от меньшего к большему (заменяет calculateUnlockedSortValue)
})
// Разделяем заблокированные на группы
lockedWithoutTasks := []WishlistItem{}
lockedWithTasks := []WishlistItem{}
for _, item := range locked {
hasUncompletedTasks := false
for _, cond := range item.UnlockConditions {
if cond.Type == "task_completion" && (cond.TaskCompleted == nil || !*cond.TaskCompleted) {
hasUncompletedTasks = true
break
}
}
if hasUncompletedTasks {
lockedWithTasks = append(lockedWithTasks, item)
} else {
lockedWithoutTasks = append(lockedWithoutTasks, item)
}
}
// Сортируем каждую группу по времени разблокировки
sort.Slice(lockedWithoutTasks, func(i, j int) bool {
valueI := a.calculateLockedSortValue(lockedWithoutTasks[i], userID)
valueJ := a.calculateLockedSortValue(lockedWithoutTasks[j], userID)
if valueI == valueJ {
return lockedWithoutTasks[i].ID < lockedWithoutTasks[j].ID
}
return valueI < valueJ
})
sort.Slice(lockedWithTasks, func(i, j int) bool {
valueI := a.calculateLockedSortValue(lockedWithTasks[i], userID)
valueJ := a.calculateLockedSortValue(lockedWithTasks[j], userID)
if valueI == valueJ {
return lockedWithTasks[i].ID < lockedWithTasks[j].ID
}
return valueI < valueJ
})
// Объединяем: сначала без задач, потом с задачами
locked = append(lockedWithoutTasks, lockedWithTasks...)
```
### 4. Обновить сортировку в getBoardItemsHandler
**Файл:** `play-life-backend/main.go` (строки 12222-12240)
**Текущее состояние:**
- Разблокированные: используют `calculateUnlockedSortValue` (сумма баллов) - **нужно заменить на цену**
- Заблокированные: сортируются по `calculateLockedSortValue` (недели) - **нужно разделить на группы**
**Изменить аналогично getWishlistHandler:**
1. Разблокированные: сортировка по цене от меньшего к большему (заменить `calculateUnlockedSortValue`)
2. Заблокированные: разделить на группы (с задачами/без задач) и сортировать каждую группу по времени
```go
// Сортируем разблокированные по цене от меньшего к большему
// ЗАМЕНА: было calculateUnlockedSortValue, стало прямая сортировка по цене
sort.Slice(unlocked, func(i, j int) bool {
priceI := 0.0
priceJ := 0.0
if unlocked[i].Price != nil {
priceI = *unlocked[i].Price
}
if unlocked[j].Price != nil {
priceJ = *unlocked[j].Price
}
if priceI == priceJ {
return unlocked[i].ID < unlocked[j].ID
}
return priceI < priceJ
})
// РАЗДЕЛЕНИЕ НА ГРУППЫ: Заблокированные с задачами и без задач
// ЗАМЕНА: было просто sort.Slice(locked, ...), стало разделение на группы
lockedWithoutTasks := []WishlistItem{}
lockedWithTasks := []WishlistItem{}
for _, item := range locked {
hasUncompletedTasks := false
for _, cond := range item.UnlockConditions {
if cond.Type == "task_completion" && (cond.TaskCompleted == nil || !*cond.TaskCompleted) {
hasUncompletedTasks = true
break
}
}
if hasUncompletedTasks {
lockedWithTasks = append(lockedWithTasks, item)
} else {
lockedWithoutTasks = append(lockedWithoutTasks, item)
}
}
// Сортируем каждую группу по времени разблокировки
sort.Slice(lockedWithoutTasks, func(i, j int) bool {
valueI := a.calculateLockedSortValue(lockedWithoutTasks[i], userID)
valueJ := a.calculateLockedSortValue(lockedWithoutTasks[j], userID)
if valueI == valueJ {
return lockedWithoutTasks[i].ID < lockedWithoutTasks[j].ID
}
return valueI < valueJ
})
sort.Slice(lockedWithTasks, func(i, j int) bool {
valueI := a.calculateLockedSortValue(lockedWithTasks[i], userID)
valueJ := a.calculateLockedSortValue(lockedWithTasks[j], userID)
if valueI == valueJ {
return lockedWithTasks[i].ID < lockedWithTasks[j].ID
}
return valueI < valueJ
})
// Объединяем: сначала без задач, потом с задачами
locked = append(lockedWithoutTasks, lockedWithTasks...)
```
## Итоговый порядок элементов
1. **Разблокированные** - отсортированы по цене от меньшего к большему
2. **Заблокированные без целей-задач** - отсортированы по максимальному времени разблокировки (среди всех проектов) от меньшего к большему
3. **Заблокированные с целями-задачами** - отсортированы по максимальному времени разблокировки (среди всех проектов) от меньшего к большему
## Обработка краевых случаев
- **Если медиана проекта = 0 или отсутствует**: `calculateProjectUnlockWeeks` возвращает 99999, что обрабатывается в `calculateLockedSortValue` (не учитывается в maxWeeks, если >= 99999)
- **Если нет условий**: возвращать 999999.0 (отсутствие условий = все условия выполнены)
- **Если все условия выполнены**: возвращать 999999.0
- **Если цена не указана (NULL)**: считать как 0.0
- **Если нет условий по проектам** (только задачи или нет условий): возвращать 999999.0
## Зависимости
- `projects_median_mv` должна существовать (проверить наличие миграции или создать при необходимости)
- Функция `getProjectMedian` уже реализована (упрощенная версия без fallback)
- Функция `calculateProjectUnlockWeeks` уже реализована и используется в `calculateLockedSortValue`
## Финальный шаг: Перезапуск приложения
**После выполнения всех изменений:**
Выполнить команду для перезапуска фронтенда и бэкенда:
```bash
./run.sh
```
Это пересоберет и перезапустит:
- Backend сервер (с пересборкой)
- Frontend приложение (с пересборкой)
- База данных

View File

@@ -0,0 +1,392 @@
# План: Создать общие функции расчета и форматирования срока разблокировки
## Цель
Создать универсальные функции для расчета и форматирования срока разблокировки проекта, которые будут использоваться везде где необходимо считать остаточный срок.
## Изменения
### 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 приложение (с пересборкой)
- База данных

View File

@@ -0,0 +1,8 @@
---
description: "Запрет доработок старых миграций"
alwaysApply: true
---
**ВАЖНО:** Если ты меняешь структуру базы данных - напиши НОВУЮ миграцию.
НИ В КОЕМ СЛУЧАЕ не меняй старые миграции, можно добавлять только новые.
Старой миграцией считается та что была уже ранее закомичена

View File

@@ -0,0 +1,16 @@
---
description: "Перезапуск приложения после изменений в бэкенде или фронтенде"
alwaysApply: true
---
## Правило перезапуска приложения
**ВАЖНО:** После применения всех изменений в бэкенде (`play-life-backend/`) или фронтенде (`play-life-web/`), а также после изменений в `docker-compose.yml`, **ОБЯЗАТЕЛЬНО** выполни команду `./run.sh` для перезапуска всех сервисов приложения.
Это правило применяется при работе с:
- Go кодом в `play-life-backend/`
- Миграциями базы данных в `play-life-backend/migrations/`
- React компонентами и стилями в `play-life-web/src/`
- Docker конфигурациями (`docker-compose.yml`, `Dockerfile`)
**Команда для перезапуска:** `./run.sh` или `bash run.sh` в корне проекта.

View File

@@ -0,0 +1,71 @@
---
description: "Правило для поднятия версии и пуша в git"
alwaysApply: true
---
## Правило поднятия версии и пуша
Когда пользователь просит **поднять версию и запушить**, выполни следующие шаги:
### 1. Определи тип версии
Определи по сообщению пользователя, какую часть версии нужно поднять:
- **major** (мажор) - первая цифра (например: 1.1.1 → 2.0.0), минор и патч должны обнулиться
- **minor** (минор) - вторая цифра (например: 1.0.1 → 1.1.0), патч должна обнулиться
- **patch** (патч) - третья цифра (например: 1.0.0 → 1.0.1)
Любая часть версии может быть больше 9, то есть может быть версия 10, 11, 12 и тд.
**Если тип версии непонятен из контекста — обязательно спроси у пользователя!**
### 2. Обнови версию в файлах
Обнови версию в двух файлах:
- `VERSION` (в корне проекта)
- `play-life-web/package.json` (поле `"version"`)
### 3. Проанализируй git diff
Выполни `git diff --staged` и `git diff` для анализа изменений. На основе изменений составь **короткий commit message** (максимум 50 символов) на русском языке, описывающий суть изменений. В начале commit message должна быть указана версия на которую осуществился переход в формате "1.2.3: Коммит мессадж"
### 4. Закоммить изменения
Выполни:
```bash
git add -A
git commit -m "<commit message>"
```
### 5. Запушь в репозиторий
Выполни:
```bash
git push
```
## Правило пуша без поднятия версии
### 1. Проанализируй git diff
Выполни `git diff --staged` и `git diff` для анализа изменений. На основе изменений составь **короткий commit message** (максимум 50 символов) на русском языке, описывающий суть изменений
### 2. Закоммить изменения
Выполни:
```bash
git add -A
git commit -m "<commit message>"
```
### 3. Запушь в репозиторий
Выполни:
```bash
git push
```
---
**Пример использования:**
- "Подними патч и запушь" → поднять patch версию
- "Bump minor and push" → поднять minor версию
- "Подними версию и запушь" → спросить какой тип версии поднять
- "Запуш именения" → запушить без изменения версии